toil 7.0.0__py3-none-any.whl → 8.1.0b1__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.
- toil/__init__.py +124 -86
- toil/batchSystems/__init__.py +1 -0
- toil/batchSystems/abstractBatchSystem.py +137 -77
- toil/batchSystems/abstractGridEngineBatchSystem.py +211 -101
- toil/batchSystems/awsBatch.py +237 -128
- toil/batchSystems/cleanup_support.py +22 -16
- toil/batchSystems/contained_executor.py +30 -26
- toil/batchSystems/gridengine.py +85 -49
- toil/batchSystems/htcondor.py +164 -87
- toil/batchSystems/kubernetes.py +622 -386
- toil/batchSystems/local_support.py +17 -12
- toil/batchSystems/lsf.py +132 -79
- toil/batchSystems/lsfHelper.py +13 -11
- toil/batchSystems/mesos/__init__.py +41 -29
- toil/batchSystems/mesos/batchSystem.py +288 -149
- toil/batchSystems/mesos/executor.py +77 -49
- toil/batchSystems/mesos/test/__init__.py +31 -23
- toil/batchSystems/options.py +39 -29
- toil/batchSystems/registry.py +53 -19
- toil/batchSystems/singleMachine.py +293 -123
- toil/batchSystems/slurm.py +651 -155
- toil/batchSystems/torque.py +46 -32
- toil/bus.py +141 -73
- toil/common.py +784 -397
- toil/cwl/__init__.py +1 -1
- toil/cwl/cwltoil.py +1137 -534
- toil/cwl/utils.py +17 -22
- toil/deferred.py +62 -41
- toil/exceptions.py +5 -3
- toil/fileStores/__init__.py +5 -5
- toil/fileStores/abstractFileStore.py +88 -57
- toil/fileStores/cachingFileStore.py +711 -247
- toil/fileStores/nonCachingFileStore.py +113 -75
- toil/job.py +1031 -349
- toil/jobStores/abstractJobStore.py +387 -243
- toil/jobStores/aws/jobStore.py +772 -412
- toil/jobStores/aws/utils.py +161 -109
- toil/jobStores/conftest.py +1 -0
- toil/jobStores/fileJobStore.py +289 -151
- toil/jobStores/googleJobStore.py +137 -70
- toil/jobStores/utils.py +36 -15
- toil/leader.py +614 -269
- toil/lib/accelerators.py +115 -18
- toil/lib/aws/__init__.py +55 -28
- toil/lib/aws/ami.py +122 -87
- toil/lib/aws/iam.py +284 -108
- toil/lib/aws/s3.py +31 -0
- toil/lib/aws/session.py +204 -58
- toil/lib/aws/utils.py +290 -213
- toil/lib/bioio.py +13 -5
- toil/lib/compatibility.py +11 -6
- toil/lib/conversions.py +83 -49
- toil/lib/docker.py +131 -103
- toil/lib/dockstore.py +379 -0
- toil/lib/ec2.py +322 -209
- toil/lib/ec2nodes.py +174 -105
- toil/lib/encryption/_dummy.py +5 -3
- toil/lib/encryption/_nacl.py +10 -6
- toil/lib/encryption/conftest.py +1 -0
- toil/lib/exceptions.py +26 -7
- toil/lib/expando.py +4 -2
- toil/lib/ftp_utils.py +217 -0
- toil/lib/generatedEC2Lists.py +127 -19
- toil/lib/history.py +1271 -0
- toil/lib/history_submission.py +681 -0
- toil/lib/humanize.py +6 -2
- toil/lib/io.py +121 -12
- toil/lib/iterables.py +4 -2
- toil/lib/memoize.py +12 -8
- toil/lib/misc.py +83 -18
- toil/lib/objects.py +2 -2
- toil/lib/resources.py +19 -7
- toil/lib/retry.py +125 -87
- toil/lib/threading.py +282 -80
- toil/lib/throttle.py +15 -14
- toil/lib/trs.py +390 -0
- toil/lib/web.py +38 -0
- toil/options/common.py +850 -402
- toil/options/cwl.py +185 -90
- toil/options/runner.py +50 -0
- toil/options/wdl.py +70 -19
- toil/provisioners/__init__.py +111 -46
- toil/provisioners/abstractProvisioner.py +322 -157
- toil/provisioners/aws/__init__.py +62 -30
- toil/provisioners/aws/awsProvisioner.py +980 -627
- toil/provisioners/clusterScaler.py +541 -279
- toil/provisioners/gceProvisioner.py +283 -180
- toil/provisioners/node.py +147 -79
- toil/realtimeLogger.py +34 -22
- toil/resource.py +137 -75
- toil/server/app.py +127 -61
- toil/server/celery_app.py +3 -1
- toil/server/cli/wes_cwl_runner.py +84 -55
- toil/server/utils.py +56 -31
- toil/server/wes/abstract_backend.py +64 -26
- toil/server/wes/amazon_wes_utils.py +21 -15
- toil/server/wes/tasks.py +121 -63
- toil/server/wes/toil_backend.py +142 -107
- toil/server/wsgi_app.py +4 -3
- toil/serviceManager.py +58 -22
- toil/statsAndLogging.py +183 -65
- toil/test/__init__.py +263 -179
- toil/test/batchSystems/batchSystemTest.py +438 -195
- toil/test/batchSystems/batch_system_plugin_test.py +18 -7
- toil/test/batchSystems/test_gridengine.py +173 -0
- toil/test/batchSystems/test_lsf_helper.py +67 -58
- toil/test/batchSystems/test_slurm.py +265 -49
- toil/test/cactus/test_cactus_integration.py +20 -22
- toil/test/cwl/conftest.py +39 -0
- toil/test/cwl/cwlTest.py +375 -72
- toil/test/cwl/measure_default_memory.cwl +12 -0
- toil/test/cwl/not_run_required_input.cwl +29 -0
- toil/test/cwl/optional-file.cwl +18 -0
- toil/test/cwl/scatter_duplicate_outputs.cwl +40 -0
- toil/test/docs/scriptsTest.py +60 -34
- toil/test/jobStores/jobStoreTest.py +412 -235
- toil/test/lib/aws/test_iam.py +116 -48
- toil/test/lib/aws/test_s3.py +16 -9
- toil/test/lib/aws/test_utils.py +5 -6
- toil/test/lib/dockerTest.py +118 -141
- toil/test/lib/test_conversions.py +113 -115
- toil/test/lib/test_ec2.py +57 -49
- toil/test/lib/test_history.py +212 -0
- toil/test/lib/test_misc.py +12 -5
- toil/test/lib/test_trs.py +161 -0
- toil/test/mesos/MesosDataStructuresTest.py +23 -10
- toil/test/mesos/helloWorld.py +7 -6
- toil/test/mesos/stress.py +25 -20
- toil/test/options/options.py +7 -2
- toil/test/provisioners/aws/awsProvisionerTest.py +293 -140
- toil/test/provisioners/clusterScalerTest.py +440 -250
- toil/test/provisioners/clusterTest.py +81 -42
- toil/test/provisioners/gceProvisionerTest.py +174 -100
- toil/test/provisioners/provisionerTest.py +25 -13
- toil/test/provisioners/restartScript.py +5 -4
- toil/test/server/serverTest.py +188 -141
- toil/test/sort/restart_sort.py +137 -68
- toil/test/sort/sort.py +134 -66
- toil/test/sort/sortTest.py +91 -49
- toil/test/src/autoDeploymentTest.py +140 -100
- toil/test/src/busTest.py +20 -18
- toil/test/src/checkpointTest.py +8 -2
- toil/test/src/deferredFunctionTest.py +49 -35
- toil/test/src/dockerCheckTest.py +33 -26
- toil/test/src/environmentTest.py +20 -10
- toil/test/src/fileStoreTest.py +538 -271
- toil/test/src/helloWorldTest.py +7 -4
- toil/test/src/importExportFileTest.py +61 -31
- toil/test/src/jobDescriptionTest.py +32 -17
- toil/test/src/jobEncapsulationTest.py +2 -0
- toil/test/src/jobFileStoreTest.py +74 -50
- toil/test/src/jobServiceTest.py +187 -73
- toil/test/src/jobTest.py +120 -70
- toil/test/src/miscTests.py +19 -18
- toil/test/src/promisedRequirementTest.py +82 -36
- toil/test/src/promisesTest.py +7 -6
- toil/test/src/realtimeLoggerTest.py +6 -6
- toil/test/src/regularLogTest.py +71 -37
- toil/test/src/resourceTest.py +80 -49
- toil/test/src/restartDAGTest.py +36 -22
- toil/test/src/resumabilityTest.py +9 -2
- toil/test/src/retainTempDirTest.py +45 -14
- toil/test/src/systemTest.py +12 -8
- toil/test/src/threadingTest.py +44 -25
- toil/test/src/toilContextManagerTest.py +10 -7
- toil/test/src/userDefinedJobArgTypeTest.py +8 -5
- toil/test/src/workerTest.py +33 -16
- toil/test/utils/toilDebugTest.py +70 -58
- toil/test/utils/toilKillTest.py +4 -5
- toil/test/utils/utilsTest.py +239 -102
- toil/test/wdl/wdltoil_test.py +789 -148
- toil/test/wdl/wdltoil_test_kubernetes.py +37 -23
- toil/toilState.py +52 -26
- toil/utils/toilConfig.py +13 -4
- toil/utils/toilDebugFile.py +44 -27
- toil/utils/toilDebugJob.py +85 -25
- toil/utils/toilDestroyCluster.py +11 -6
- toil/utils/toilKill.py +8 -3
- toil/utils/toilLaunchCluster.py +251 -145
- toil/utils/toilMain.py +37 -16
- toil/utils/toilRsyncCluster.py +27 -14
- toil/utils/toilSshCluster.py +45 -22
- toil/utils/toilStats.py +75 -36
- toil/utils/toilStatus.py +226 -119
- toil/utils/toilUpdateEC2Instances.py +3 -1
- toil/version.py +6 -6
- toil/wdl/utils.py +5 -5
- toil/wdl/wdltoil.py +3528 -1053
- toil/worker.py +370 -149
- toil-8.1.0b1.dist-info/METADATA +178 -0
- toil-8.1.0b1.dist-info/RECORD +259 -0
- {toil-7.0.0.dist-info → toil-8.1.0b1.dist-info}/WHEEL +1 -1
- toil-7.0.0.dist-info/METADATA +0 -158
- toil-7.0.0.dist-info/RECORD +0 -244
- {toil-7.0.0.dist-info → toil-8.1.0b1.dist-info}/LICENSE +0 -0
- {toil-7.0.0.dist-info → toil-8.1.0b1.dist-info}/entry_points.txt +0 -0
- {toil-7.0.0.dist-info → toil-8.1.0b1.dist-info}/top_level.txt +0 -0
toil/bus.py
CHANGED
|
@@ -67,32 +67,25 @@ import os
|
|
|
67
67
|
import queue
|
|
68
68
|
import tempfile
|
|
69
69
|
import threading
|
|
70
|
+
from collections.abc import Iterator
|
|
70
71
|
from dataclasses import dataclass
|
|
71
|
-
from typing import
|
|
72
|
-
Any,
|
|
73
|
-
Callable,
|
|
74
|
-
Dict,
|
|
75
|
-
Iterator,
|
|
76
|
-
List,
|
|
77
|
-
NamedTuple,
|
|
78
|
-
Optional,
|
|
79
|
-
Type,
|
|
80
|
-
TypeVar,
|
|
81
|
-
cast)
|
|
72
|
+
from typing import IO, Any, Callable, NamedTuple, Optional, TypeVar, cast
|
|
82
73
|
|
|
83
74
|
from pubsub.core import Publisher
|
|
84
75
|
from pubsub.core.listener import Listener
|
|
85
76
|
from pubsub.core.topicobj import Topic
|
|
86
77
|
from pubsub.core.topicutils import ALL_TOPICS
|
|
87
78
|
|
|
88
|
-
logger = logging.getLogger(
|
|
79
|
+
logger = logging.getLogger(__name__)
|
|
89
80
|
|
|
90
81
|
# We define some ways to talk about jobs.
|
|
91
82
|
|
|
83
|
+
|
|
92
84
|
class Names(NamedTuple):
|
|
93
85
|
"""
|
|
94
86
|
Stores all the kinds of name a job can have.
|
|
95
87
|
"""
|
|
88
|
+
|
|
96
89
|
# Name of the kind of job this is
|
|
97
90
|
job_name: str
|
|
98
91
|
# Name of this particular work unit
|
|
@@ -104,6 +97,7 @@ class Names(NamedTuple):
|
|
|
104
97
|
# Job store ID of the job for the work unit
|
|
105
98
|
job_store_id: str
|
|
106
99
|
|
|
100
|
+
|
|
107
101
|
def get_job_kind(names: Names) -> str:
|
|
108
102
|
"""
|
|
109
103
|
Return an identifying string for the job.
|
|
@@ -127,10 +121,12 @@ def get_job_kind(names: Names) -> str:
|
|
|
127
121
|
# We define a bunch of named tuple message types.
|
|
128
122
|
# These all need to be plain data: only hold ints, strings, etc.
|
|
129
123
|
|
|
124
|
+
|
|
130
125
|
class JobIssuedMessage(NamedTuple):
|
|
131
126
|
"""
|
|
132
127
|
Produced when a job is issued to run on the batch system.
|
|
133
128
|
"""
|
|
129
|
+
|
|
134
130
|
# The kind of job issued, for statistics aggregation
|
|
135
131
|
job_type: str
|
|
136
132
|
# The job store ID of the job
|
|
@@ -138,20 +134,24 @@ class JobIssuedMessage(NamedTuple):
|
|
|
138
134
|
# The toil batch ID of the job
|
|
139
135
|
toil_batch_id: int
|
|
140
136
|
|
|
137
|
+
|
|
141
138
|
class JobUpdatedMessage(NamedTuple):
|
|
142
139
|
"""
|
|
143
140
|
Produced when a job is "updated" and ready to have something happen to it.
|
|
144
141
|
"""
|
|
142
|
+
|
|
145
143
|
# The job store ID of the job
|
|
146
144
|
job_id: str
|
|
147
145
|
# The error code/return code for the job, which is nonzero if something has
|
|
148
146
|
# gone wrong, and 0 otherwise.
|
|
149
147
|
result_status: int
|
|
150
148
|
|
|
149
|
+
|
|
151
150
|
class JobCompletedMessage(NamedTuple):
|
|
152
151
|
"""
|
|
153
152
|
Produced when a job is completed, whether successful or not.
|
|
154
153
|
"""
|
|
154
|
+
|
|
155
155
|
# The kind of job issued, for statistics aggregation
|
|
156
156
|
job_type: str
|
|
157
157
|
# The job store ID of the job
|
|
@@ -159,27 +159,33 @@ class JobCompletedMessage(NamedTuple):
|
|
|
159
159
|
# Exit code for job_id
|
|
160
160
|
exit_code: int
|
|
161
161
|
|
|
162
|
+
|
|
162
163
|
class JobFailedMessage(NamedTuple):
|
|
163
164
|
"""
|
|
164
165
|
Produced when a job is completely failed, and will not be retried again.
|
|
165
166
|
"""
|
|
167
|
+
|
|
166
168
|
# The kind of job issued, for statistics aggregation
|
|
167
169
|
job_type: str
|
|
168
170
|
# The job store ID of the job
|
|
169
171
|
job_id: str
|
|
170
172
|
|
|
173
|
+
|
|
171
174
|
class JobMissingMessage(NamedTuple):
|
|
172
175
|
"""
|
|
173
176
|
Produced when a job goes missing and should be in the batch system but isn't.
|
|
174
177
|
"""
|
|
178
|
+
|
|
175
179
|
# The job store ID of the job
|
|
176
180
|
job_id: str
|
|
177
181
|
|
|
182
|
+
|
|
178
183
|
class JobAnnotationMessage(NamedTuple):
|
|
179
184
|
"""
|
|
180
185
|
Produced when extra information (such as an AWS Batch job ID from the
|
|
181
186
|
AWSBatchBatchSystem) is available that goes with a job.
|
|
182
187
|
"""
|
|
188
|
+
|
|
183
189
|
# The job store ID of the job
|
|
184
190
|
job_id: str
|
|
185
191
|
# The name of the annotation
|
|
@@ -187,50 +193,60 @@ class JobAnnotationMessage(NamedTuple):
|
|
|
187
193
|
# The annotation data
|
|
188
194
|
annotation_value: str
|
|
189
195
|
|
|
196
|
+
|
|
190
197
|
class ExternalBatchIdMessage(NamedTuple):
|
|
191
198
|
"""
|
|
192
199
|
Produced when using a batch system, links toil assigned batch ID to
|
|
193
200
|
Batch system ID (Whatever's returned by local implementation, PID, batch ID, etc)
|
|
194
201
|
"""
|
|
195
|
-
|
|
202
|
+
|
|
203
|
+
# Assigned toil batch job id
|
|
196
204
|
toil_batch_id: int
|
|
197
|
-
#Batch system scheduler identity
|
|
205
|
+
# Batch system scheduler identity
|
|
198
206
|
external_batch_id: str
|
|
199
|
-
#Batch system name
|
|
207
|
+
# Batch system name
|
|
200
208
|
batch_system: str
|
|
201
209
|
|
|
210
|
+
|
|
202
211
|
class QueueSizeMessage(NamedTuple):
|
|
203
212
|
"""
|
|
204
213
|
Produced to describe the size of the queue of jobs issued but not yet
|
|
205
214
|
completed. Theoretically recoverable from other messages.
|
|
206
215
|
"""
|
|
216
|
+
|
|
207
217
|
# The size of the queue
|
|
208
218
|
queue_size: int
|
|
209
219
|
|
|
220
|
+
|
|
210
221
|
class ClusterSizeMessage(NamedTuple):
|
|
211
222
|
"""
|
|
212
223
|
Produced by the Toil-integrated autoscaler describe the number of
|
|
213
224
|
instances of a certain type in a cluster.
|
|
214
225
|
"""
|
|
226
|
+
|
|
215
227
|
# The instance type name, like t4g.medium
|
|
216
228
|
instance_type: str
|
|
217
229
|
# The number of instances of that type that the Toil autoscaler thinks
|
|
218
230
|
# there are
|
|
219
231
|
current_size: int
|
|
220
232
|
|
|
233
|
+
|
|
221
234
|
class ClusterDesiredSizeMessage(NamedTuple):
|
|
222
235
|
"""
|
|
223
236
|
Produced by the Toil-integrated autoscaler to describe the number of
|
|
224
237
|
instances of a certain type that it thinks will be needed.
|
|
225
238
|
"""
|
|
239
|
+
|
|
226
240
|
# The instance type name, like t4g.medium
|
|
227
241
|
instance_type: str
|
|
228
242
|
# The number of instances of that type that the Toil autoscaler wants there
|
|
229
243
|
# to be
|
|
230
244
|
desired_size: int
|
|
231
245
|
|
|
246
|
+
|
|
232
247
|
# Then we define a serialization format.
|
|
233
248
|
|
|
249
|
+
|
|
234
250
|
def message_to_bytes(message: NamedTuple) -> bytes:
|
|
235
251
|
"""
|
|
236
252
|
Convert a plain-old-data named tuple into a byte string.
|
|
@@ -240,32 +256,39 @@ def message_to_bytes(message: NamedTuple) -> bytes:
|
|
|
240
256
|
if isinstance(item, (int, float, bool)) or item is None:
|
|
241
257
|
# This also handles e.g. values from an IntEnum, where the type extends int.
|
|
242
258
|
# They might replace __str__() but we hope they use a compatible __format__()
|
|
243
|
-
parts.append(f"{item}".encode(
|
|
259
|
+
parts.append(f"{item}".encode())
|
|
244
260
|
elif isinstance(item, str):
|
|
245
|
-
parts.append(item.encode(
|
|
261
|
+
parts.append(item.encode("unicode_escape"))
|
|
246
262
|
else:
|
|
247
263
|
# We haven't implemented this type yet.
|
|
248
|
-
raise RuntimeError(
|
|
249
|
-
|
|
264
|
+
raise RuntimeError(
|
|
265
|
+
f"Cannot store message argument of type {type(item)}: {item}"
|
|
266
|
+
)
|
|
267
|
+
return b"\t".join(parts)
|
|
250
268
|
|
|
251
269
|
|
|
252
270
|
# TODO: Messages have to be named tuple types.
|
|
253
|
-
MessageType = TypeVar(
|
|
254
|
-
|
|
271
|
+
MessageType = TypeVar("MessageType")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def bytes_to_message(message_type: type[MessageType], data: bytes) -> MessageType:
|
|
255
275
|
"""
|
|
256
276
|
Convert bytes from message_to_bytes back to a message of the given type.
|
|
257
277
|
"""
|
|
258
|
-
parts = data.split(b
|
|
278
|
+
parts = data.split(b"\t")
|
|
259
279
|
|
|
260
280
|
# Get a mapping from field name to type in the named tuple.
|
|
261
281
|
# We need to check a couple different fields because this moved in a recent
|
|
262
282
|
# Python 3 release.
|
|
263
|
-
field_to_type: Optional[
|
|
264
|
-
|
|
265
|
-
|
|
283
|
+
field_to_type: Optional[dict[str, type]] = cast(
|
|
284
|
+
Optional[dict[str, type]],
|
|
285
|
+
getattr(
|
|
286
|
+
message_type, "__annotations__", getattr(message_type, "_field_types", None)
|
|
287
|
+
),
|
|
288
|
+
)
|
|
266
289
|
if field_to_type is None:
|
|
267
290
|
raise RuntimeError(f"Cannot get field types from {message_type}")
|
|
268
|
-
field_names:
|
|
291
|
+
field_names: list[str] = getattr(message_type, "_fields")
|
|
269
292
|
|
|
270
293
|
if len(field_names) != len(parts):
|
|
271
294
|
raise RuntimeError(f"Cannot parse {field_names} from {parts}")
|
|
@@ -276,10 +299,10 @@ def bytes_to_message(message_type: Type[MessageType], data: bytes) -> MessageTyp
|
|
|
276
299
|
for name, part in zip(field_names, parts):
|
|
277
300
|
field_type = field_to_type[name]
|
|
278
301
|
if field_type in [int, float, bool]:
|
|
279
|
-
typed_parts.append(field_type(part.decode(
|
|
302
|
+
typed_parts.append(field_type(part.decode("utf-8")))
|
|
280
303
|
elif field_type == str:
|
|
281
304
|
# Decode, accounting for escape sequences
|
|
282
|
-
typed_parts.append(part.decode(
|
|
305
|
+
typed_parts.append(part.decode("unicode_escape"))
|
|
283
306
|
else:
|
|
284
307
|
raise RuntimeError(f"Cannot read message argument of type {field_type}")
|
|
285
308
|
|
|
@@ -287,8 +310,6 @@ def bytes_to_message(message_type: Type[MessageType], data: bytes) -> MessageTyp
|
|
|
287
310
|
return message_type(*typed_parts)
|
|
288
311
|
|
|
289
312
|
|
|
290
|
-
|
|
291
|
-
|
|
292
313
|
class MessageBus:
|
|
293
314
|
"""
|
|
294
315
|
Holds messages that should cause jobs to change their scheduling states.
|
|
@@ -317,7 +338,7 @@ class MessageBus:
|
|
|
317
338
|
characters, hierarchically dotted).
|
|
318
339
|
"""
|
|
319
340
|
|
|
320
|
-
return
|
|
341
|
+
return ".".join([message_type.__module__, message_type.__name__])
|
|
321
342
|
|
|
322
343
|
# All our messages are NamedTuples, but NamedTuples don't actually inherit
|
|
323
344
|
# from NamedTupe, so MyPy complains if we require that here.
|
|
@@ -360,13 +381,16 @@ class MessageBus:
|
|
|
360
381
|
Runs only in the owning thread. Delivers a message to its listeners.
|
|
361
382
|
"""
|
|
362
383
|
topic = self._type_to_name(type(message))
|
|
363
|
-
logger.debug(
|
|
384
|
+
logger.debug("Notifying %s with message: %s", topic, message)
|
|
364
385
|
self._pubsub.sendMessage(topic, message=message)
|
|
365
386
|
|
|
366
387
|
# This next function takes callables that take things of the type that was passed in as a
|
|
367
388
|
# runtime argument, which we can explain to MyPy using a TypeVar and Type[]
|
|
368
|
-
MessageType = TypeVar(
|
|
369
|
-
|
|
389
|
+
MessageType = TypeVar("MessageType", bound="NamedTuple")
|
|
390
|
+
|
|
391
|
+
def subscribe(
|
|
392
|
+
self, message_type: type[MessageType], handler: Callable[[MessageType], Any]
|
|
393
|
+
) -> Listener:
|
|
370
394
|
"""
|
|
371
395
|
Register the given callable to be called when messages of the given type are sent.
|
|
372
396
|
It will be called with messages sent after the subscription is created.
|
|
@@ -374,7 +398,7 @@ class MessageBus:
|
|
|
374
398
|
"""
|
|
375
399
|
|
|
376
400
|
topic = self._type_to_name(message_type)
|
|
377
|
-
logger.debug(
|
|
401
|
+
logger.debug("Listening for message topic: %s", topic)
|
|
378
402
|
|
|
379
403
|
# Make sure to wrap the handler so we get the right argument name and
|
|
380
404
|
# we can control lifetime.
|
|
@@ -387,10 +411,10 @@ class MessageBus:
|
|
|
387
411
|
# Hide the handler function in the pubsub listener to keep it alive.
|
|
388
412
|
# If it goes out of scope the subscription expires, and the pubsub
|
|
389
413
|
# system only uses weak references.
|
|
390
|
-
setattr(listener,
|
|
414
|
+
setattr(listener, "handler_wrapper", handler_wraper)
|
|
391
415
|
return listener
|
|
392
416
|
|
|
393
|
-
def connect(self, wanted_types:
|
|
417
|
+
def connect(self, wanted_types: list[type]) -> "MessageBusConnection":
|
|
394
418
|
"""
|
|
395
419
|
Get a connection object that serves as an inbox for messages of the
|
|
396
420
|
given types.
|
|
@@ -402,7 +426,7 @@ class MessageBus:
|
|
|
402
426
|
connection._set_bus_and_message_types(self, wanted_types)
|
|
403
427
|
return connection
|
|
404
428
|
|
|
405
|
-
def outbox(self) ->
|
|
429
|
+
def outbox(self) -> "MessageOutbox":
|
|
406
430
|
"""
|
|
407
431
|
Get a connection object that only allows sending messages.
|
|
408
432
|
"""
|
|
@@ -420,24 +444,27 @@ class MessageBus:
|
|
|
420
444
|
somewhere or delete it.
|
|
421
445
|
"""
|
|
422
446
|
|
|
423
|
-
|
|
424
|
-
stream = open(file_path, 'wb')
|
|
447
|
+
stream = open(file_path, "wb")
|
|
425
448
|
|
|
426
449
|
# Type of the ** is the value type of the dictionary; key type is always string.
|
|
427
|
-
def handler(
|
|
450
|
+
def handler(
|
|
451
|
+
topic_object: Topic = Listener.AUTO_TOPIC, **message_data: NamedTuple
|
|
452
|
+
) -> None:
|
|
428
453
|
"""
|
|
429
454
|
Log the message in the given message data, associated with the
|
|
430
455
|
given topic.
|
|
431
456
|
"""
|
|
432
457
|
# There should always be a "message"
|
|
433
|
-
if len(message_data) != 1 or
|
|
434
|
-
raise RuntimeError(
|
|
435
|
-
|
|
458
|
+
if len(message_data) != 1 or "message" not in message_data:
|
|
459
|
+
raise RuntimeError(
|
|
460
|
+
"Cannot log the bus message. The message is either empty/malformed or there are too many messages provided."
|
|
461
|
+
)
|
|
462
|
+
message = message_data["message"]
|
|
436
463
|
topic = topic_object.getName()
|
|
437
|
-
stream.write(topic.encode(
|
|
438
|
-
stream.write(b
|
|
464
|
+
stream.write(topic.encode("utf-8"))
|
|
465
|
+
stream.write(b"\t")
|
|
439
466
|
stream.write(message_to_bytes(message))
|
|
440
|
-
stream.write(b
|
|
467
|
+
stream.write(b"\n")
|
|
441
468
|
stream.flush()
|
|
442
469
|
|
|
443
470
|
listener, _ = self._pubsub.subscribe(handler, ALL_TOPICS)
|
|
@@ -446,7 +473,6 @@ class MessageBus:
|
|
|
446
473
|
# want the pypubsub Listener.
|
|
447
474
|
return (handler, listener)
|
|
448
475
|
|
|
449
|
-
|
|
450
476
|
# TODO: If we annotate this as returning an Iterator[NamedTuple], MyPy
|
|
451
477
|
# complains when we loop over it that the loop variable is a <nothing>,
|
|
452
478
|
# ifen in code protected by isinstance(). Using a typevar makes it complain
|
|
@@ -456,7 +482,9 @@ class MessageBus:
|
|
|
456
482
|
# union of the types passed in message_types, in a way that MyPy can
|
|
457
483
|
# understand.
|
|
458
484
|
@classmethod
|
|
459
|
-
def scan_bus_messages(
|
|
485
|
+
def scan_bus_messages(
|
|
486
|
+
cls, stream: IO[bytes], message_types: list[type[NamedTuple]]
|
|
487
|
+
) -> Iterator[Any]:
|
|
460
488
|
"""
|
|
461
489
|
Get an iterator over all messages in the given log stream of the given
|
|
462
490
|
types, in order. Discard any trailing partial messages.
|
|
@@ -466,15 +494,15 @@ class MessageBus:
|
|
|
466
494
|
name_to_type = {cls._type_to_name(t): t for t in message_types}
|
|
467
495
|
|
|
468
496
|
for line in stream:
|
|
469
|
-
logger.debug(
|
|
470
|
-
if not line.endswith(b
|
|
497
|
+
logger.debug("Got message: %s", line)
|
|
498
|
+
if not line.endswith(b"\n"):
|
|
471
499
|
# Skip unterminated line
|
|
472
500
|
continue
|
|
473
501
|
# Drop the newline and split on first tab
|
|
474
|
-
parts = line[:-1].split(b
|
|
502
|
+
parts = line[:-1].split(b"\t", 1)
|
|
475
503
|
|
|
476
504
|
# Get the type of the message
|
|
477
|
-
message_type = name_to_type.get(parts[0].decode(
|
|
505
|
+
message_type = name_to_type.get(parts[0].decode("utf-8"))
|
|
478
506
|
if message_type is None:
|
|
479
507
|
# We aren't interested in this kind of message.
|
|
480
508
|
continue
|
|
@@ -485,6 +513,7 @@ class MessageBus:
|
|
|
485
513
|
# And produce it
|
|
486
514
|
yield message
|
|
487
515
|
|
|
516
|
+
|
|
488
517
|
class MessageBusClient:
|
|
489
518
|
"""
|
|
490
519
|
Base class for clients (inboxes and outboxes) of a message bus. Handles
|
|
@@ -507,6 +536,7 @@ class MessageBusClient:
|
|
|
507
536
|
"""
|
|
508
537
|
self._bus = bus
|
|
509
538
|
|
|
539
|
+
|
|
510
540
|
class MessageInbox(MessageBusClient):
|
|
511
541
|
"""
|
|
512
542
|
A buffered connection to a message bus that lets us receive messages.
|
|
@@ -522,16 +552,19 @@ class MessageInbox(MessageBusClient):
|
|
|
522
552
|
super().__init__()
|
|
523
553
|
|
|
524
554
|
# This holds all the messages on the bus, organized by type.
|
|
525
|
-
self._messages_by_type:
|
|
555
|
+
self._messages_by_type: dict[type, list[Any]] = {}
|
|
526
556
|
# This holds listeners for all the types, when we connect to a bus
|
|
527
|
-
self._listeners_by_type:
|
|
557
|
+
self._listeners_by_type: dict[type, Listener] = {}
|
|
528
558
|
|
|
529
559
|
# We define a handler for messages
|
|
530
560
|
def on_message(message: Any) -> None:
|
|
531
561
|
self._messages_by_type[type(message)].append(message)
|
|
562
|
+
|
|
532
563
|
self._handler = on_message
|
|
533
564
|
|
|
534
|
-
def _set_bus_and_message_types(
|
|
565
|
+
def _set_bus_and_message_types(
|
|
566
|
+
self, bus: MessageBus, wanted_types: list[type]
|
|
567
|
+
) -> None:
|
|
535
568
|
"""
|
|
536
569
|
Connect to the given bus and collect the given message types.
|
|
537
570
|
|
|
@@ -576,8 +609,9 @@ class MessageInbox(MessageBusClient):
|
|
|
576
609
|
|
|
577
610
|
# This next function returns things of the type that was passed in as a
|
|
578
611
|
# runtime argument, which we can explain to MyPy using a TypeVar and Type[]
|
|
579
|
-
MessageType = TypeVar(
|
|
580
|
-
|
|
612
|
+
MessageType = TypeVar("MessageType")
|
|
613
|
+
|
|
614
|
+
def for_each(self, message_type: type[MessageType]) -> Iterator[MessageType]:
|
|
581
615
|
"""
|
|
582
616
|
Loop over all messages currently pending of the given type. Each that
|
|
583
617
|
is handled without raising an exception will be removed.
|
|
@@ -607,7 +641,9 @@ class MessageInbox(MessageBusClient):
|
|
|
607
641
|
try:
|
|
608
642
|
# Emit the message
|
|
609
643
|
if not isinstance(message, message_type):
|
|
610
|
-
raise RuntimeError(
|
|
644
|
+
raise RuntimeError(
|
|
645
|
+
f"Unacceptable message type {type(message)} in list for type {message_type}"
|
|
646
|
+
)
|
|
611
647
|
yield message
|
|
612
648
|
# If we get here it was handled without error.
|
|
613
649
|
handled = True
|
|
@@ -622,7 +658,10 @@ class MessageInbox(MessageBusClient):
|
|
|
622
658
|
# Dump anything remaining in our buffer back into the main buffer,
|
|
623
659
|
# in the right order, and before the later messages.
|
|
624
660
|
message_list.reverse()
|
|
625
|
-
self._messages_by_type[message_type] =
|
|
661
|
+
self._messages_by_type[message_type] = (
|
|
662
|
+
message_list + self._messages_by_type[message_type]
|
|
663
|
+
)
|
|
664
|
+
|
|
626
665
|
|
|
627
666
|
class MessageOutbox(MessageBusClient):
|
|
628
667
|
"""
|
|
@@ -645,6 +684,7 @@ class MessageOutbox(MessageBusClient):
|
|
|
645
684
|
raise RuntimeError("Cannot send message when not connected to a bus")
|
|
646
685
|
self._bus.publish(message)
|
|
647
686
|
|
|
687
|
+
|
|
648
688
|
class MessageBusConnection(MessageInbox, MessageOutbox):
|
|
649
689
|
"""
|
|
650
690
|
A two-way connection to a message bus. Buffers incoming messages until you
|
|
@@ -657,7 +697,9 @@ class MessageBusConnection(MessageInbox, MessageOutbox):
|
|
|
657
697
|
"""
|
|
658
698
|
super().__init__()
|
|
659
699
|
|
|
660
|
-
def _set_bus_and_message_types(
|
|
700
|
+
def _set_bus_and_message_types(
|
|
701
|
+
self, bus: MessageBus, wanted_types: list[type]
|
|
702
|
+
) -> None:
|
|
661
703
|
"""
|
|
662
704
|
Connect to the given bus and collect the given message types.
|
|
663
705
|
|
|
@@ -673,20 +715,28 @@ class MessageBusConnection(MessageInbox, MessageOutbox):
|
|
|
673
715
|
class JobStatus:
|
|
674
716
|
"""
|
|
675
717
|
Records the status of a job.
|
|
718
|
+
|
|
719
|
+
When exit_code is -1, this means the job is either not observed or currently running.
|
|
676
720
|
"""
|
|
677
721
|
|
|
678
722
|
job_store_id: str
|
|
679
723
|
name: str
|
|
680
724
|
exit_code: int
|
|
681
|
-
annotations:
|
|
725
|
+
annotations: dict[str, str]
|
|
682
726
|
toil_batch_id: int
|
|
683
727
|
external_batch_id: str
|
|
684
728
|
batch_system: str
|
|
685
729
|
|
|
686
730
|
def __repr__(self) -> str:
|
|
687
|
-
return json.dumps(self, default=
|
|
731
|
+
return json.dumps(self, default=lambda o: o.__dict__, indent=4)
|
|
732
|
+
|
|
733
|
+
def is_running(self) -> bool:
|
|
734
|
+
return (
|
|
735
|
+
self.exit_code < 0 and self.job_store_id != ""
|
|
736
|
+
) # if the exit code is -1 and the job id is specified, we assume the job is running
|
|
688
737
|
|
|
689
|
-
|
|
738
|
+
|
|
739
|
+
def replay_message_bus(path: str) -> dict[str, JobStatus]:
|
|
690
740
|
"""
|
|
691
741
|
Replay all the messages and work out what they mean for jobs.
|
|
692
742
|
|
|
@@ -702,15 +752,26 @@ def replay_message_bus(path: str) -> Dict[str, JobStatus]:
|
|
|
702
752
|
is running.
|
|
703
753
|
"""
|
|
704
754
|
|
|
705
|
-
job_statuses:
|
|
755
|
+
job_statuses: dict[str, JobStatus] = collections.defaultdict(
|
|
756
|
+
lambda: JobStatus("", "", -1, {}, -1, "", "")
|
|
757
|
+
)
|
|
706
758
|
batch_to_job_id = {}
|
|
707
759
|
try:
|
|
708
|
-
with open(path,
|
|
760
|
+
with open(path, "rb") as log_stream:
|
|
709
761
|
# Read all the full, properly-terminated messages about job updates
|
|
710
|
-
for event in MessageBus.scan_bus_messages(
|
|
711
|
-
|
|
762
|
+
for event in MessageBus.scan_bus_messages(
|
|
763
|
+
log_stream,
|
|
764
|
+
[
|
|
765
|
+
JobUpdatedMessage,
|
|
766
|
+
JobIssuedMessage,
|
|
767
|
+
JobCompletedMessage,
|
|
768
|
+
JobFailedMessage,
|
|
769
|
+
JobAnnotationMessage,
|
|
770
|
+
ExternalBatchIdMessage,
|
|
771
|
+
],
|
|
772
|
+
):
|
|
712
773
|
# And for each of them
|
|
713
|
-
logger.
|
|
774
|
+
logger.debug("Got message from workflow: %s", event)
|
|
714
775
|
|
|
715
776
|
if isinstance(event, JobUpdatedMessage):
|
|
716
777
|
# Apply the latest return code from the job with this ID.
|
|
@@ -731,16 +792,23 @@ def replay_message_bus(path: str) -> Dict[str, JobStatus]:
|
|
|
731
792
|
job_statuses[event.job_id].exit_code = 1
|
|
732
793
|
elif isinstance(event, JobAnnotationMessage):
|
|
733
794
|
# Remember the last value of any annotation that is set
|
|
734
|
-
job_statuses[event.job_id].annotations[
|
|
795
|
+
job_statuses[event.job_id].annotations[
|
|
796
|
+
event.annotation_name
|
|
797
|
+
] = event.annotation_value
|
|
735
798
|
elif isinstance(event, ExternalBatchIdMessage):
|
|
736
799
|
if event.toil_batch_id in batch_to_job_id:
|
|
737
|
-
job_statuses[
|
|
738
|
-
|
|
800
|
+
job_statuses[
|
|
801
|
+
batch_to_job_id[event.toil_batch_id]
|
|
802
|
+
].external_batch_id = event.external_batch_id
|
|
803
|
+
job_statuses[
|
|
804
|
+
batch_to_job_id[event.toil_batch_id]
|
|
805
|
+
].batch_system = event.batch_system
|
|
739
806
|
except FileNotFoundError:
|
|
740
807
|
logger.warning("We were unable to access the file")
|
|
741
808
|
|
|
742
809
|
return job_statuses
|
|
743
810
|
|
|
811
|
+
|
|
744
812
|
def gen_message_bus_path(tmpdir: Optional[str] = None) -> str:
|
|
745
813
|
"""
|
|
746
814
|
Return a file path in tmp to store the message bus at.
|
|
@@ -753,4 +821,4 @@ def gen_message_bus_path(tmpdir: Optional[str] = None) -> str:
|
|
|
753
821
|
fd, path = tempfile.mkstemp(dir=tmpdir)
|
|
754
822
|
os.close(fd)
|
|
755
823
|
return path
|
|
756
|
-
#TODO Might want to clean up the tmpfile at some point after running the workflow
|
|
824
|
+
# TODO Might want to clean up the tmpfile at some point after running the workflow
|