Skip to content

Commit e338787

Browse files
committed
Honor slave attributes as contrsaints, fixes #47
1 parent a2eb166 commit e338787

File tree

6 files changed

+117
-66
lines changed

6 files changed

+117
-66
lines changed

satyr/constraint.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from __future__ import absolute_import, division, print_function
2+
3+
from .utils import partition
4+
5+
6+
def pour(offers):
7+
return offers, [] # accepts all offers
8+
9+
10+
def has(offers, attribute):
11+
def pred(offer):
12+
for attrib in offer.attributes:
13+
if attrib.name == attribute:
14+
return True
15+
return False
16+
17+
return partition(pred, offers)

satyr/messages.py

Lines changed: 57 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -40,62 +40,6 @@ def exception(self):
4040
return None
4141

4242

43-
class PythonExecutor(ExecutorInfo):
44-
45-
proto = mesos_pb2.ExecutorInfo(
46-
labels=mesos_pb2.Labels(
47-
labels=[mesos_pb2.Label(key='python')]))
48-
49-
def __init__(self, docker='satyr', force_pull=False, envs={}, uris=[], **kwds):
50-
super(PythonExecutor, self).__init__(**kwds)
51-
self.container = ContainerInfo(
52-
type='MESOS',
53-
mesos=ContainerInfo.MesosInfo(
54-
image=Image(type='DOCKER',
55-
docker=Image.Docker())))
56-
self.command = CommandInfo(value='python -m satyr.executor',
57-
shell=True)
58-
self.force_pull = force_pull
59-
self.docker = docker
60-
self.envs = envs
61-
self.uris = uris
62-
63-
@property
64-
def docker(self):
65-
return self.container.mesos.image.docker.name
66-
67-
@docker.setter
68-
def docker(self, value):
69-
self.container.mesos.image.docker.name = value
70-
71-
@property
72-
def force_pull(self):
73-
# cached is the opposite of force pull image
74-
return not self.container.mesos.image.cached
75-
76-
@force_pull.setter
77-
def force_pull(self, value):
78-
self.container.mesos.image.cached = not value
79-
80-
@property
81-
def uris(self):
82-
return [uri.value for uri in self.command.uris]
83-
84-
@uris.setter
85-
def uris(self, value):
86-
self.command.uris = [{'value': v} for v in value]
87-
88-
@property
89-
def envs(self):
90-
envs = self.command.environment.variables
91-
return {env.name: env.value for env in envs}
92-
93-
@envs.setter
94-
def envs(self, value):
95-
envs = [{'name': k, 'value': v} for k, v in value.items()]
96-
self.command.environment = Environment(variables=envs)
97-
98-
9943
# TODO create custom messages per executor
10044
class PythonTask(PickleMixin, TaskInfo):
10145

@@ -160,3 +104,60 @@ def on_fail(self, status):
160104
else:
161105
logging.error('Aborting due to task {} failed with state {} and message '
162106
'{}'.format(self.id, status.state, status.message))
107+
108+
109+
class PythonExecutor(ExecutorInfo):
110+
111+
proto = mesos_pb2.ExecutorInfo(
112+
labels=mesos_pb2.Labels(
113+
labels=[mesos_pb2.Label(key='python')]))
114+
115+
def __init__(self, docker='satyr', force_pull=False,
116+
envs={}, uris=[], **kwds):
117+
super(PythonExecutor, self).__init__(**kwds)
118+
self.container = ContainerInfo(
119+
type='MESOS',
120+
mesos=ContainerInfo.MesosInfo(
121+
image=Image(type='DOCKER',
122+
docker=Image.Docker())))
123+
self.command = CommandInfo(value='python -m satyr.executor',
124+
shell=True)
125+
self.force_pull = force_pull
126+
self.docker = docker
127+
self.envs = envs
128+
self.uris = uris
129+
130+
@property
131+
def docker(self):
132+
return self.container.mesos.image.docker.name
133+
134+
@docker.setter
135+
def docker(self, value):
136+
self.container.mesos.image.docker.name = value
137+
138+
@property
139+
def force_pull(self):
140+
# cached is the opposite of force pull image
141+
return not self.container.mesos.image.cached
142+
143+
@force_pull.setter
144+
def force_pull(self, value):
145+
self.container.mesos.image.cached = not value
146+
147+
@property
148+
def uris(self):
149+
return [uri.value for uri in self.command.uris]
150+
151+
@uris.setter
152+
def uris(self, value):
153+
self.command.uris = [{'value': v} for v in value]
154+
155+
@property
156+
def envs(self):
157+
envs = self.command.environment.variables
158+
return {env.name: env.value for env in envs}
159+
160+
@envs.setter
161+
def envs(self, value):
162+
envs = [{'name': k, 'value': v} for k, v in value.items()]
163+
self.command.environment = Environment(variables=envs)
File renamed without changes.

