jolt 0.9.355__py3-none-any.whl → 0.9.370__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jolt/__init__.py +47 -0
- jolt/cache.py +339 -159
- jolt/cli.py +29 -98
- jolt/config.py +14 -26
- jolt/graph.py +27 -15
- jolt/loader.py +141 -180
- jolt/manifest.py +0 -46
- jolt/options.py +35 -12
- jolt/plugins/conan.py +238 -0
- jolt/plugins/docker.py +1 -1
- jolt/plugins/environ.py +11 -0
- jolt/plugins/gdb.py +6 -5
- jolt/plugins/linux.py +943 -0
- jolt/plugins/ninja-compdb.py +7 -6
- jolt/plugins/podman.py +4 -4
- jolt/plugins/scheduler.py +18 -14
- jolt/plugins/selfdeploy/setup.py +1 -1
- jolt/plugins/selfdeploy.py +1 -22
- jolt/plugins/strings.py +16 -6
- jolt/scheduler.py +428 -138
- jolt/tasks.py +23 -0
- jolt/tools.py +15 -8
- jolt/version.py +1 -1
- {jolt-0.9.355.dist-info → jolt-0.9.370.dist-info}/METADATA +2 -2
- {jolt-0.9.355.dist-info → jolt-0.9.370.dist-info}/RECORD +28 -29
- jolt/plugins/debian.py +0 -338
- jolt/plugins/repo.py +0 -253
- {jolt-0.9.355.dist-info → jolt-0.9.370.dist-info}/WHEEL +0 -0
- {jolt-0.9.355.dist-info → jolt-0.9.370.dist-info}/entry_points.txt +0 -0
- {jolt-0.9.355.dist-info → jolt-0.9.370.dist-info}/top_level.txt +0 -0
jolt/scheduler.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from concurrent.futures import ThreadPoolExecutor, as_completed, Future
|
|
2
2
|
import copy
|
|
3
|
+
from functools import wraps
|
|
3
4
|
import os
|
|
4
5
|
import queue
|
|
5
6
|
from threading import Lock
|
|
@@ -13,8 +14,6 @@ from jolt import tools
|
|
|
13
14
|
from jolt.error import raise_task_error
|
|
14
15
|
from jolt.error import raise_task_error_if
|
|
15
16
|
from jolt.graph import PruneStrategy
|
|
16
|
-
from jolt.manifest import ManifestExtension
|
|
17
|
-
from jolt.manifest import ManifestExtensionRegistry
|
|
18
17
|
from jolt.options import JoltOptions
|
|
19
18
|
from jolt.timer import Timer
|
|
20
19
|
|
|
@@ -25,12 +24,13 @@ class JoltEnvironment(object):
|
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
class TaskQueue(object):
|
|
28
|
-
|
|
27
|
+
"""
|
|
28
|
+
A helper class for tracking tasks in progress and their completion.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self):
|
|
29
32
|
self.futures = {}
|
|
30
33
|
self.futures_lock = Lock()
|
|
31
|
-
self.strategy = strategy
|
|
32
|
-
self.cache = cache
|
|
33
|
-
self.session = session
|
|
34
34
|
self.duration_acc = utils.duration_diff(0)
|
|
35
35
|
self._aborted = False
|
|
36
36
|
self._timer = Timer(60, self._log_task_running_time)
|
|
@@ -41,23 +41,32 @@ class TaskQueue(object):
|
|
|
41
41
|
for future in self.futures:
|
|
42
42
|
self.futures[future].task.log_running_time()
|
|
43
43
|
|
|
44
|
-
def submit(self,
|
|
44
|
+
def submit(self, executor):
|
|
45
|
+
"""
|
|
46
|
+
Submit an exeuctor to the task queue for execution.
|
|
47
|
+
|
|
48
|
+
The method schedules the executor for execution and returns a Future object
|
|
49
|
+
that may be used to track completion of the task.
|
|
50
|
+
"""
|
|
51
|
+
|
|
45
52
|
if self._aborted:
|
|
46
53
|
return None
|
|
47
54
|
|
|
48
|
-
env = JoltEnvironment(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"no executor can execute the task; "
|
|
53
|
-
"requesting a distributed network build without proper configuration?")
|
|
54
|
-
|
|
55
|
-
task.set_in_progress()
|
|
56
|
-
future = executor.submit(env)
|
|
57
|
-
self.futures[future] = executor
|
|
55
|
+
env = JoltEnvironment(queue=self)
|
|
56
|
+
future = executor.schedule(env)
|
|
57
|
+
with self.futures_lock:
|
|
58
|
+
self.futures[future] = executor
|
|
58
59
|
return future
|
|
59
60
|
|
|
60
61
|
def wait(self):
|
|
62
|
+
"""
|
|
63
|
+
Wait for any task to complete.
|
|
64
|
+
|
|
65
|
+
The method waits for the next task to complete and returns the task and any
|
|
66
|
+
exception that may have occurred during execution. If no task is in progress,
|
|
67
|
+
the method returns None, None.
|
|
68
|
+
"""
|
|
69
|
+
|
|
61
70
|
for future in as_completed(self.futures):
|
|
62
71
|
task = self.futures[future].task
|
|
63
72
|
try:
|
|
@@ -72,6 +81,13 @@ class TaskQueue(object):
|
|
|
72
81
|
return None, None
|
|
73
82
|
|
|
74
83
|
def abort(self):
|
|
84
|
+
"""
|
|
85
|
+
Abort all tasks in progress.
|
|
86
|
+
|
|
87
|
+
The method cancels all tasks in progress and prevents any new tasks from being
|
|
88
|
+
submitted to the task queue. The method doesn't wait for all tasks to complete
|
|
89
|
+
before returning.
|
|
90
|
+
"""
|
|
75
91
|
self._aborted = True
|
|
76
92
|
with self.futures_lock:
|
|
77
93
|
for future, executor in self.futures.items():
|
|
@@ -79,49 +95,131 @@ class TaskQueue(object):
|
|
|
79
95
|
future.cancel()
|
|
80
96
|
if len(self.futures):
|
|
81
97
|
log.info("Waiting for tasks to finish, please be patient")
|
|
82
|
-
self.strategy.executors.shutdown()
|
|
83
98
|
self._timer.cancel()
|
|
84
99
|
|
|
85
100
|
def shutdown(self):
|
|
101
|
+
"""
|
|
102
|
+
Shutdown the task queue.
|
|
103
|
+
"""
|
|
86
104
|
self._timer.cancel()
|
|
87
105
|
|
|
88
106
|
def is_aborted(self):
|
|
107
|
+
""" Returns true if the task queue has been aborted. """
|
|
89
108
|
return self._aborted
|
|
90
109
|
|
|
91
110
|
def in_progress(self, task):
|
|
111
|
+
""" Returns true if the task is in progress. """
|
|
92
112
|
with self.futures_lock:
|
|
93
113
|
return task in self.futures.values()
|
|
94
114
|
|
|
95
115
|
def empty(self):
|
|
116
|
+
""" Returns true if the task queue is empty. """
|
|
96
117
|
with self.futures_lock:
|
|
97
118
|
return len(self.futures) == 0
|
|
98
119
|
|
|
99
120
|
|
|
100
121
|
class Executor(object):
|
|
122
|
+
"""
|
|
123
|
+
Base class for all executors.
|
|
124
|
+
|
|
125
|
+
An executor is responsible for running a task. It is created by an executor
|
|
126
|
+
factory and is submitted to a task queue. The factory is also
|
|
127
|
+
responsible for hosting a thread pool that will run the executors it creates.
|
|
128
|
+
|
|
129
|
+
The type of executor created by the factory depends on the execution strategy
|
|
130
|
+
selected by the user through command line options. The strategy is responsible
|
|
131
|
+
for deciding which executor to create for each task.
|
|
132
|
+
|
|
133
|
+
An implementation of an executor must implement the run method, which is called
|
|
134
|
+
from the thread pool. The run method is responsible for running the task and
|
|
135
|
+
handling any exceptions that may occur during execution.
|
|
136
|
+
"""
|
|
137
|
+
|
|
101
138
|
def __init__(self, factory):
|
|
102
139
|
self.factory = factory
|
|
103
|
-
self._status = None
|
|
104
140
|
|
|
105
|
-
def
|
|
141
|
+
def schedule(self, env):
|
|
142
|
+
""" Schedule the task for execution.
|
|
143
|
+
|
|
144
|
+
This method is called by the task queue to schedule the task for execution
|
|
145
|
+
in the factory thread pool. The method must return a Future object that
|
|
146
|
+
represents the task execution. The Future object is used to track the
|
|
147
|
+
execution of the task and to retrieve the result of the execution
|
|
148
|
+
once it is completed.
|
|
149
|
+
|
|
150
|
+
The method must be implemented by all executors. They must call the
|
|
151
|
+
factory submit method to schedule the task for execution and also
|
|
152
|
+
mark the task as in progress with set_in_progress().
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
env: The JoltEnvironment object that contains the queue and cache objects.
|
|
156
|
+
|
|
157
|
+
"""
|
|
106
158
|
return self.factory.submit(self, env)
|
|
107
159
|
|
|
108
160
|
def cancel(self):
|
|
161
|
+
"""
|
|
162
|
+
Cancel the task.
|
|
163
|
+
|
|
164
|
+
This method is optional and may be implemented by executors that support
|
|
165
|
+
cancellation of tasks, such as network executors where a remote scheduler
|
|
166
|
+
may be able to cancel a task that is already running.
|
|
167
|
+
|
|
168
|
+
By default, the method does nothing.
|
|
169
|
+
"""
|
|
109
170
|
pass
|
|
110
171
|
|
|
111
172
|
def is_aborted(self):
|
|
173
|
+
""" Check if executor has been aborted. """
|
|
112
174
|
return self.factory.is_aborted()
|
|
113
175
|
|
|
114
176
|
def run(self, env):
|
|
115
|
-
|
|
177
|
+
"""
|
|
178
|
+
Run the task.
|
|
179
|
+
|
|
180
|
+
This method must be implemented by all executors. It is called from the
|
|
181
|
+
factory thread pool and is responsible for running the task
|
|
182
|
+
and handling any exceptions that may occur during execution.
|
|
183
|
+
Any exceptions raised by the task must, if caught, be re-raised to
|
|
184
|
+
the caller unless the task is marked as unstable, in which case the
|
|
185
|
+
exception should be logged and ignored.
|
|
186
|
+
|
|
187
|
+
The task run() method shall be run within a hooks.task_run()
|
|
188
|
+
context manager to ensure that the task status is recognized by
|
|
189
|
+
the report hooks and other plugins.
|
|
190
|
+
|
|
191
|
+
Network executors have additional requirements. See the
|
|
192
|
+
NetworkExecutor class for more information.
|
|
193
|
+
"""
|
|
194
|
+
raise NotImplementedError
|
|
116
195
|
|
|
117
196
|
|
|
118
197
|
class LocalExecutor(Executor):
|
|
198
|
+
"""
|
|
199
|
+
An Executor that runs a task locally.
|
|
200
|
+
|
|
201
|
+
The executor runs the task on the local machine. The task is run
|
|
202
|
+
by calling the task.run() method.
|
|
203
|
+
|
|
204
|
+
The executor is created by the local executor factory and is
|
|
205
|
+
typically run sequentially with other executors.
|
|
206
|
+
"""
|
|
207
|
+
|
|
119
208
|
def __init__(self, factory, task, force_upload=False, force_build=False):
|
|
120
209
|
super().__init__(factory)
|
|
121
210
|
self.task = task
|
|
122
211
|
self.force_build = force_build
|
|
123
212
|
self.force_upload = force_upload
|
|
124
213
|
|
|
214
|
+
def schedule(self, env):
|
|
215
|
+
"""
|
|
216
|
+
Schedule the task for execution.
|
|
217
|
+
|
|
218
|
+
The task is marked as in progress before scheduling.
|
|
219
|
+
"""
|
|
220
|
+
self.task.set_in_progress()
|
|
221
|
+
return super().schedule(env)
|
|
222
|
+
|
|
125
223
|
def _run(self, env, task):
|
|
126
224
|
if self.is_aborted():
|
|
127
225
|
return
|
|
@@ -138,8 +236,6 @@ class LocalExecutor(Executor):
|
|
|
138
236
|
self.task.raise_for_status(log_error=getattr(env, "worker", False))
|
|
139
237
|
raise e
|
|
140
238
|
|
|
141
|
-
return task
|
|
142
|
-
|
|
143
239
|
def get_all_extensions(self, task):
|
|
144
240
|
extensions = copy.copy(task.extensions)
|
|
145
241
|
for ext in extensions:
|
|
@@ -155,15 +251,61 @@ class LocalExecutor(Executor):
|
|
|
155
251
|
|
|
156
252
|
|
|
157
253
|
class NetworkExecutor(Executor):
|
|
158
|
-
|
|
254
|
+
def run(self, env):
|
|
255
|
+
"""
|
|
256
|
+
Run the task.
|
|
257
|
+
|
|
258
|
+
See the Executor class for basic information.
|
|
259
|
+
|
|
260
|
+
Network executors have additional requirements. Before scheduling
|
|
261
|
+
the task to a remote scheduler, the executor must call
|
|
262
|
+
run_resources() on the task. This acquires any Resources marked
|
|
263
|
+
local=True and uploads the resulting session artifacts
|
|
264
|
+
to the remote cache.
|
|
265
|
+
|
|
266
|
+
Once the task has been submitted to the remote scheduler, the executor
|
|
267
|
+
must run task.queued() on the task and its extensions. This is done
|
|
268
|
+
to ensure that the task status is correctly reported to the
|
|
269
|
+
user.
|
|
270
|
+
|
|
271
|
+
For any change in state of task, the executor must run one of:
|
|
272
|
+
|
|
273
|
+
- task.running_execution(remote=True) - when the task has started
|
|
274
|
+
- task.failed_execution(remote=True) - when the task has failed
|
|
275
|
+
- task.failed_execution(remote=True, interrupt=True) - when the
|
|
276
|
+
task has been interrupted, e.g. by a user request or rescheduling
|
|
277
|
+
- task.finished_execution(remote=True) - when the task has passed
|
|
278
|
+
|
|
279
|
+
Upon completion of the task, whether successful or not, task
|
|
280
|
+
session artifacts must be downloaded to the local cache, if
|
|
281
|
+
the task is marked as downloadable. This is done by calling
|
|
282
|
+
task.download() with the session_only flag set to True.
|
|
283
|
+
|
|
284
|
+
Persistent artifacts are downloaded only if the task is successful
|
|
285
|
+
and the task is marked as downloadable.
|
|
286
|
+
"""
|
|
287
|
+
raise NotImplementedError
|
|
159
288
|
|
|
160
289
|
|
|
161
290
|
class SkipTask(Executor):
|
|
291
|
+
"""
|
|
292
|
+
An Executor that skips a task.
|
|
293
|
+
|
|
294
|
+
This executor is created by the concurrent executor factory when a task
|
|
295
|
+
is skipped, i.e. when the task artifacts are already available locally or
|
|
296
|
+
remotely and the task does not need to be run.
|
|
297
|
+
"""
|
|
298
|
+
|
|
162
299
|
def __init__(self, factory, task, *args, **kwargs):
|
|
163
300
|
super().__init__(factory, *args, **kwargs)
|
|
164
301
|
self.task = task
|
|
165
302
|
|
|
166
303
|
def run(self, env):
|
|
304
|
+
"""
|
|
305
|
+
Skip the task.
|
|
306
|
+
|
|
307
|
+
The task and its extensions are marked as skipped.
|
|
308
|
+
"""
|
|
167
309
|
self.task.skipped()
|
|
168
310
|
for ext in self.task.extensions:
|
|
169
311
|
ext.skipped()
|
|
@@ -171,11 +313,30 @@ class SkipTask(Executor):
|
|
|
171
313
|
|
|
172
314
|
|
|
173
315
|
class Downloader(Executor):
|
|
316
|
+
"""
|
|
317
|
+
An Executor that downloads task artifacts.
|
|
318
|
+
|
|
319
|
+
The executor downloads the task artifacts and its extensions from the
|
|
320
|
+
remote cache to the local cache. Failure to download the artifacts
|
|
321
|
+
is reported by raising an exception.
|
|
322
|
+
|
|
323
|
+
Downloader executors are typically run in parallel with other executors.
|
|
324
|
+
|
|
325
|
+
"""
|
|
174
326
|
def __init__(self, factory, task, *args, **kwargs):
|
|
175
327
|
super().__init__(factory, *args, **kwargs)
|
|
176
328
|
self.task = task
|
|
177
329
|
|
|
178
|
-
def
|
|
330
|
+
def schedule(self, env):
|
|
331
|
+
"""
|
|
332
|
+
Schedule the task for execution.
|
|
333
|
+
|
|
334
|
+
The task is marked as in progress before scheduling.
|
|
335
|
+
"""
|
|
336
|
+
self.task.set_in_progress()
|
|
337
|
+
return super().schedule(env)
|
|
338
|
+
|
|
339
|
+
def _download(self, task):
|
|
179
340
|
if self.is_aborted():
|
|
180
341
|
return
|
|
181
342
|
if not task.is_downloadable():
|
|
@@ -194,18 +355,39 @@ class Downloader(Executor):
|
|
|
194
355
|
task.finished_download()
|
|
195
356
|
|
|
196
357
|
def run(self, env):
|
|
197
|
-
|
|
358
|
+
""" Downloads artifacts. """
|
|
359
|
+
|
|
360
|
+
self._download(self.task)
|
|
198
361
|
for ext in self.task.extensions:
|
|
199
|
-
self._download(
|
|
362
|
+
self._download(ext)
|
|
200
363
|
return self.task
|
|
201
364
|
|
|
202
365
|
|
|
203
366
|
class Uploader(Executor):
|
|
367
|
+
"""
|
|
368
|
+
An Executor that uploads task artifacts.
|
|
369
|
+
|
|
370
|
+
The executor uploads the task artifacts and its extensions from the
|
|
371
|
+
local cache to the remote cache. Failure to upload the artifacts
|
|
372
|
+
is reported by raising an exception.
|
|
373
|
+
|
|
374
|
+
Uploader executors are typically run in parallel with other executors.
|
|
375
|
+
"""
|
|
376
|
+
|
|
204
377
|
def __init__(self, factory, task, *args, **kwargs):
|
|
205
378
|
super().__init__(factory, *args, **kwargs)
|
|
206
379
|
self.task = task
|
|
207
380
|
|
|
208
|
-
def
|
|
381
|
+
def schedule(self, env):
|
|
382
|
+
"""
|
|
383
|
+
Schedule the task for execution.
|
|
384
|
+
|
|
385
|
+
The task is marked as in progress before scheduling.
|
|
386
|
+
"""
|
|
387
|
+
self.task.set_in_progress()
|
|
388
|
+
return super().schedule(env)
|
|
389
|
+
|
|
390
|
+
def _upload(self, task):
|
|
209
391
|
if self.is_aborted():
|
|
210
392
|
return
|
|
211
393
|
try:
|
|
@@ -222,50 +404,79 @@ class Uploader(Executor):
|
|
|
222
404
|
task.finished_upload()
|
|
223
405
|
|
|
224
406
|
def run(self, env):
|
|
225
|
-
|
|
407
|
+
""" Uploads artifacts. """
|
|
408
|
+
|
|
409
|
+
self._upload(self.task)
|
|
226
410
|
for ext in self.task.extensions:
|
|
227
|
-
self._upload(
|
|
411
|
+
self._upload(ext)
|
|
228
412
|
|
|
229
413
|
return self.task
|
|
230
414
|
|
|
231
415
|
|
|
232
416
|
@utils.Singleton
|
|
233
417
|
class ExecutorRegistry(object):
|
|
418
|
+
"""
|
|
419
|
+
The ExecutorRegistry is responsible for creating executors.
|
|
420
|
+
|
|
421
|
+
The types of executors that are possible to create are:
|
|
422
|
+
|
|
423
|
+
- create_local: Runs tasks locally.
|
|
424
|
+
- create_network: Schedules tasks for remote execution.
|
|
425
|
+
- create_downloader: Downloads task artifacts.
|
|
426
|
+
- create_uploader: Uploads task artifacts.
|
|
427
|
+
- create_skipper: Skips tasks.
|
|
428
|
+
|
|
429
|
+
The registry utilizes different ExecutorFactory objects to create executors. Plugins
|
|
430
|
+
can register their own NetworkExecutorFactory objects with the help of the
|
|
431
|
+
ExecutorFactory.Register decorator.
|
|
432
|
+
"""
|
|
433
|
+
|
|
234
434
|
executor_factories = []
|
|
235
|
-
extension_factories = []
|
|
236
435
|
|
|
237
436
|
def __init__(self, options=None):
|
|
238
437
|
self._options = options or JoltOptions()
|
|
239
438
|
self._factories = [factory(self._options) for factory in self.__class__.executor_factories]
|
|
240
439
|
self._local_factory = LocalExecutorFactory(self._options)
|
|
241
440
|
self._concurrent_factory = ConcurrentLocalExecutorFactory(self._options)
|
|
242
|
-
self._extensions = [factory().create() for factory in self.__class__.extension_factories]
|
|
243
441
|
|
|
244
442
|
def shutdown(self):
|
|
443
|
+
""" Shuts all executor factories and thread-pools down """
|
|
444
|
+
|
|
245
445
|
for factory in self._factories:
|
|
246
446
|
factory.shutdown()
|
|
247
447
|
self._local_factory.shutdown()
|
|
248
448
|
self._concurrent_factory.shutdown()
|
|
249
449
|
|
|
250
450
|
def create_session(self, graph):
|
|
451
|
+
""" Creates a session for all factories. """
|
|
251
452
|
return {factory: factory.create_session(graph) for factory in self._factories}
|
|
252
453
|
|
|
253
454
|
def create_skipper(self, task):
|
|
455
|
+
""" Creates an executor that skips a task. """
|
|
254
456
|
return SkipTask(self._concurrent_factory, task)
|
|
255
457
|
|
|
256
458
|
def create_downloader(self, task):
|
|
257
|
-
|
|
459
|
+
""" Creates an executor that downloads task artifacts. """
|
|
258
460
|
return Downloader(self._concurrent_factory, task)
|
|
259
461
|
|
|
260
462
|
def create_uploader(self, task):
|
|
261
|
-
|
|
463
|
+
""" Creates an executor that uploads task artifacts. """
|
|
262
464
|
return Uploader(self._concurrent_factory, task)
|
|
263
465
|
|
|
264
466
|
def create_local(self, task, force=False):
|
|
467
|
+
""" Creates an executor that runs a task locally. """
|
|
265
468
|
task.set_locally_executed()
|
|
266
469
|
return self._local_factory.create(task, force=force)
|
|
267
470
|
|
|
268
471
|
def create_network(self, session, task):
|
|
472
|
+
"""
|
|
473
|
+
Creates an executor that schedules a task for remote execution.
|
|
474
|
+
|
|
475
|
+
All registred network executor factories are queried to create an executor.
|
|
476
|
+
The first factory that can create an executor is used. If no factory is able
|
|
477
|
+
to create an executor, a local executor is created as fallback.
|
|
478
|
+
"""
|
|
479
|
+
|
|
269
480
|
for factory in self._factories:
|
|
270
481
|
executor = factory.create(session[factory], task)
|
|
271
482
|
if executor is not None:
|
|
@@ -273,54 +484,53 @@ class ExecutorRegistry(object):
|
|
|
273
484
|
return executor
|
|
274
485
|
return self.create_local(task)
|
|
275
486
|
|
|
276
|
-
def get_network_parameters(self, task):
|
|
277
|
-
parameters = {}
|
|
278
|
-
for extension in self._extensions:
|
|
279
|
-
parameters.update(extension.get_parameters(task))
|
|
280
|
-
return parameters
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
class NetworkExecutorExtensionFactory(object):
|
|
284
|
-
@staticmethod
|
|
285
|
-
def Register(cls):
|
|
286
|
-
ExecutorRegistry.extension_factories.insert(0, cls)
|
|
287
|
-
return cls
|
|
288
487
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
class NetworkExecutorExtension(object):
|
|
294
|
-
def get_parameters(self, task):
|
|
295
|
-
return {}
|
|
488
|
+
class ExecutorFactory(object):
|
|
489
|
+
"""
|
|
490
|
+
The ExecutorFactory class is responsible for creating executors.
|
|
296
491
|
|
|
492
|
+
The factory is responsible for creating executors that run tasks. The factory
|
|
493
|
+
is also responsible for hosting a thread pool that will run the executors it creates.
|
|
297
494
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
self.executor = executor
|
|
303
|
-
self.env = env
|
|
495
|
+
"""
|
|
496
|
+
class QueueItem(object):
|
|
497
|
+
"""
|
|
498
|
+
The type of item that is put into the queue thread-pool queue.
|
|
304
499
|
|
|
305
|
-
|
|
306
|
-
|
|
500
|
+
It wraps the executor and its priority.
|
|
501
|
+
"""
|
|
502
|
+
def __init__(self, priority: int, future: Future, executor: Executor, env: JoltEnvironment):
|
|
503
|
+
self.priority = priority
|
|
504
|
+
self.future = future
|
|
505
|
+
self.executor = executor
|
|
506
|
+
self.env = env
|
|
307
507
|
|
|
308
|
-
|
|
309
|
-
|
|
508
|
+
def __le__(self, o):
|
|
509
|
+
return self.priority <= o.priority
|
|
310
510
|
|
|
311
|
-
|
|
312
|
-
|
|
511
|
+
def __ge__(self, o):
|
|
512
|
+
return self.priority >= o.priority
|
|
313
513
|
|
|
314
|
-
|
|
315
|
-
|
|
514
|
+
def __lt__(self, o):
|
|
515
|
+
return self.priority < o.priority
|
|
316
516
|
|
|
317
|
-
|
|
318
|
-
|
|
517
|
+
def __gt__(self, o):
|
|
518
|
+
return self.priority > o.priority
|
|
319
519
|
|
|
520
|
+
def __eq__(self, o):
|
|
521
|
+
return self.priority == o.priority
|
|
320
522
|
|
|
321
|
-
class ExecutorFactory(object):
|
|
322
523
|
@staticmethod
|
|
323
524
|
def Register(cls):
|
|
525
|
+
"""
|
|
526
|
+
Decorator to register an executor factory.
|
|
527
|
+
|
|
528
|
+
The decorator is used by plugins that whish to register their own
|
|
529
|
+
executor factories. Such factories are used by the ExecutorRegistry
|
|
530
|
+
to create executors for tasks, as determined by the execution strategy
|
|
531
|
+
selected by the user.
|
|
532
|
+
"""
|
|
533
|
+
|
|
324
534
|
ExecutorRegistry.executor_factories.insert(0, cls)
|
|
325
535
|
return cls
|
|
326
536
|
|
|
@@ -331,42 +541,76 @@ class ExecutorFactory(object):
|
|
|
331
541
|
self._options = options or JoltOptions()
|
|
332
542
|
|
|
333
543
|
def is_aborted(self):
|
|
544
|
+
""" Returns true if the build and thus the factory has been aborted. """
|
|
334
545
|
return self._aborted
|
|
335
546
|
|
|
336
547
|
def is_keep_going(self):
|
|
548
|
+
""" Returns true if the build should continue even if a task fails. """
|
|
337
549
|
return self._options.keep_going
|
|
338
550
|
|
|
339
551
|
def shutdown(self):
|
|
552
|
+
"""
|
|
553
|
+
Called to shutdown the factory and its thread-pool.
|
|
554
|
+
|
|
555
|
+
The method is called when the build is complete or when the build is aborted.
|
|
556
|
+
After the method is called, no more tasks can be submitted to the factory and
|
|
557
|
+
the is_aborted() method will return True.
|
|
558
|
+
"""
|
|
340
559
|
self._aborted = True
|
|
341
560
|
self.pool.shutdown()
|
|
342
561
|
|
|
343
562
|
def create(self, task):
|
|
563
|
+
"""
|
|
564
|
+
Create an executor for the provided task.
|
|
565
|
+
|
|
566
|
+
Must be implemented by all executor factories. The method must return
|
|
567
|
+
an executor that is capable of running the task. The executor must be
|
|
568
|
+
created with the factory as its parent so that it can be submitted to
|
|
569
|
+
the correct thread-pool for execution.
|
|
570
|
+
"""
|
|
344
571
|
raise NotImplementedError()
|
|
345
572
|
|
|
346
573
|
def _run(self):
|
|
347
|
-
|
|
574
|
+
item = self._queue.get(False)
|
|
348
575
|
self._queue.task_done()
|
|
349
576
|
try:
|
|
350
577
|
if not self.is_aborted():
|
|
351
|
-
|
|
578
|
+
item.executor.run(item.env)
|
|
352
579
|
except KeyboardInterrupt as e:
|
|
353
580
|
self._aborted = True
|
|
354
|
-
|
|
581
|
+
item.future.set_exception(e)
|
|
355
582
|
except Exception as e:
|
|
356
583
|
if not self.is_keep_going():
|
|
357
584
|
self._aborted = True
|
|
358
|
-
|
|
585
|
+
item.future.set_exception(e)
|
|
359
586
|
else:
|
|
360
|
-
|
|
587
|
+
item.future.set_result(item.executor)
|
|
361
588
|
|
|
362
589
|
def submit(self, executor, env):
|
|
590
|
+
"""
|
|
591
|
+
Submit an executor to the thread-pool for execution.
|
|
592
|
+
|
|
593
|
+
The method submits the executor to the thread-pool for execution. The executor
|
|
594
|
+
is wrapped in a Future object that is returned to the caller. The Future object
|
|
595
|
+
is used to track the execution of the task and to retrieve the result of the
|
|
596
|
+
execution once it is completed.
|
|
597
|
+
"""
|
|
363
598
|
future = Future()
|
|
364
|
-
self._queue.put(
|
|
599
|
+
self._queue.put(ExecutorFactory.QueueItem(-executor.task.weight, future, executor, env))
|
|
365
600
|
self.pool.submit(self._run)
|
|
366
601
|
return future
|
|
367
602
|
|
|
368
603
|
|
|
369
604
|
class LocalExecutorFactory(ExecutorFactory):
|
|
605
|
+
"""
|
|
606
|
+
Factory for creating local executors.
|
|
607
|
+
|
|
608
|
+
The factory creates executors that run tasks locally. Typically,
|
|
609
|
+
only one LocalExecutor is allowed to run at a time, unless the
|
|
610
|
+
user has specified a higher number of parallel tasks in the
|
|
611
|
+
configuration file or through command line options (-j).
|
|
612
|
+
"""
|
|
613
|
+
|
|
370
614
|
def __init__(self, options=None):
|
|
371
615
|
max_workers = config.getint(
|
|
372
616
|
"jolt", "parallel_tasks",
|
|
@@ -376,10 +620,19 @@ class LocalExecutorFactory(ExecutorFactory):
|
|
|
376
620
|
max_workers=max_workers)
|
|
377
621
|
|
|
378
622
|
def create(self, task, force=False):
|
|
623
|
+
""" Create a LocalExecutor for the task. """
|
|
379
624
|
return LocalExecutor(self, task, force_build=force)
|
|
380
625
|
|
|
381
626
|
|
|
382
627
|
class ConcurrentLocalExecutorFactory(ExecutorFactory):
|
|
628
|
+
"""
|
|
629
|
+
A shared factory for local executors that are allowed to run concurrently.
|
|
630
|
+
|
|
631
|
+
The factory cannot create any executors on its own. Instead, its executors
|
|
632
|
+
are created by the ExecutorRegistry. The factory thread-pool is then used to
|
|
633
|
+
run executors concurrently.
|
|
634
|
+
"""
|
|
635
|
+
|
|
383
636
|
def __init__(self, options=None):
|
|
384
637
|
max_workers = tools.Tools().thread_count()
|
|
385
638
|
super().__init__(
|
|
@@ -391,6 +644,10 @@ class ConcurrentLocalExecutorFactory(ExecutorFactory):
|
|
|
391
644
|
|
|
392
645
|
|
|
393
646
|
class NetworkExecutorFactory(ExecutorFactory):
|
|
647
|
+
"""
|
|
648
|
+
Base class for executors that schedule task executions remotely in a build cluster.
|
|
649
|
+
"""
|
|
650
|
+
|
|
394
651
|
def __init__(self, *args, **kwargs):
|
|
395
652
|
super().__init__(*args, **kwargs)
|
|
396
653
|
|
|
@@ -398,17 +655,72 @@ class NetworkExecutorFactory(ExecutorFactory):
|
|
|
398
655
|
raise NotImplementedError()
|
|
399
656
|
|
|
400
657
|
|
|
658
|
+
def ensure_executor_return(func):
|
|
659
|
+
""" Decorator to ensure that an executor is returned by factories. """
|
|
660
|
+
|
|
661
|
+
@wraps(func)
|
|
662
|
+
def wrapper(self, session, task):
|
|
663
|
+
executor = func(self, session, task)
|
|
664
|
+
raise_task_error_if(
|
|
665
|
+
not executor, task,
|
|
666
|
+
"no executor can execute the task; "
|
|
667
|
+
"requesting a distributed network build without proper configuration?")
|
|
668
|
+
return executor
|
|
669
|
+
|
|
670
|
+
return wrapper
|
|
671
|
+
|
|
672
|
+
|
|
401
673
|
class ExecutionStrategy(object):
|
|
674
|
+
"""
|
|
675
|
+
Base class for all execution strategies.
|
|
676
|
+
|
|
677
|
+
An execution strategy is responsible for deciding which executor to create for each task.
|
|
678
|
+
The decision is based on the type of task and the availability of the task's artifacts in
|
|
679
|
+
local and remote caches.
|
|
680
|
+
|
|
681
|
+
The strategy is also responsible for deciding if task requirements should be pruned
|
|
682
|
+
from the build graph. This is done to avoid processing tasks that are not needed for the build.
|
|
683
|
+
|
|
684
|
+
Strategies are selected by the user through command line options.
|
|
685
|
+
|
|
686
|
+
"""
|
|
402
687
|
def create_executor(self, session, task):
|
|
688
|
+
"""
|
|
689
|
+
Create an executor for the task.
|
|
690
|
+
|
|
691
|
+
The method must be implemented by all execution strategies. It is responsible for
|
|
692
|
+
creating an executor that is capable of running or processing the task. Creation
|
|
693
|
+
of an executor should be delegated to the ExecutorRegistry which has the knowledge
|
|
694
|
+
of all available executor factories.
|
|
695
|
+
"""
|
|
696
|
+
raise NotImplementedError()
|
|
697
|
+
|
|
698
|
+
def should_prune_requirements(self, task):
|
|
699
|
+
"""
|
|
700
|
+
Return True if the task requirements should be pruned from the build graph.
|
|
701
|
+
|
|
702
|
+
The method must be implemented by all execution strategies.
|
|
703
|
+
"""
|
|
403
704
|
raise NotImplementedError()
|
|
404
705
|
|
|
405
706
|
|
|
406
707
|
class LocalStrategy(ExecutionStrategy, PruneStrategy):
|
|
708
|
+
"""
|
|
709
|
+
Strategy for local builds.
|
|
710
|
+
|
|
711
|
+
By default, the strategy schedules tasks for local execution, unless the task
|
|
712
|
+
artifacts are available in the local cache. If available remotely, the strategy
|
|
713
|
+
will create a downloader executor to download the artifacts.
|
|
714
|
+
"""
|
|
715
|
+
|
|
407
716
|
def __init__(self, executors, cache):
|
|
408
717
|
self.executors = executors
|
|
409
718
|
self.cache = cache
|
|
410
719
|
|
|
720
|
+
@ensure_executor_return
|
|
411
721
|
def create_executor(self, session, task):
|
|
722
|
+
""" Create an executor for the task. """
|
|
723
|
+
|
|
412
724
|
if task.is_alias() or task.is_resource():
|
|
413
725
|
return self.executors.create_skipper(task)
|
|
414
726
|
if not task.is_cacheable():
|
|
@@ -420,6 +732,8 @@ class LocalStrategy(ExecutionStrategy, PruneStrategy):
|
|
|
420
732
|
return self.executors.create_local(task)
|
|
421
733
|
|
|
422
734
|
def should_prune_requirements(self, task):
|
|
735
|
+
""" Prune task requirements if possible """
|
|
736
|
+
|
|
423
737
|
if task.is_alias() or not task.is_cacheable():
|
|
424
738
|
return False
|
|
425
739
|
if task.is_available_locally():
|
|
@@ -430,10 +744,21 @@ class LocalStrategy(ExecutionStrategy, PruneStrategy):
|
|
|
430
744
|
|
|
431
745
|
|
|
432
746
|
class DownloadStrategy(ExecutionStrategy, PruneStrategy):
|
|
747
|
+
"""
|
|
748
|
+
Strategy for downloading task artifacts.
|
|
749
|
+
|
|
750
|
+
The strategy is used when the user has requested that task artifacts be downloaded.
|
|
751
|
+
If the task artifacts are available in the local cache, the strategy will skip the
|
|
752
|
+
task. If the task artifacts are available in the remote cache, the strategy will
|
|
753
|
+
create a downloader executor to download the artifacts. If the task artifacts are
|
|
754
|
+
not available in either cache, the strategy reports an error.
|
|
755
|
+
"""
|
|
756
|
+
|
|
433
757
|
def __init__(self, executors, cache):
|
|
434
758
|
self.executors = executors
|
|
435
759
|
self.cache = cache
|
|
436
760
|
|
|
761
|
+
@ensure_executor_return
|
|
437
762
|
def create_executor(self, session, task):
|
|
438
763
|
if task.is_alias():
|
|
439
764
|
return self.executors.create_skipper(task)
|
|
@@ -452,11 +777,22 @@ class DownloadStrategy(ExecutionStrategy, PruneStrategy):
|
|
|
452
777
|
|
|
453
778
|
|
|
454
779
|
class DistributedStrategy(ExecutionStrategy, PruneStrategy):
|
|
780
|
+
"""
|
|
781
|
+
Strategy for distributed network builds.
|
|
782
|
+
|
|
783
|
+
By default, the strategy schedules tasks for remote execution, if there is no
|
|
784
|
+
artifact available. Otherwise, artifacts are either uploaded or downloaded as
|
|
785
|
+
needed.
|
|
786
|
+
"""
|
|
787
|
+
|
|
455
788
|
def __init__(self, executors, cache):
|
|
456
789
|
self.executors = executors
|
|
457
790
|
self.cache = cache
|
|
458
791
|
|
|
792
|
+
@ensure_executor_return
|
|
459
793
|
def create_executor(self, session, task):
|
|
794
|
+
""" Create an executor for the task. """
|
|
795
|
+
|
|
460
796
|
if task.is_alias() or task.is_resource():
|
|
461
797
|
return self.executors.create_skipper(task)
|
|
462
798
|
|
|
@@ -490,6 +826,8 @@ class DistributedStrategy(ExecutionStrategy, PruneStrategy):
|
|
|
490
826
|
return self.executors.create_network(session, task)
|
|
491
827
|
|
|
492
828
|
def should_prune_requirements(self, task):
|
|
829
|
+
""" Prune task requirements if possible """
|
|
830
|
+
|
|
493
831
|
if task.is_alias() or not task.is_cacheable():
|
|
494
832
|
return False
|
|
495
833
|
if task.is_available_remotely():
|
|
@@ -498,11 +836,23 @@ class DistributedStrategy(ExecutionStrategy, PruneStrategy):
|
|
|
498
836
|
|
|
499
837
|
|
|
500
838
|
class WorkerStrategy(ExecutionStrategy, PruneStrategy):
|
|
839
|
+
"""
|
|
840
|
+
Strategy for worker builds.
|
|
841
|
+
|
|
842
|
+
This strategy is used on workers when the user has requested a network build.
|
|
843
|
+
It is similar to the LocalStrategy in that it will run tasks locally if no
|
|
844
|
+
artifacts are available. However, if artifacts are available locally, the
|
|
845
|
+
strategy will upload them to the remote cache.
|
|
846
|
+
"""
|
|
847
|
+
|
|
501
848
|
def __init__(self, executors, cache):
|
|
502
849
|
self.executors = executors
|
|
503
850
|
self.cache = cache
|
|
504
851
|
|
|
852
|
+
@ensure_executor_return
|
|
505
853
|
def create_executor(self, session, task):
|
|
854
|
+
""" Create an executor for the task. """
|
|
855
|
+
|
|
506
856
|
if task.is_alias() or task.is_resource():
|
|
507
857
|
return self.executors.create_skipper(task)
|
|
508
858
|
|
|
@@ -535,6 +885,8 @@ class WorkerStrategy(ExecutionStrategy, PruneStrategy):
|
|
|
535
885
|
return self.executors.create_local(task)
|
|
536
886
|
|
|
537
887
|
def should_prune_requirements(self, task):
|
|
888
|
+
""" Prune task requirements if possible """
|
|
889
|
+
|
|
538
890
|
if task.is_alias() or not task.is_cacheable():
|
|
539
891
|
return False
|
|
540
892
|
if task.is_available_locally():
|
|
@@ -555,68 +907,6 @@ def get_exported_task_set(task):
|
|
|
555
907
|
return list(set(children))
|
|
556
908
|
|
|
557
909
|
|
|
558
|
-
class TaskIdentityExtension(ManifestExtension):
|
|
559
|
-
def export_manifest(self, manifest, tasks):
|
|
560
|
-
# Generate a list of all tasks that must be evaluated
|
|
561
|
-
# for inclusion in the manifest
|
|
562
|
-
all_tasks = []
|
|
563
|
-
for task in tasks:
|
|
564
|
-
all_tasks += get_exported_task_set(task)
|
|
565
|
-
all_tasks = list(set(all_tasks))
|
|
566
|
-
|
|
567
|
-
for child in all_tasks:
|
|
568
|
-
manifest_task = manifest.find_task(child.qualified_name)
|
|
569
|
-
if manifest_task is None:
|
|
570
|
-
manifest_task = manifest.create_task()
|
|
571
|
-
manifest_task.name = child.qualified_name
|
|
572
|
-
manifest_task.identity = child.identity
|
|
573
|
-
manifest_task.instance = child.instance
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
ManifestExtensionRegistry.add(TaskIdentityExtension())
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
class TaskExportExtension(ManifestExtension):
|
|
580
|
-
def export_manifest(self, manifest, tasks):
|
|
581
|
-
short_task_names = set()
|
|
582
|
-
|
|
583
|
-
# Generate a list of all tasks that must be evaluated
|
|
584
|
-
# for inclusion in the manifest
|
|
585
|
-
all_tasks = []
|
|
586
|
-
for task in tasks:
|
|
587
|
-
all_tasks += get_exported_task_set(task)
|
|
588
|
-
all_tasks = list(set(all_tasks))
|
|
589
|
-
|
|
590
|
-
# Add all tasks with export attributes to the manifest
|
|
591
|
-
for child in all_tasks:
|
|
592
|
-
manifest_task = manifest.find_task(child.qualified_name)
|
|
593
|
-
if manifest_task is None:
|
|
594
|
-
manifest_task = manifest.create_task()
|
|
595
|
-
manifest_task.name = child.qualified_name
|
|
596
|
-
for key, export in child.task._get_export_objects().items():
|
|
597
|
-
attrib = manifest_task.create_attribute()
|
|
598
|
-
attrib.name = key
|
|
599
|
-
attrib.value = export.export(child.task)
|
|
600
|
-
short_task_names.add(child.name)
|
|
601
|
-
|
|
602
|
-
# Figure out if any task with an overridden default parameter
|
|
603
|
-
# value was included in the manifest. If so, add info about it.
|
|
604
|
-
default_task_names = set()
|
|
605
|
-
for task in all_tasks:
|
|
606
|
-
for task in task.options.default:
|
|
607
|
-
short_name, _ = utils.parse_task_name(task)
|
|
608
|
-
if short_name in short_task_names:
|
|
609
|
-
default_task_names.add(task)
|
|
610
|
-
if default_task_names:
|
|
611
|
-
build = manifest.create_build()
|
|
612
|
-
for task in default_task_names:
|
|
613
|
-
default = build.create_default()
|
|
614
|
-
default.name = task
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
ManifestExtensionRegistry.add(TaskExportExtension())
|
|
618
|
-
|
|
619
|
-
|
|
620
910
|
def export_tasks(tasks):
|
|
621
911
|
pb_tasks = {}
|
|
622
912
|
|