satyr/scheduler.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
import os
55
import time
66
from collections import Counter
7+
from functools import partial
78

89
from mesos.native import MesosSchedulerDriver
910

10-
from .binpack import bfd
11+
from .constraint import pour
1112
from .interface import Scheduler
13+
from .placement import bfd
1214
from .proxies import SchedulerDriverProxy, SchedulerProxy
1315
from .proxies.messages import FrameworkInfo, TaskInfo, encode
1416
from .utils import Interruptable, timeout
@@ -25,18 +27,26 @@ def __init__(self, scheduler, name, user='', master=os.getenv('MESOS_MASTER'),
2527
super(SchedulerDriver, self).__init__(driver)
2628

2729

28-
# TODO create a scheduler which is reusing the same type of executors
29-
# todo configurable to reuse executors
30-
class QueueScheduler(Scheduler):
30+
# TODO reuse the same type of executors
31+
class Framework(Scheduler):
3132

32-
def __init__(self, *args, **kwargs):
33-
self.tasks = {} # holding task_id => task pairs
33+
def __init__(self, constraint=pour, placement=partial(bfd, cpus=1, mem=1),
34+
*args, **kwargs):
3435
self.healthy = True
36+
self.tasks = {} # holds task_id => task pairs
37+
self.placement = placement
38+
self.constraint = constraint
3539

3640
@property
3741
def statuses(self):
3842
return {task_id: task.status for task_id, task in self.tasks.items()}
3943

44+
# @property
45+
# def executors(self):
46+
# tpls = (((task.slave_id, task.executor.id), task)
47+
# for task_id, task in self.tasks.items())
48+
# return {k: list(v) for k, v in groupby(tpls)}
49+
4050
def is_idle(self):
4151
return not len(self.tasks)
4252

@@ -63,19 +73,28 @@ def on_offers(self, driver, offers):
6373
logging.info('Received offers: {}'.format(sum(offers)))
6474
self.report()
6575

66-
# maybe limit to the first n tasks
76+
# query tasks ready for scheduling
6777
staging = [self.tasks[status.task_id]
6878
for status in self.statuses.values() if status.is_staging()]
79+
80+
# filter acceptable offers
81+
accepts, declines = self.constraint(offers)
82+
6983
# best-fit-decreasing binpacking
70-
bins, skip = bfd(staging, offers, cpus=1, mem=1)
84+
bins, skip = self.placement(staging, accepts)
7185

86+
# reject offers not met constraints
87+
for offer in declines:
88+
driver.decline(offer)
89+
90+
# launch tasks
7291
for offer, tasks in bins:
7392
try:
7493
for task in tasks:
7594
task.slave_id = offer.slave_id
7695
task.status.state = 'TASK_STARTING'
7796
# running with empty task list will decline the offer
78-
logging.info('lanunches {}'.format(tasks))
97+
logging.info('launches {}'.format(tasks))
7998
driver.launch(offer.id, tasks)
8099
except Exception:
81100
logging.exception('Exception occured during task launch!')
@@ -97,6 +116,10 @@ def on_update(self, driver, status):
97116
self.report()
98117

99118

119+
# backward compatibility
120+
QueueScheduler = Framework
121+
122+
100123
if __name__ == '__main__':
101124
scheduler = QueueScheduler()
102125
with SchedulerDriver(scheduler, name='test') as fw:

satyr/tests/test_binpack.py renamed to satyr/tests/test_placement.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import absolute_import, division, print_function
22

33
import pytest
4-
from satyr.binpack import bf, bfd, ff, ffd, mr, weight
4+
from satyr.placement import bf, bfd, ff, ffd, mr, weight
55
from satyr.proxies.messages import Cpus, Mem, Offer, TaskInfo
66

77

satyr/utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44
from contextlib import contextmanager
55

66

7+
def partition(pred, iterable):
8+
trues, falses = [], []
9+
for item in iterable:
10+
if pred(item):
11+
trues.append(item)
12+
else:
13+
falses.append(item)
14+
return trues, falses
15+
16+
717
class TimeoutError(Exception):
818
pass
919

0 commit comments

Comments
 (0)