parsl 2024.3.18__py3-none-any.whl → 2024.3.25__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.
- parsl/dataflow/dflow.py +11 -4
- parsl/executors/base.py +10 -0
- parsl/executors/status_handling.py +2 -9
- parsl/executors/taskvine/executor.py +21 -1
- parsl/executors/workqueue/executor.py +19 -0
- parsl/jobs/error_handlers.py +1 -1
- parsl/jobs/job_status_poller.py +4 -4
- parsl/monitoring/monitoring.py +26 -217
- parsl/monitoring/router.py +208 -0
- parsl/tests/test_monitoring/test_fuzz_zmq.py +2 -2
- parsl/tests/test_monitoring/test_htex_init_blocks_vs_monitoring.py +82 -0
- parsl/tests/test_python_apps/test_context_manager.py +40 -0
- parsl/tests/test_shutdown/__init__.py +0 -0
- parsl/tests/test_shutdown/test_kill_monitoring.py +65 -0
- parsl/version.py +1 -1
- {parsl-2024.3.18.dist-info → parsl-2024.3.25.dist-info}/METADATA +4 -4
- {parsl-2024.3.18.dist-info → parsl-2024.3.25.dist-info}/RECORD +24 -19
- {parsl-2024.3.18.data → parsl-2024.3.25.data}/scripts/exec_parsl_function.py +0 -0
- {parsl-2024.3.18.data → parsl-2024.3.25.data}/scripts/parsl_coprocess.py +0 -0
- {parsl-2024.3.18.data → parsl-2024.3.25.data}/scripts/process_worker_pool.py +0 -0
- {parsl-2024.3.18.dist-info → parsl-2024.3.25.dist-info}/LICENSE +0 -0
- {parsl-2024.3.18.dist-info → parsl-2024.3.25.dist-info}/WHEEL +0 -0
- {parsl-2024.3.18.dist-info → parsl-2024.3.25.dist-info}/entry_points.txt +0 -0
- {parsl-2024.3.18.dist-info → parsl-2024.3.25.dist-info}/top_level.txt +0 -0
parsl/dataflow/dflow.py
CHANGED
@@ -108,12 +108,12 @@ class DataFlowKernel:
|
|
108
108
|
|
109
109
|
# hub address and port for interchange to connect
|
110
110
|
self.hub_address = None # type: Optional[str]
|
111
|
-
self.
|
111
|
+
self.hub_zmq_port = None # type: Optional[int]
|
112
112
|
if self.monitoring:
|
113
113
|
if self.monitoring.logdir is None:
|
114
114
|
self.monitoring.logdir = self.run_dir
|
115
115
|
self.hub_address = self.monitoring.hub_address
|
116
|
-
self.
|
116
|
+
self.hub_zmq_port = self.monitoring.start(self.run_id, self.run_dir, self.config.run_dir)
|
117
117
|
|
118
118
|
self.time_began = datetime.datetime.now()
|
119
119
|
self.time_completed: Optional[datetime.datetime] = None
|
@@ -206,6 +206,13 @@ class DataFlowKernel:
|
|
206
206
|
|
207
207
|
atexit.register(self.atexit_cleanup)
|
208
208
|
|
209
|
+
def __enter__(self):
|
210
|
+
pass
|
211
|
+
|
212
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
213
|
+
logger.debug("Exiting the context manager, calling cleanup for DFK")
|
214
|
+
self.cleanup()
|
215
|
+
|
209
216
|
def _send_task_log_info(self, task_record: TaskRecord) -> None:
|
210
217
|
if self.monitoring:
|
211
218
|
task_log_info = self._create_task_log_info(task_record)
|
@@ -1115,12 +1122,12 @@ class DataFlowKernel:
|
|
1115
1122
|
|
1116
1123
|
channel.makedirs(channel.script_dir, exist_ok=True)
|
1117
1124
|
|
1118
|
-
def add_executors(self, executors):
|
1125
|
+
def add_executors(self, executors: Sequence[ParslExecutor]) -> None:
|
1119
1126
|
for executor in executors:
|
1120
1127
|
executor.run_id = self.run_id
|
1121
1128
|
executor.run_dir = self.run_dir
|
1122
1129
|
executor.hub_address = self.hub_address
|
1123
|
-
executor.hub_port = self.
|
1130
|
+
executor.hub_port = self.hub_zmq_port
|
1124
1131
|
if hasattr(executor, 'provider'):
|
1125
1132
|
if hasattr(executor.provider, 'script_dir'):
|
1126
1133
|
executor.provider.script_dir = os.path.join(self.run_dir, 'submit_scripts')
|
parsl/executors/base.py
CHANGED
@@ -106,6 +106,16 @@ class ParslExecutor(metaclass=ABCMeta):
|
|
106
106
|
def run_dir(self, value: str) -> None:
|
107
107
|
self._run_dir = value
|
108
108
|
|
109
|
+
@property
|
110
|
+
def run_id(self) -> Optional[str]:
|
111
|
+
"""UUID for the enclosing DFK.
|
112
|
+
"""
|
113
|
+
return self._run_id
|
114
|
+
|
115
|
+
@run_id.setter
|
116
|
+
def run_id(self, value: Optional[str]) -> None:
|
117
|
+
self._run_id = value
|
118
|
+
|
109
119
|
@property
|
110
120
|
def hub_address(self) -> Optional[str]:
|
111
121
|
"""Address to the Hub for monitoring.
|
@@ -61,7 +61,7 @@ class BlockProviderExecutor(ParslExecutor):
|
|
61
61
|
# errors can happen during the submit call to the provider; this is used
|
62
62
|
# to keep track of such errors so that they can be handled in one place
|
63
63
|
# together with errors reported by status()
|
64
|
-
self._simulated_status: Dict[
|
64
|
+
self._simulated_status: Dict[str, JobStatus] = {}
|
65
65
|
self._executor_bad_state = threading.Event()
|
66
66
|
self._executor_exception: Optional[Exception] = None
|
67
67
|
|
@@ -102,13 +102,10 @@ class BlockProviderExecutor(ParslExecutor):
|
|
102
102
|
else:
|
103
103
|
return self._provider.status_polling_interval
|
104
104
|
|
105
|
-
def _fail_job_async(self, block_id:
|
105
|
+
def _fail_job_async(self, block_id: str, message: str):
|
106
106
|
"""Marks a job that has failed to start but would not otherwise be included in status()
|
107
107
|
as failed and report it in status()
|
108
108
|
"""
|
109
|
-
if block_id is None:
|
110
|
-
block_id = str(self._block_id_counter.get_id())
|
111
|
-
logger.info(f"Allocated block ID {block_id} for simulated failure")
|
112
109
|
self._simulated_status[block_id] = JobStatus(JobState.FAILED, message)
|
113
110
|
|
114
111
|
@abstractproperty
|
@@ -211,10 +208,6 @@ class BlockProviderExecutor(ParslExecutor):
|
|
211
208
|
|
212
209
|
Cause the executor to reduce the number of blocks by count.
|
213
210
|
|
214
|
-
We should have the scale in method simply take resource object
|
215
|
-
which will have the scaling methods, scale_in itself should be a coroutine, since
|
216
|
-
scaling tasks can be slow.
|
217
|
-
|
218
211
|
:return: A list of block ids corresponding to the blocks that were removed.
|
219
212
|
"""
|
220
213
|
pass
|
@@ -4,6 +4,7 @@ high-throughput system for delegating Parsl tasks to thousands of remote machine
|
|
4
4
|
"""
|
5
5
|
|
6
6
|
# Import Python built-in libraries
|
7
|
+
import atexit
|
7
8
|
import threading
|
8
9
|
import multiprocessing
|
9
10
|
import logging
|
@@ -171,7 +172,7 @@ class TaskVineExecutor(BlockProviderExecutor, putils.RepresentationMixin):
|
|
171
172
|
# Path to directory that holds all tasks' data and results.
|
172
173
|
self._function_data_dir = ""
|
173
174
|
|
174
|
-
#
|
175
|
+
# Helper scripts to prepare package tarballs for Parsl apps
|
175
176
|
self._package_analyze_script = shutil.which("poncho_package_analyze")
|
176
177
|
self._package_create_script = shutil.which("poncho_package_create")
|
177
178
|
if self._package_analyze_script is None or self._package_create_script is None:
|
@@ -179,6 +180,18 @@ class TaskVineExecutor(BlockProviderExecutor, putils.RepresentationMixin):
|
|
179
180
|
else:
|
180
181
|
self._poncho_available = True
|
181
182
|
|
183
|
+
# Register atexit handler to cleanup when Python shuts down
|
184
|
+
atexit.register(self.atexit_cleanup)
|
185
|
+
|
186
|
+
# Attribute indicating whether this executor was started to shut it down properly.
|
187
|
+
# This safeguards cases where an object of this executor is created but
|
188
|
+
# the executor never starts, so it shouldn't be shutdowned.
|
189
|
+
self._started = False
|
190
|
+
|
191
|
+
def atexit_cleanup(self):
|
192
|
+
# Calls this executor's shutdown method upon Python exiting the process.
|
193
|
+
self.shutdown()
|
194
|
+
|
182
195
|
def _get_launch_command(self, block_id):
|
183
196
|
# Implements BlockProviderExecutor's abstract method.
|
184
197
|
# This executor uses different terminology for worker/launch
|
@@ -238,6 +251,9 @@ class TaskVineExecutor(BlockProviderExecutor, putils.RepresentationMixin):
|
|
238
251
|
retrieve Parsl tasks within the TaskVine system.
|
239
252
|
"""
|
240
253
|
|
254
|
+
# Mark this executor object as started
|
255
|
+
self._started = True
|
256
|
+
|
241
257
|
# Synchronize connection and communication settings between the manager and factory
|
242
258
|
self.__synchronize_manager_factory_comm_settings()
|
243
259
|
|
@@ -598,6 +614,10 @@ class TaskVineExecutor(BlockProviderExecutor, putils.RepresentationMixin):
|
|
598
614
|
"""Shutdown the executor. Sets flag to cancel the submit process and
|
599
615
|
collector thread, which shuts down the TaskVine system submission.
|
600
616
|
"""
|
617
|
+
if not self._started:
|
618
|
+
# Don't shutdown if the executor never starts.
|
619
|
+
return
|
620
|
+
|
601
621
|
logger.debug("TaskVine shutdown started")
|
602
622
|
self._should_stop.set()
|
603
623
|
|
@@ -3,6 +3,7 @@ Cooperative Computing Lab (CCL) at Notre Dame to provide a fault-tolerant,
|
|
3
3
|
high-throughput system for delegating Parsl tasks to thousands of remote machines
|
4
4
|
"""
|
5
5
|
|
6
|
+
import atexit
|
6
7
|
import threading
|
7
8
|
import multiprocessing
|
8
9
|
import logging
|
@@ -298,6 +299,18 @@ class WorkQueueExecutor(BlockProviderExecutor, putils.RepresentationMixin):
|
|
298
299
|
if self.init_command != "":
|
299
300
|
self.launch_cmd = self.init_command + "; " + self.launch_cmd
|
300
301
|
|
302
|
+
# register atexit handler to cleanup when Python shuts down
|
303
|
+
atexit.register(self.atexit_cleanup)
|
304
|
+
|
305
|
+
# Attribute indicating whether this executor was started to shut it down properly.
|
306
|
+
# This safeguards cases where an object of this executor is created but
|
307
|
+
# the executor never starts, so it shouldn't be shutdowned.
|
308
|
+
self.started = False
|
309
|
+
|
310
|
+
def atexit_cleanup(self):
|
311
|
+
# Calls this executor's shutdown method upon Python exiting the process.
|
312
|
+
self.shutdown()
|
313
|
+
|
301
314
|
def _get_launch_command(self, block_id):
|
302
315
|
# this executor uses different terminology for worker/launch
|
303
316
|
# commands than in htex
|
@@ -307,6 +320,8 @@ class WorkQueueExecutor(BlockProviderExecutor, putils.RepresentationMixin):
|
|
307
320
|
"""Create submit process and collector thread to create, send, and
|
308
321
|
retrieve Parsl tasks within the Work Queue system.
|
309
322
|
"""
|
323
|
+
# Mark this executor object as started
|
324
|
+
self.started = True
|
310
325
|
self.tasks_lock = threading.Lock()
|
311
326
|
|
312
327
|
# Create directories for data and results
|
@@ -695,6 +710,10 @@ class WorkQueueExecutor(BlockProviderExecutor, putils.RepresentationMixin):
|
|
695
710
|
"""Shutdown the executor. Sets flag to cancel the submit process and
|
696
711
|
collector thread, which shuts down the Work Queue system submission.
|
697
712
|
"""
|
713
|
+
if not self.started:
|
714
|
+
# Don't shutdown if the executor never starts.
|
715
|
+
return
|
716
|
+
|
698
717
|
logger.debug("Work Queue shutdown started")
|
699
718
|
self.should_stop.value = True
|
700
719
|
|
parsl/jobs/error_handlers.py
CHANGED
@@ -20,7 +20,7 @@ def simple_error_handler(executor: status_handling.BlockProviderExecutor, status
|
|
20
20
|
executor.set_bad_state_and_fail_all(_get_error(status))
|
21
21
|
|
22
22
|
|
23
|
-
def windowed_error_handler(executor: status_handling.BlockProviderExecutor, status: Dict[str, JobStatus], threshold: int = 3):
|
23
|
+
def windowed_error_handler(executor: status_handling.BlockProviderExecutor, status: Dict[str, JobStatus], threshold: int = 3) -> None:
|
24
24
|
sorted_status = [(key, status[key]) for key in sorted(status, key=lambda x: int(x))]
|
25
25
|
current_window = dict(sorted_status[-threshold:])
|
26
26
|
total, failed = _count_jobs(current_window)
|
parsl/jobs/job_status_poller.py
CHANGED
@@ -29,7 +29,7 @@ class PollItem:
|
|
29
29
|
if self._dfk and self._dfk.monitoring is not None:
|
30
30
|
self.monitoring_enabled = True
|
31
31
|
hub_address = self._dfk.hub_address
|
32
|
-
hub_port = self._dfk.
|
32
|
+
hub_port = self._dfk.hub_zmq_port
|
33
33
|
context = zmq.Context()
|
34
34
|
self.hub_channel = context.socket(zmq.DEALER)
|
35
35
|
self.hub_channel.set_hwm(0)
|
@@ -72,7 +72,7 @@ class PollItem:
|
|
72
72
|
def executor(self) -> BlockProviderExecutor:
|
73
73
|
return self._executor
|
74
74
|
|
75
|
-
def scale_in(self, n, max_idletime=None):
|
75
|
+
def scale_in(self, n: int, max_idletime: Optional[float] = None) -> List[str]:
|
76
76
|
|
77
77
|
if max_idletime is None:
|
78
78
|
block_ids = self._executor.scale_in(n)
|
@@ -82,7 +82,7 @@ class PollItem:
|
|
82
82
|
# scale_in method really does come from HighThroughputExecutor,
|
83
83
|
# and so does have an extra max_idletime parameter not present
|
84
84
|
# in the executor interface.
|
85
|
-
block_ids = self._executor.scale_in(n, max_idletime=max_idletime)
|
85
|
+
block_ids = self._executor.scale_in(n, max_idletime=max_idletime) # type: ignore[call-arg]
|
86
86
|
if block_ids is not None:
|
87
87
|
new_status = {}
|
88
88
|
for block_id in block_ids:
|
@@ -91,7 +91,7 @@ class PollItem:
|
|
91
91
|
self.send_monitoring_info(new_status)
|
92
92
|
return block_ids
|
93
93
|
|
94
|
-
def scale_out(self, n):
|
94
|
+
def scale_out(self, n: int) -> List[str]:
|
95
95
|
block_ids = self._executor.scale_out(n)
|
96
96
|
if block_ids is not None:
|
97
97
|
new_status = {}
|
parsl/monitoring/monitoring.py
CHANGED
@@ -1,9 +1,7 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import os
|
4
|
-
import socket
|
5
4
|
import time
|
6
|
-
import pickle
|
7
5
|
import logging
|
8
6
|
import typeguard
|
9
7
|
import zmq
|
@@ -22,8 +20,9 @@ from parsl.utils import setproctitle
|
|
22
20
|
|
23
21
|
from parsl.serialize import deserialize
|
24
22
|
|
23
|
+
from parsl.monitoring.router import router_starter
|
25
24
|
from parsl.monitoring.message_type import MessageType
|
26
|
-
from parsl.monitoring.types import AddressedMonitoringMessage
|
25
|
+
from parsl.monitoring.types import AddressedMonitoringMessage
|
27
26
|
from typing import cast, Any, Callable, Dict, Optional, Sequence, Tuple, Union, TYPE_CHECKING
|
28
27
|
|
29
28
|
_db_manager_excepts: Optional[Exception]
|
@@ -93,8 +92,6 @@ class MonitoringHub(RepresentationMixin):
|
|
93
92
|
Default: 30 seconds
|
94
93
|
"""
|
95
94
|
|
96
|
-
self.logger = logger
|
97
|
-
|
98
95
|
# Any is used to disable typechecking on uses of _dfk_channel,
|
99
96
|
# because it is used in the code as if it points to a channel, but
|
100
97
|
# the static type is that it can also be None. The code relies on
|
@@ -120,6 +117,8 @@ class MonitoringHub(RepresentationMixin):
|
|
120
117
|
|
121
118
|
def start(self, run_id: str, dfk_run_dir: str, config_run_dir: Union[str, os.PathLike]) -> int:
|
122
119
|
|
120
|
+
logger.debug("Starting MonitoringHub")
|
121
|
+
|
123
122
|
if self.logdir is None:
|
124
123
|
self.logdir = "."
|
125
124
|
|
@@ -128,9 +127,6 @@ class MonitoringHub(RepresentationMixin):
|
|
128
127
|
|
129
128
|
os.makedirs(self.logdir, exist_ok=True)
|
130
129
|
|
131
|
-
# Initialize the ZMQ pipe to the Parsl Client
|
132
|
-
|
133
|
-
self.logger.debug("Initializing ZMQ Pipes to client")
|
134
130
|
self.monitoring_hub_active = True
|
135
131
|
|
136
132
|
# This annotation is incompatible with typeguard 4.x instrumentation
|
@@ -166,8 +162,8 @@ class MonitoringHub(RepresentationMixin):
|
|
166
162
|
self.router_proc = ForkProcess(target=router_starter,
|
167
163
|
args=(comm_q, self.exception_q, self.priority_msgs, self.node_msgs, self.block_msgs, self.resource_msgs),
|
168
164
|
kwargs={"hub_address": self.hub_address,
|
169
|
-
"
|
170
|
-
"
|
165
|
+
"udp_port": self.hub_port,
|
166
|
+
"zmq_port_range": self.hub_port_range,
|
171
167
|
"logdir": self.logdir,
|
172
168
|
"logging_level": logging.DEBUG if self.monitoring_debug else logging.INFO,
|
173
169
|
"run_id": run_id
|
@@ -187,7 +183,7 @@ class MonitoringHub(RepresentationMixin):
|
|
187
183
|
daemon=True,
|
188
184
|
)
|
189
185
|
self.dbm_proc.start()
|
190
|
-
|
186
|
+
logger.info("Started the router process {} and DBM process {}".format(self.router_proc.pid, self.dbm_proc.pid))
|
191
187
|
|
192
188
|
self.filesystem_proc = Process(target=filesystem_receiver,
|
193
189
|
args=(self.logdir, self.resource_msgs, dfk_run_dir),
|
@@ -195,19 +191,19 @@ class MonitoringHub(RepresentationMixin):
|
|
195
191
|
daemon=True
|
196
192
|
)
|
197
193
|
self.filesystem_proc.start()
|
198
|
-
|
194
|
+
logger.info(f"Started filesystem radio receiver process {self.filesystem_proc.pid}")
|
199
195
|
|
200
196
|
try:
|
201
197
|
comm_q_result = comm_q.get(block=True, timeout=120)
|
202
198
|
except queue.Empty:
|
203
|
-
|
199
|
+
logger.error("Hub has not completed initialization in 120s. Aborting")
|
204
200
|
raise Exception("Hub failed to start")
|
205
201
|
|
206
202
|
if isinstance(comm_q_result, str):
|
207
|
-
|
203
|
+
logger.error(f"MonitoringRouter sent an error message: {comm_q_result}")
|
208
204
|
raise RuntimeError(f"MonitoringRouter failed to start: {comm_q_result}")
|
209
205
|
|
210
|
-
udp_port,
|
206
|
+
udp_port, zmq_port = comm_q_result
|
211
207
|
|
212
208
|
self.monitoring_hub_url = "udp://{}:{}".format(self.hub_address, udp_port)
|
213
209
|
|
@@ -217,28 +213,28 @@ class MonitoringHub(RepresentationMixin):
|
|
217
213
|
self._dfk_channel.setsockopt(zmq.LINGER, 0)
|
218
214
|
self._dfk_channel.set_hwm(0)
|
219
215
|
self._dfk_channel.setsockopt(zmq.SNDTIMEO, self.dfk_channel_timeout)
|
220
|
-
self._dfk_channel.connect("tcp://{}:{}".format(self.hub_address,
|
216
|
+
self._dfk_channel.connect("tcp://{}:{}".format(self.hub_address, zmq_port))
|
221
217
|
|
222
|
-
|
218
|
+
logger.info("Monitoring Hub initialized")
|
223
219
|
|
224
|
-
return
|
220
|
+
return zmq_port
|
225
221
|
|
226
222
|
# TODO: tighten the Any message format
|
227
223
|
def send(self, mtype: MessageType, message: Any) -> None:
|
228
|
-
|
224
|
+
logger.debug("Sending message type {}".format(mtype))
|
229
225
|
try:
|
230
226
|
self._dfk_channel.send_pyobj((mtype, message))
|
231
227
|
except zmq.Again:
|
232
|
-
|
228
|
+
logger.exception(
|
233
229
|
"The monitoring message sent from DFK to router timed-out after {}ms".format(self.dfk_channel_timeout))
|
234
230
|
|
235
231
|
def close(self) -> None:
|
236
|
-
|
232
|
+
logger.info("Terminating Monitoring Hub")
|
237
233
|
exception_msgs = []
|
238
234
|
while True:
|
239
235
|
try:
|
240
236
|
exception_msgs.append(self.exception_q.get(block=False))
|
241
|
-
|
237
|
+
logger.error("There was a queued exception (Either router or DBM process got exception much earlier?)")
|
242
238
|
except queue.Empty:
|
243
239
|
break
|
244
240
|
if self._dfk_channel and self.monitoring_hub_active:
|
@@ -246,7 +242,7 @@ class MonitoringHub(RepresentationMixin):
|
|
246
242
|
self._dfk_channel.close()
|
247
243
|
if exception_msgs:
|
248
244
|
for exception_msg in exception_msgs:
|
249
|
-
|
245
|
+
logger.error(
|
250
246
|
"{} process delivered an exception: {}. Terminating all monitoring processes immediately.".format(
|
251
247
|
exception_msg[0],
|
252
248
|
exception_msg[1]
|
@@ -255,21 +251,21 @@ class MonitoringHub(RepresentationMixin):
|
|
255
251
|
self.router_proc.terminate()
|
256
252
|
self.dbm_proc.terminate()
|
257
253
|
self.filesystem_proc.terminate()
|
258
|
-
|
254
|
+
logger.info("Waiting for router to terminate")
|
259
255
|
self.router_proc.join()
|
260
|
-
|
256
|
+
logger.debug("Finished waiting for router termination")
|
261
257
|
if len(exception_msgs) == 0:
|
262
|
-
|
258
|
+
logger.debug("Sending STOP to DBM")
|
263
259
|
self.priority_msgs.put(("STOP", 0))
|
264
260
|
else:
|
265
|
-
|
266
|
-
|
261
|
+
logger.debug("Not sending STOP to DBM, because there were DBM exceptions")
|
262
|
+
logger.debug("Waiting for DB termination")
|
267
263
|
self.dbm_proc.join()
|
268
|
-
|
264
|
+
logger.debug("Finished waiting for DBM termination")
|
269
265
|
|
270
266
|
# should this be message based? it probably doesn't need to be if
|
271
267
|
# we believe we've received all messages
|
272
|
-
|
268
|
+
logger.info("Terminating filesystem radio receiver process")
|
273
269
|
self.filesystem_proc.terminate()
|
274
270
|
self.filesystem_proc.join()
|
275
271
|
|
@@ -325,190 +321,3 @@ def filesystem_receiver(logdir: str, q: "queue.Queue[AddressedMonitoringMessage]
|
|
325
321
|
logger.exception(f"Exception processing {filename} - probably will be retried next iteration")
|
326
322
|
|
327
323
|
time.sleep(1) # whats a good time for this poll?
|
328
|
-
|
329
|
-
|
330
|
-
class MonitoringRouter:
|
331
|
-
|
332
|
-
def __init__(self,
|
333
|
-
*,
|
334
|
-
hub_address: str,
|
335
|
-
hub_port: Optional[int] = None,
|
336
|
-
hub_port_range: Tuple[int, int] = (55050, 56000),
|
337
|
-
|
338
|
-
monitoring_hub_address: str = "127.0.0.1",
|
339
|
-
logdir: str = ".",
|
340
|
-
run_id: str,
|
341
|
-
logging_level: int = logging.INFO,
|
342
|
-
atexit_timeout: int = 3 # in seconds
|
343
|
-
):
|
344
|
-
""" Initializes a monitoring configuration class.
|
345
|
-
|
346
|
-
Parameters
|
347
|
-
----------
|
348
|
-
hub_address : str
|
349
|
-
The ip address at which the workers will be able to reach the Hub.
|
350
|
-
hub_port : int
|
351
|
-
The specific port at which workers will be able to reach the Hub via UDP. Default: None
|
352
|
-
hub_port_range : tuple(int, int)
|
353
|
-
The MonitoringHub picks ports at random from the range which will be used by Hub.
|
354
|
-
This is overridden when the hub_port option is set. Default: (55050, 56000)
|
355
|
-
logdir : str
|
356
|
-
Parsl log directory paths. Logs and temp files go here. Default: '.'
|
357
|
-
logging_level : int
|
358
|
-
Logging level as defined in the logging module. Default: logging.INFO
|
359
|
-
atexit_timeout : float, optional
|
360
|
-
The amount of time in seconds to terminate the hub without receiving any messages, after the last dfk workflow message is received.
|
361
|
-
|
362
|
-
"""
|
363
|
-
os.makedirs(logdir, exist_ok=True)
|
364
|
-
self.logger = set_file_logger("{}/monitoring_router.log".format(logdir),
|
365
|
-
name="monitoring_router",
|
366
|
-
level=logging_level)
|
367
|
-
self.logger.debug("Monitoring router starting")
|
368
|
-
|
369
|
-
self.hub_address = hub_address
|
370
|
-
self.atexit_timeout = atexit_timeout
|
371
|
-
self.run_id = run_id
|
372
|
-
|
373
|
-
self.loop_freq = 10.0 # milliseconds
|
374
|
-
|
375
|
-
# Initialize the UDP socket
|
376
|
-
self.sock = socket.socket(socket.AF_INET,
|
377
|
-
socket.SOCK_DGRAM,
|
378
|
-
socket.IPPROTO_UDP)
|
379
|
-
|
380
|
-
# We are trying to bind to all interfaces with 0.0.0.0
|
381
|
-
if not hub_port:
|
382
|
-
self.sock.bind(('0.0.0.0', 0))
|
383
|
-
self.hub_port = self.sock.getsockname()[1]
|
384
|
-
else:
|
385
|
-
self.hub_port = hub_port
|
386
|
-
try:
|
387
|
-
self.sock.bind(('0.0.0.0', self.hub_port))
|
388
|
-
except Exception as e:
|
389
|
-
raise RuntimeError(f"Could not bind to hub_port {hub_port} because: {e}")
|
390
|
-
self.sock.settimeout(self.loop_freq / 1000)
|
391
|
-
self.logger.info("Initialized the UDP socket on 0.0.0.0:{}".format(self.hub_port))
|
392
|
-
|
393
|
-
self._context = zmq.Context()
|
394
|
-
self.ic_channel = self._context.socket(zmq.DEALER)
|
395
|
-
self.ic_channel.setsockopt(zmq.LINGER, 0)
|
396
|
-
self.ic_channel.set_hwm(0)
|
397
|
-
self.ic_channel.RCVTIMEO = int(self.loop_freq) # in milliseconds
|
398
|
-
self.logger.debug("hub_address: {}. hub_port_range {}".format(hub_address, hub_port_range))
|
399
|
-
self.ic_port = self.ic_channel.bind_to_random_port("tcp://*",
|
400
|
-
min_port=hub_port_range[0],
|
401
|
-
max_port=hub_port_range[1])
|
402
|
-
|
403
|
-
def start(self,
|
404
|
-
priority_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
405
|
-
node_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
406
|
-
block_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
407
|
-
resource_msgs: "queue.Queue[AddressedMonitoringMessage]") -> None:
|
408
|
-
try:
|
409
|
-
router_keep_going = True
|
410
|
-
while router_keep_going:
|
411
|
-
try:
|
412
|
-
data, addr = self.sock.recvfrom(2048)
|
413
|
-
resource_msg = pickle.loads(data)
|
414
|
-
self.logger.debug("Got UDP Message from {}: {}".format(addr, resource_msg))
|
415
|
-
resource_msgs.put((resource_msg, addr))
|
416
|
-
except socket.timeout:
|
417
|
-
pass
|
418
|
-
|
419
|
-
try:
|
420
|
-
dfk_loop_start = time.time()
|
421
|
-
while time.time() - dfk_loop_start < 1.0: # TODO make configurable
|
422
|
-
# note that nothing checks that msg really is of the annotated type
|
423
|
-
msg: TaggedMonitoringMessage
|
424
|
-
msg = self.ic_channel.recv_pyobj()
|
425
|
-
|
426
|
-
assert isinstance(msg, tuple), "IC Channel expects only tuples, got {}".format(msg)
|
427
|
-
assert len(msg) >= 1, "IC Channel expects tuples of length at least 1, got {}".format(msg)
|
428
|
-
assert len(msg) == 2, "IC Channel expects message tuples of exactly length 2, got {}".format(msg)
|
429
|
-
|
430
|
-
msg_0: AddressedMonitoringMessage
|
431
|
-
msg_0 = (msg, 0)
|
432
|
-
|
433
|
-
if msg[0] == MessageType.NODE_INFO:
|
434
|
-
msg[1]['run_id'] = self.run_id
|
435
|
-
node_msgs.put(msg_0)
|
436
|
-
elif msg[0] == MessageType.RESOURCE_INFO:
|
437
|
-
resource_msgs.put(msg_0)
|
438
|
-
elif msg[0] == MessageType.BLOCK_INFO:
|
439
|
-
block_msgs.put(msg_0)
|
440
|
-
elif msg[0] == MessageType.TASK_INFO:
|
441
|
-
priority_msgs.put(msg_0)
|
442
|
-
elif msg[0] == MessageType.WORKFLOW_INFO:
|
443
|
-
priority_msgs.put(msg_0)
|
444
|
-
if 'exit_now' in msg[1] and msg[1]['exit_now']:
|
445
|
-
router_keep_going = False
|
446
|
-
else:
|
447
|
-
# There is a type: ignore here because if msg[0]
|
448
|
-
# is of the correct type, this code is unreachable,
|
449
|
-
# but there is no verification that the message
|
450
|
-
# received from ic_channel.recv_pyobj() is actually
|
451
|
-
# of that type.
|
452
|
-
self.logger.error("Discarding message " # type: ignore[unreachable]
|
453
|
-
f"from interchange with unknown type {msg[0].value}")
|
454
|
-
except zmq.Again:
|
455
|
-
pass
|
456
|
-
except Exception:
|
457
|
-
# This will catch malformed messages. What happens if the
|
458
|
-
# channel is broken in such a way that it always raises
|
459
|
-
# an exception? Looping on this would maybe be the wrong
|
460
|
-
# thing to do.
|
461
|
-
self.logger.warning("Failure processing a ZMQ message", exc_info=True)
|
462
|
-
|
463
|
-
self.logger.info("Monitoring router draining")
|
464
|
-
last_msg_received_time = time.time()
|
465
|
-
while time.time() - last_msg_received_time < self.atexit_timeout:
|
466
|
-
try:
|
467
|
-
data, addr = self.sock.recvfrom(2048)
|
468
|
-
msg = pickle.loads(data)
|
469
|
-
self.logger.debug("Got UDP Message from {}: {}".format(addr, msg))
|
470
|
-
resource_msgs.put((msg, addr))
|
471
|
-
last_msg_received_time = time.time()
|
472
|
-
except socket.timeout:
|
473
|
-
pass
|
474
|
-
|
475
|
-
self.logger.info("Monitoring router finishing normally")
|
476
|
-
finally:
|
477
|
-
self.logger.info("Monitoring router finished")
|
478
|
-
|
479
|
-
|
480
|
-
@wrap_with_logs
|
481
|
-
def router_starter(comm_q: "queue.Queue[Union[Tuple[int, int], str]]",
|
482
|
-
exception_q: "queue.Queue[Tuple[str, str]]",
|
483
|
-
priority_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
484
|
-
node_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
485
|
-
block_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
486
|
-
resource_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
487
|
-
|
488
|
-
hub_address: str,
|
489
|
-
hub_port: Optional[int],
|
490
|
-
hub_port_range: Tuple[int, int],
|
491
|
-
|
492
|
-
logdir: str,
|
493
|
-
logging_level: int,
|
494
|
-
run_id: str) -> None:
|
495
|
-
setproctitle("parsl: monitoring router")
|
496
|
-
try:
|
497
|
-
router = MonitoringRouter(hub_address=hub_address,
|
498
|
-
hub_port=hub_port,
|
499
|
-
hub_port_range=hub_port_range,
|
500
|
-
logdir=logdir,
|
501
|
-
logging_level=logging_level,
|
502
|
-
run_id=run_id)
|
503
|
-
except Exception as e:
|
504
|
-
logger.error("MonitoringRouter construction failed.", exc_info=True)
|
505
|
-
comm_q.put(f"Monitoring router construction failed: {e}")
|
506
|
-
else:
|
507
|
-
comm_q.put((router.hub_port, router.ic_port))
|
508
|
-
|
509
|
-
router.logger.info("Starting MonitoringRouter in router_starter")
|
510
|
-
try:
|
511
|
-
router.start(priority_msgs, node_msgs, block_msgs, resource_msgs)
|
512
|
-
except Exception as e:
|
513
|
-
router.logger.exception("router.start exception")
|
514
|
-
exception_q.put(('Hub', str(e)))
|
@@ -0,0 +1,208 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import os
|
4
|
+
import socket
|
5
|
+
import time
|
6
|
+
import pickle
|
7
|
+
import logging
|
8
|
+
import zmq
|
9
|
+
|
10
|
+
import queue
|
11
|
+
|
12
|
+
from parsl.log_utils import set_file_logger
|
13
|
+
from parsl.process_loggers import wrap_with_logs
|
14
|
+
from parsl.utils import setproctitle
|
15
|
+
|
16
|
+
from parsl.monitoring.message_type import MessageType
|
17
|
+
from parsl.monitoring.types import AddressedMonitoringMessage, TaggedMonitoringMessage
|
18
|
+
from typing import Optional, Tuple, Union
|
19
|
+
|
20
|
+
|
21
|
+
logger = logging.getLogger(__name__)
|
22
|
+
|
23
|
+
|
24
|
+
class MonitoringRouter:
|
25
|
+
|
26
|
+
def __init__(self,
|
27
|
+
*,
|
28
|
+
hub_address: str,
|
29
|
+
udp_port: Optional[int] = None,
|
30
|
+
zmq_port_range: Tuple[int, int] = (55050, 56000),
|
31
|
+
|
32
|
+
monitoring_hub_address: str = "127.0.0.1",
|
33
|
+
logdir: str = ".",
|
34
|
+
run_id: str,
|
35
|
+
logging_level: int = logging.INFO,
|
36
|
+
atexit_timeout: int = 3 # in seconds
|
37
|
+
):
|
38
|
+
""" Initializes a monitoring configuration class.
|
39
|
+
|
40
|
+
Parameters
|
41
|
+
----------
|
42
|
+
hub_address : str
|
43
|
+
The ip address at which the workers will be able to reach the Hub.
|
44
|
+
udp_port : int
|
45
|
+
The specific port at which workers will be able to reach the Hub via UDP. Default: None
|
46
|
+
zmq_port_range : tuple(int, int)
|
47
|
+
The MonitoringHub picks ports at random from the range which will be used by Hub.
|
48
|
+
Default: (55050, 56000)
|
49
|
+
logdir : str
|
50
|
+
Parsl log directory paths. Logs and temp files go here. Default: '.'
|
51
|
+
logging_level : int
|
52
|
+
Logging level as defined in the logging module. Default: logging.INFO
|
53
|
+
atexit_timeout : float, optional
|
54
|
+
The amount of time in seconds to terminate the hub without receiving any messages, after the last dfk workflow message is received.
|
55
|
+
|
56
|
+
"""
|
57
|
+
os.makedirs(logdir, exist_ok=True)
|
58
|
+
self.logger = set_file_logger("{}/monitoring_router.log".format(logdir),
|
59
|
+
name="monitoring_router",
|
60
|
+
level=logging_level)
|
61
|
+
self.logger.debug("Monitoring router starting")
|
62
|
+
|
63
|
+
self.hub_address = hub_address
|
64
|
+
self.atexit_timeout = atexit_timeout
|
65
|
+
self.run_id = run_id
|
66
|
+
|
67
|
+
self.loop_freq = 10.0 # milliseconds
|
68
|
+
|
69
|
+
# Initialize the UDP socket
|
70
|
+
self.udp_sock = socket.socket(socket.AF_INET,
|
71
|
+
socket.SOCK_DGRAM,
|
72
|
+
socket.IPPROTO_UDP)
|
73
|
+
|
74
|
+
# We are trying to bind to all interfaces with 0.0.0.0
|
75
|
+
if not udp_port:
|
76
|
+
self.udp_sock.bind(('0.0.0.0', 0))
|
77
|
+
self.udp_port = self.udp_sock.getsockname()[1]
|
78
|
+
else:
|
79
|
+
self.udp_port = udp_port
|
80
|
+
try:
|
81
|
+
self.udp_sock.bind(('0.0.0.0', self.udp_port))
|
82
|
+
except Exception as e:
|
83
|
+
raise RuntimeError(f"Could not bind to udp_port {udp_port} because: {e}")
|
84
|
+
self.udp_sock.settimeout(self.loop_freq / 1000)
|
85
|
+
self.logger.info("Initialized the UDP socket on 0.0.0.0:{}".format(self.udp_port))
|
86
|
+
|
87
|
+
self._context = zmq.Context()
|
88
|
+
self.zmq_receiver_channel = self._context.socket(zmq.DEALER)
|
89
|
+
self.zmq_receiver_channel.setsockopt(zmq.LINGER, 0)
|
90
|
+
self.zmq_receiver_channel.set_hwm(0)
|
91
|
+
self.zmq_receiver_channel.RCVTIMEO = int(self.loop_freq) # in milliseconds
|
92
|
+
self.logger.debug("hub_address: {}. zmq_port_range {}".format(hub_address, zmq_port_range))
|
93
|
+
self.zmq_receiver_port = self.zmq_receiver_channel.bind_to_random_port("tcp://*",
|
94
|
+
min_port=zmq_port_range[0],
|
95
|
+
max_port=zmq_port_range[1])
|
96
|
+
|
97
|
+
def start(self,
|
98
|
+
priority_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
99
|
+
node_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
100
|
+
block_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
101
|
+
resource_msgs: "queue.Queue[AddressedMonitoringMessage]") -> None:
|
102
|
+
try:
|
103
|
+
router_keep_going = True
|
104
|
+
while router_keep_going:
|
105
|
+
try:
|
106
|
+
data, addr = self.udp_sock.recvfrom(2048)
|
107
|
+
resource_msg = pickle.loads(data)
|
108
|
+
self.logger.debug("Got UDP Message from {}: {}".format(addr, resource_msg))
|
109
|
+
resource_msgs.put((resource_msg, addr))
|
110
|
+
except socket.timeout:
|
111
|
+
pass
|
112
|
+
|
113
|
+
try:
|
114
|
+
dfk_loop_start = time.time()
|
115
|
+
while time.time() - dfk_loop_start < 1.0: # TODO make configurable
|
116
|
+
# note that nothing checks that msg really is of the annotated type
|
117
|
+
msg: TaggedMonitoringMessage
|
118
|
+
msg = self.zmq_receiver_channel.recv_pyobj()
|
119
|
+
|
120
|
+
assert isinstance(msg, tuple), "ZMQ Receiver expects only tuples, got {}".format(msg)
|
121
|
+
assert len(msg) >= 1, "ZMQ Receiver expects tuples of length at least 1, got {}".format(msg)
|
122
|
+
assert len(msg) == 2, "ZMQ Receiver expects message tuples of exactly length 2, got {}".format(msg)
|
123
|
+
|
124
|
+
msg_0: AddressedMonitoringMessage
|
125
|
+
msg_0 = (msg, 0)
|
126
|
+
|
127
|
+
if msg[0] == MessageType.NODE_INFO:
|
128
|
+
msg[1]['run_id'] = self.run_id
|
129
|
+
node_msgs.put(msg_0)
|
130
|
+
elif msg[0] == MessageType.RESOURCE_INFO:
|
131
|
+
resource_msgs.put(msg_0)
|
132
|
+
elif msg[0] == MessageType.BLOCK_INFO:
|
133
|
+
block_msgs.put(msg_0)
|
134
|
+
elif msg[0] == MessageType.TASK_INFO:
|
135
|
+
priority_msgs.put(msg_0)
|
136
|
+
elif msg[0] == MessageType.WORKFLOW_INFO:
|
137
|
+
priority_msgs.put(msg_0)
|
138
|
+
if 'exit_now' in msg[1] and msg[1]['exit_now']:
|
139
|
+
router_keep_going = False
|
140
|
+
else:
|
141
|
+
# There is a type: ignore here because if msg[0]
|
142
|
+
# is of the correct type, this code is unreachable,
|
143
|
+
# but there is no verification that the message
|
144
|
+
# received from zmq_receiver_channel.recv_pyobj() is actually
|
145
|
+
# of that type.
|
146
|
+
self.logger.error("Discarding message " # type: ignore[unreachable]
|
147
|
+
f"from interchange with unknown type {msg[0].value}")
|
148
|
+
except zmq.Again:
|
149
|
+
pass
|
150
|
+
except Exception:
|
151
|
+
# This will catch malformed messages. What happens if the
|
152
|
+
# channel is broken in such a way that it always raises
|
153
|
+
# an exception? Looping on this would maybe be the wrong
|
154
|
+
# thing to do.
|
155
|
+
self.logger.warning("Failure processing a ZMQ message", exc_info=True)
|
156
|
+
|
157
|
+
self.logger.info("Monitoring router draining")
|
158
|
+
last_msg_received_time = time.time()
|
159
|
+
while time.time() - last_msg_received_time < self.atexit_timeout:
|
160
|
+
try:
|
161
|
+
data, addr = self.udp_sock.recvfrom(2048)
|
162
|
+
msg = pickle.loads(data)
|
163
|
+
self.logger.debug("Got UDP Message from {}: {}".format(addr, msg))
|
164
|
+
resource_msgs.put((msg, addr))
|
165
|
+
last_msg_received_time = time.time()
|
166
|
+
except socket.timeout:
|
167
|
+
pass
|
168
|
+
|
169
|
+
self.logger.info("Monitoring router finishing normally")
|
170
|
+
finally:
|
171
|
+
self.logger.info("Monitoring router finished")
|
172
|
+
|
173
|
+
|
174
|
+
@wrap_with_logs
|
175
|
+
def router_starter(comm_q: "queue.Queue[Union[Tuple[int, int], str]]",
|
176
|
+
exception_q: "queue.Queue[Tuple[str, str]]",
|
177
|
+
priority_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
178
|
+
node_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
179
|
+
block_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
180
|
+
resource_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
181
|
+
|
182
|
+
hub_address: str,
|
183
|
+
udp_port: Optional[int],
|
184
|
+
zmq_port_range: Tuple[int, int],
|
185
|
+
|
186
|
+
logdir: str,
|
187
|
+
logging_level: int,
|
188
|
+
run_id: str) -> None:
|
189
|
+
setproctitle("parsl: monitoring router")
|
190
|
+
try:
|
191
|
+
router = MonitoringRouter(hub_address=hub_address,
|
192
|
+
udp_port=udp_port,
|
193
|
+
zmq_port_range=zmq_port_range,
|
194
|
+
logdir=logdir,
|
195
|
+
logging_level=logging_level,
|
196
|
+
run_id=run_id)
|
197
|
+
except Exception as e:
|
198
|
+
logger.error("MonitoringRouter construction failed.", exc_info=True)
|
199
|
+
comm_q.put(f"Monitoring router construction failed: {e}")
|
200
|
+
else:
|
201
|
+
comm_q.put((router.udp_port, router.zmq_receiver_port))
|
202
|
+
|
203
|
+
router.logger.info("Starting MonitoringRouter in router_starter")
|
204
|
+
try:
|
205
|
+
router.start(priority_msgs, node_msgs, block_msgs, resource_msgs)
|
206
|
+
except Exception as e:
|
207
|
+
router.logger.exception("router.start exception")
|
208
|
+
exception_q.put(('Hub', str(e)))
|
@@ -41,11 +41,11 @@ def test_row_counts():
|
|
41
41
|
|
42
42
|
# dig out the interchange port...
|
43
43
|
hub_address = parsl.dfk().hub_address
|
44
|
-
|
44
|
+
hub_zmq_port = parsl.dfk().hub_zmq_port
|
45
45
|
|
46
46
|
# this will send a string to a new socket connection
|
47
47
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
48
|
-
s.connect((hub_address,
|
48
|
+
s.connect((hub_address, hub_zmq_port))
|
49
49
|
s.sendall(b'fuzzing\r')
|
50
50
|
|
51
51
|
# this will send a non-object down the DFK's existing ZMQ connection
|
@@ -0,0 +1,82 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
import parsl
|
4
|
+
import pytest
|
5
|
+
import time
|
6
|
+
|
7
|
+
from parsl.providers import LocalProvider
|
8
|
+
from parsl.channels import LocalChannel
|
9
|
+
from parsl.launchers import SimpleLauncher
|
10
|
+
|
11
|
+
from parsl.config import Config
|
12
|
+
from parsl.executors import HighThroughputExecutor
|
13
|
+
from parsl.monitoring import MonitoringHub
|
14
|
+
|
15
|
+
|
16
|
+
def fresh_config(run_dir, strategy, db_url):
|
17
|
+
return Config(
|
18
|
+
run_dir=os.fspath(run_dir),
|
19
|
+
executors=[
|
20
|
+
HighThroughputExecutor(
|
21
|
+
label="htex_local",
|
22
|
+
cores_per_worker=1,
|
23
|
+
encrypted=True,
|
24
|
+
provider=LocalProvider(
|
25
|
+
channel=LocalChannel(),
|
26
|
+
init_blocks=1,
|
27
|
+
# min and max are set to 0 to ensure that we don't get
|
28
|
+
# a block from ongoing strategy scaling, only from
|
29
|
+
# init_blocks
|
30
|
+
min_blocks=0,
|
31
|
+
max_blocks=0,
|
32
|
+
launcher=SimpleLauncher(),
|
33
|
+
),
|
34
|
+
)
|
35
|
+
],
|
36
|
+
strategy=strategy,
|
37
|
+
strategy_period=0.1,
|
38
|
+
monitoring=MonitoringHub(
|
39
|
+
hub_address="localhost",
|
40
|
+
hub_port=55055,
|
41
|
+
logging_endpoint=db_url
|
42
|
+
)
|
43
|
+
)
|
44
|
+
|
45
|
+
|
46
|
+
@parsl.python_app
|
47
|
+
def this_app():
|
48
|
+
pass
|
49
|
+
|
50
|
+
|
51
|
+
@pytest.mark.local
|
52
|
+
@pytest.mark.parametrize("strategy", ('none', 'simple', 'htex_auto_scale'))
|
53
|
+
def test_row_counts(tmpd_cwd, strategy):
|
54
|
+
# this is imported here rather than at module level because
|
55
|
+
# it isn't available in a plain parsl install, so this module
|
56
|
+
# would otherwise fail to import and break even a basic test
|
57
|
+
# run.
|
58
|
+
import sqlalchemy
|
59
|
+
from sqlalchemy import text
|
60
|
+
|
61
|
+
db_url = f"sqlite:///{tmpd_cwd}/monitoring.db"
|
62
|
+
parsl.load(fresh_config(tmpd_cwd, strategy, db_url))
|
63
|
+
|
64
|
+
this_app().result()
|
65
|
+
|
66
|
+
parsl.dfk().cleanup()
|
67
|
+
parsl.clear()
|
68
|
+
|
69
|
+
engine = sqlalchemy.create_engine(db_url)
|
70
|
+
with engine.begin() as connection:
|
71
|
+
|
72
|
+
result = connection.execute(text("SELECT COUNT(DISTINCT block_id) FROM block"))
|
73
|
+
(c, ) = result.first()
|
74
|
+
assert c == 1, "We should see a single block in this database"
|
75
|
+
|
76
|
+
result = connection.execute(text("SELECT COUNT(*) FROM block WHERE block_id = 0 AND status = 'PENDING'"))
|
77
|
+
(c, ) = result.first()
|
78
|
+
assert c == 1, "There should be a single pending status"
|
79
|
+
|
80
|
+
result = connection.execute(text("SELECT COUNT(*) FROM block WHERE block_id = 0 AND status = 'CANCELLED'"))
|
81
|
+
(c, ) = result.first()
|
82
|
+
assert c == 1, "There should be a single cancelled status"
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import parsl
|
2
|
+
from parsl.tests.configs.local_threads import fresh_config
|
3
|
+
import pytest
|
4
|
+
from parsl.errors import NoDataFlowKernelError
|
5
|
+
|
6
|
+
|
7
|
+
@parsl.python_app
|
8
|
+
def square(x):
|
9
|
+
return x * x
|
10
|
+
|
11
|
+
|
12
|
+
@parsl.bash_app
|
13
|
+
def foo(x, stdout='foo.stdout'):
|
14
|
+
return f"echo {x + 1}"
|
15
|
+
|
16
|
+
|
17
|
+
def local_setup():
|
18
|
+
pass
|
19
|
+
|
20
|
+
|
21
|
+
def local_teardown():
|
22
|
+
parsl.clear()
|
23
|
+
|
24
|
+
|
25
|
+
@pytest.mark.local
|
26
|
+
def test_within_context_manger():
|
27
|
+
config = fresh_config()
|
28
|
+
with parsl.load(config=config):
|
29
|
+
py_future = square(2)
|
30
|
+
assert py_future.result() == 4
|
31
|
+
|
32
|
+
bash_future = foo(1)
|
33
|
+
assert bash_future.result() == 0
|
34
|
+
|
35
|
+
with open('foo.stdout', 'r') as f:
|
36
|
+
assert f.read() == "2\n"
|
37
|
+
|
38
|
+
with pytest.raises(NoDataFlowKernelError) as excinfo:
|
39
|
+
square(2).result()
|
40
|
+
assert str(excinfo.value) == "Cannot submit to a DFK that has been cleaned up"
|
File without changes
|
@@ -0,0 +1,65 @@
|
|
1
|
+
import os
|
2
|
+
import parsl
|
3
|
+
import pytest
|
4
|
+
import signal
|
5
|
+
import time
|
6
|
+
|
7
|
+
from parsl.tests.configs.htex_local_alternate import fresh_config
|
8
|
+
|
9
|
+
# This is a very generous upper bound on expected shutdown time of target
|
10
|
+
# process after receiving a signal, measured in seconds.
|
11
|
+
PERMITTED_SHUTDOWN_TIME_S = 60
|
12
|
+
|
13
|
+
|
14
|
+
@parsl.python_app
|
15
|
+
def simple_app():
|
16
|
+
return True
|
17
|
+
|
18
|
+
|
19
|
+
@pytest.mark.local
|
20
|
+
def test_no_kills():
|
21
|
+
"""This tests that we can create a monitoring-enabled DFK and shut it down."""
|
22
|
+
|
23
|
+
parsl.load(fresh_config())
|
24
|
+
|
25
|
+
assert parsl.dfk().monitoring is not None, "This test requires monitoring"
|
26
|
+
|
27
|
+
parsl.dfk().cleanup()
|
28
|
+
parsl.clear()
|
29
|
+
|
30
|
+
|
31
|
+
@pytest.mark.local
|
32
|
+
@pytest.mark.parametrize("sig", [signal.SIGINT, signal.SIGTERM, signal.SIGKILL, signal.SIGQUIT])
|
33
|
+
@pytest.mark.parametrize("process_attr", ["router_proc", "dbm_proc"])
|
34
|
+
def test_kill_monitoring_helper_process(sig, process_attr, try_assert):
|
35
|
+
"""This tests that we can kill a monitoring process and still have successful shutdown.
|
36
|
+
SIGINT emulates some racy behaviour when ctrl-C is pressed: that
|
37
|
+
monitoring processes receive a ctrl-C too, and so the other processes
|
38
|
+
need to be tolerant to monitoring processes arbitrarily exiting.
|
39
|
+
"""
|
40
|
+
|
41
|
+
parsl.load(fresh_config())
|
42
|
+
|
43
|
+
dfk = parsl.dfk()
|
44
|
+
|
45
|
+
assert dfk.monitoring is not None, "Monitoring required"
|
46
|
+
|
47
|
+
target_proc = getattr(dfk.monitoring, process_attr)
|
48
|
+
|
49
|
+
assert target_proc is not None, "prereq: target process must exist"
|
50
|
+
assert target_proc.is_alive(), "prereq: target process must be alive"
|
51
|
+
|
52
|
+
target_pid = target_proc.pid
|
53
|
+
assert target_pid is not None, "prereq: target process must have a pid"
|
54
|
+
|
55
|
+
os.kill(target_pid, sig)
|
56
|
+
|
57
|
+
try_assert(lambda: not target_proc.is_alive(), timeout_ms=PERMITTED_SHUTDOWN_TIME_S * 1000)
|
58
|
+
|
59
|
+
# now we have broken one piece of the monitoring system, do some app
|
60
|
+
# execution and then shut down.
|
61
|
+
|
62
|
+
simple_app().result()
|
63
|
+
|
64
|
+
parsl.dfk().cleanup()
|
65
|
+
parsl.clear()
|
parsl/version.py
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: parsl
|
3
|
-
Version: 2024.3.
|
3
|
+
Version: 2024.3.25
|
4
4
|
Summary: Simple data dependent workflows in Python
|
5
5
|
Home-page: https://github.com/Parsl/parsl
|
6
|
-
Download-URL: https://github.com/Parsl/parsl/archive/2024.03.
|
6
|
+
Download-URL: https://github.com/Parsl/parsl/archive/2024.03.25.tar.gz
|
7
7
|
Author: The Parsl Team
|
8
8
|
Author-email: parsl@googlegroups.com
|
9
9
|
License: Apache 2.0
|
@@ -54,7 +54,7 @@ Requires-Dist: pyyaml ; extra == 'all'
|
|
54
54
|
Requires-Dist: cffi ; extra == 'all'
|
55
55
|
Requires-Dist: jsonschema ; extra == 'all'
|
56
56
|
Requires-Dist: proxystore ; extra == 'all'
|
57
|
-
Requires-Dist: radical.pilot ; extra == 'all'
|
57
|
+
Requires-Dist: radical.pilot ==1.47 ; extra == 'all'
|
58
58
|
Provides-Extra: aws
|
59
59
|
Requires-Dist: boto3 ; extra == 'aws'
|
60
60
|
Provides-Extra: azure
|
@@ -83,7 +83,7 @@ Requires-Dist: oauth-ssh >=0.9 ; extra == 'oauth_ssh'
|
|
83
83
|
Provides-Extra: proxystore
|
84
84
|
Requires-Dist: proxystore ; extra == 'proxystore'
|
85
85
|
Provides-Extra: radical-pilot
|
86
|
-
Requires-Dist: radical.pilot ; extra == 'radical-pilot'
|
86
|
+
Requires-Dist: radical.pilot ==1.47 ; extra == 'radical-pilot'
|
87
87
|
Provides-Extra: visualization
|
88
88
|
Requires-Dist: pydot ; extra == 'visualization'
|
89
89
|
Requires-Dist: networkx <2.6,>=2.5 ; extra == 'visualization'
|
@@ -8,7 +8,7 @@ parsl/multiprocessing.py,sha256=hakfdg-sgxEjwloZeDrt6EhzwdzecvjJhkPHHxh8lII,1938
|
|
8
8
|
parsl/process_loggers.py,sha256=1G3Rfrh5wuZNo2X03grG4kTYPGOxz7hHCyG6L_A3b0A,1137
|
9
9
|
parsl/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
10
|
parsl/utils.py,sha256=A3WDMGaNB4ajVx_jCuc-74W6PFy4zswJy-pLE7u8Dz0,10979
|
11
|
-
parsl/version.py,sha256=
|
11
|
+
parsl/version.py,sha256=xw1wZ0QmQ9UaGCS5xiCsTOYpLaH5Ht4qp-xYq_4FbaE,131
|
12
12
|
parsl/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
13
|
parsl/app/app.py,sha256=wAHchJetgnicT1pn0NJKDeDX0lV3vDFlG8cQd_Ciax4,8522
|
14
14
|
parsl/app/bash.py,sha256=bx9x1XFwkOTpZZD3CPwnVL9SyNRDjbUGtOnuGLvxN_8,5396
|
@@ -60,7 +60,7 @@ parsl/data_provider/http.py,sha256=nDHTW7XmJqAukWJjPRQjyhUXt8r6GsQ36mX9mv_wOig,2
|
|
60
60
|
parsl/data_provider/rsync.py,sha256=2-ZxqrT-hBj39x082NusJaBqsGW4Jd2qCW6JkVPpEl0,4254
|
61
61
|
parsl/data_provider/staging.py,sha256=l-mAXFburs3BWPjkSmiQKuAgJpsxCG62yATPDbrafYI,4523
|
62
62
|
parsl/dataflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
63
|
-
parsl/dataflow/dflow.py,sha256=
|
63
|
+
parsl/dataflow/dflow.py,sha256=38bZ0cFnCKqlbyf-XYxyA8PscsypQNOzd_8bIUWT0wM,64461
|
64
64
|
parsl/dataflow/errors.py,sha256=w2vOt_ymzG2dOqJUO4IDcmTlrCIHlMZL8nBVyVq0O_8,2176
|
65
65
|
parsl/dataflow/futures.py,sha256=aVfEUTzp4-EdunDAtNcqVQf8l_A7ArDi2c82KZMwxfY,5256
|
66
66
|
parsl/dataflow/memoization.py,sha256=AsJO6c6cRp2ac6H8uGn2USlEi78_nX3QWvpxYt4XdYE,9583
|
@@ -68,9 +68,9 @@ parsl/dataflow/rundirs.py,sha256=XKmBZpBEIsGACBhYOkbbs2e5edC0pQegJcSlk4FWeag,115
|
|
68
68
|
parsl/dataflow/states.py,sha256=hV6mfv-y4A6xrujeQglcomnfEs7y3Xm2g6JFwC6dvgQ,2612
|
69
69
|
parsl/dataflow/taskrecord.py,sha256=bzIBmlDTsRrELtB9PUQwxTWcwrCd8aMsUAzvijle1eo,3114
|
70
70
|
parsl/executors/__init__.py,sha256=J50N97Nm9YRjz6K0oNXDxUYIsDjL43_tp3LVb2w7n-M,381
|
71
|
-
parsl/executors/base.py,sha256=
|
71
|
+
parsl/executors/base.py,sha256=CNWddQ7eP_Kqd2THv4bj5Dg1Jgb3dbI3z3aznsRP6dc,4574
|
72
72
|
parsl/executors/errors.py,sha256=xVswxgi7vmJcUMCeYDAPK8sQT2kHFFROVoOr0dnmcWE,2098
|
73
|
-
parsl/executors/status_handling.py,sha256=
|
73
|
+
parsl/executors/status_handling.py,sha256=DP7Wu2BjoMPcJ8bdYmwALHvC5Fd1y8-g9sLj_cKoGOQ,10574
|
74
74
|
parsl/executors/threads.py,sha256=bMU3JFghm17Lpcua13pr3NgQhkUDDc2mqvF2yJBrVNQ,3353
|
75
75
|
parsl/executors/flux/__init__.py,sha256=P9grTTeRPXfqXurFhlSS7XhmE6tTbnCnyQ1f9b-oYHE,136
|
76
76
|
parsl/executors/flux/execute_parsl_task.py,sha256=yUG_WjZLcX8LrgPl26mpEBWZhQMlVNbRLGu08yIjdf4,1553
|
@@ -95,7 +95,7 @@ parsl/executors/radical/rpex_worker.py,sha256=1M1df-hzFdmZMWbRZlUzIX7uAWMKJ_SkxL
|
|
95
95
|
parsl/executors/taskvine/__init__.py,sha256=sWIJdvSLgQKul9dlSjIkNat7yBDgU3SrBF3X2yhT86E,293
|
96
96
|
parsl/executors/taskvine/errors.py,sha256=MNS_NjpvHjwevQXOjqjSEBFroqEWi-LT1ZEVZ2C5Dx0,652
|
97
97
|
parsl/executors/taskvine/exec_parsl_function.py,sha256=oUAKbPWwpbzWwQ47bZQlVDxS8txhnhPsonMf3AOEMGQ,7085
|
98
|
-
parsl/executors/taskvine/executor.py,sha256=
|
98
|
+
parsl/executors/taskvine/executor.py,sha256=YAPnZZV31R_H1A4mILNIiDQVVvzO6G1wUo0HIjmcw7g,32264
|
99
99
|
parsl/executors/taskvine/factory.py,sha256=sHhfGv7xRFrWkQclzRXuFEAHuSXhsZu2lR5LJ81aucA,2638
|
100
100
|
parsl/executors/taskvine/factory_config.py,sha256=AbE2fN2snrF5ITYrrS4DnGn2XkJHUFr_17DYHDHIwq0,3693
|
101
101
|
parsl/executors/taskvine/manager.py,sha256=VxVN2L5zFVPNfSAJrGgq87MRJKpcxf-BHdO5QWxB4TU,25822
|
@@ -104,13 +104,13 @@ parsl/executors/taskvine/utils.py,sha256=iSrIogeiauL3UNy_9tiZp1cBSNn6fIJkMYQRVi1
|
|
104
104
|
parsl/executors/workqueue/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
105
105
|
parsl/executors/workqueue/errors.py,sha256=ghB93Ptb_QbOAvgLe7siV_snRRkU_T-cFHv3AR6Ziwo,541
|
106
106
|
parsl/executors/workqueue/exec_parsl_function.py,sha256=NtWNeBvRqksej38eRPw8zPBJ1CeW6vgaitve0tfz_qc,7801
|
107
|
-
parsl/executors/workqueue/executor.py,sha256=
|
107
|
+
parsl/executors/workqueue/executor.py,sha256=QnHNdj7aVVYOzK-jmo0YqKMqW2__XmmruHHilqGUVy0,49823
|
108
108
|
parsl/executors/workqueue/parsl_coprocess.py,sha256=kEFGC-A97c_gweUPvrc9EEGume7vUpkJLJlyAb87xtQ,5737
|
109
109
|
parsl/executors/workqueue/parsl_coprocess_stub.py,sha256=_bJmpPIgL42qM6bVzeEKt1Mn1trSP41rtJguXxPGfHI,735
|
110
110
|
parsl/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
111
|
-
parsl/jobs/error_handlers.py,sha256=
|
111
|
+
parsl/jobs/error_handlers.py,sha256=WcWZUA7KyE1ocX5zrMf_EwqOob8Jb7uHMjD3nlb_BUo,2319
|
112
112
|
parsl/jobs/errors.py,sha256=cpSQXCrlKtuHsQf7usjF-lX8XsDkFnE5kWpmFjiN6OU,178
|
113
|
-
parsl/jobs/job_status_poller.py,sha256=
|
113
|
+
parsl/jobs/job_status_poller.py,sha256=hLFyT9tnvYrIev72bg0jZUhabYR9GJ0o15M5l9TR3Qo,5422
|
114
114
|
parsl/jobs/states.py,sha256=rPBoAEEudKngWFijlwvXXhAagDs_9DCXvQP9rwzVgCM,4855
|
115
115
|
parsl/jobs/strategy.py,sha256=a-W3vxEHHCfe521LMfSoZLpJjdTtwCfTgdn1ChxzUuI,12959
|
116
116
|
parsl/launchers/__init__.py,sha256=k8zAB3IBP-brfqXUptKwGkvsIRaXjAJZNBJa2XVtY1A,546
|
@@ -120,9 +120,10 @@ parsl/launchers/launchers.py,sha256=VB--fiVv_IQne3DydTMSdGUY0o0g69puAs-Hd3mJ2vo,
|
|
120
120
|
parsl/monitoring/__init__.py,sha256=0ywNz6i0lM1xo_7_BIxhETDGeVd2C_0wwD7qgeaMR4c,83
|
121
121
|
parsl/monitoring/db_manager.py,sha256=hdmmXSTXp8Wwhr7vLpQalD_ahRl3SNxKYVsplnThRk8,37021
|
122
122
|
parsl/monitoring/message_type.py,sha256=Khn88afNxcOIciKiCK4GLnn90I5BlRTiOL3zK-P07yQ,401
|
123
|
-
parsl/monitoring/monitoring.py,sha256=
|
123
|
+
parsl/monitoring/monitoring.py,sha256=ZkwbIKGG7Zx8Nxj8TEaTT_FrmjwJvmv1jNlEmaUZYAM,14313
|
124
124
|
parsl/monitoring/radios.py,sha256=T2_6QuUjC-dd_7qMnIk6WHQead1iWz7m_P6ZC4QAqdA,5265
|
125
125
|
parsl/monitoring/remote.py,sha256=OcIgudujtPO_DsY-YV36x92skeiNdGt-6aEOqaCU8T0,13900
|
126
|
+
parsl/monitoring/router.py,sha256=Y_PJjffS23HwfTJClhg5W4gUXnkAI_3crjjZMoyzxVA,9592
|
126
127
|
parsl/monitoring/types.py,sha256=SO6Fjjbb83sv_MtbutoxGssiWh6oXKkEEsD4EvwOnZ4,629
|
127
128
|
parsl/monitoring/queries/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
128
129
|
parsl/monitoring/queries/pandas.py,sha256=J09lIxSvVtAfBTbFcwKgUkHwZfaS6vYOzXHxS7UBLoc,2233
|
@@ -351,7 +352,8 @@ parsl/tests/test_monitoring/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
|
|
351
352
|
parsl/tests/test_monitoring/test_app_names.py,sha256=4Ziggxv0JLP0UGAd5jjXdivUdZQLlMvVVMfiTStjxRk,2191
|
352
353
|
parsl/tests/test_monitoring/test_basic.py,sha256=uXWx2O2Y2gfSO4e8zTjyj5bucKHG9OVzMxQNnq9abeY,2776
|
353
354
|
parsl/tests/test_monitoring/test_db_locks.py,sha256=PGoRmvqA6AYPXTPHOZPLH38Z4D6EEgSb6ZgNfZtwIGk,2910
|
354
|
-
parsl/tests/test_monitoring/test_fuzz_zmq.py,sha256=
|
355
|
+
parsl/tests/test_monitoring/test_fuzz_zmq.py,sha256=CpTRF2z2xpshlHHTNiNIIJMOx8bxSmSyAwbMYcOkgBk,3121
|
356
|
+
parsl/tests/test_monitoring/test_htex_init_blocks_vs_monitoring.py,sha256=aFxQWL0p3kmVWHgLjYTx0MdY3JMkVPQSae3pmRn5T3s,2605
|
355
357
|
parsl/tests/test_monitoring/test_incomplete_futures.py,sha256=9lJhkWlVB8gCCTkFjObzoh1uCL1pRmU6gFgEzLCztnY,2021
|
356
358
|
parsl/tests/test_monitoring/test_memoization_representation.py,sha256=tErT7zseSMaQ5eNmK3hH90J6OZKuAaFQG50OXK6Jy9s,2660
|
357
359
|
parsl/tests/test_monitoring/test_viz_colouring.py,sha256=k8SiELxPtnGYZ4r02VQt46RC61fGDVC4nmY768snX1U,591
|
@@ -372,6 +374,7 @@ parsl/tests/test_providers/test_submiterror_deprecation.py,sha256=ZutVj_0VJ7M-5U
|
|
372
374
|
parsl/tests/test_python_apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
373
375
|
parsl/tests/test_python_apps/test_arg_input_types.py,sha256=JXpfHiu8lr9BN6u1OzqFvGwBhxzsGTPMewHx6Wdo-HI,670
|
374
376
|
parsl/tests/test_python_apps/test_basic.py,sha256=lFqh4ugePbp_FRiHGUXxzV34iS7l8C5UkxTHuLcpnYs,855
|
377
|
+
parsl/tests/test_python_apps/test_context_manager.py,sha256=ajG8Vy0gxXzMO_rAm7yO_WO6XJCjEqUgmdRHdPBLCY0,844
|
375
378
|
parsl/tests/test_python_apps/test_dep_standard_futures.py,sha256=BloeaYBci0jS5al2d8Eqe3OfZ1tvolA5ZflOBQPR9Wo,859
|
376
379
|
parsl/tests/test_python_apps/test_dependencies.py,sha256=IRiTI_lPoWBSFSFnaBlE6Bv08PKEaf-qj5dfqO2RjT0,272
|
377
380
|
parsl/tests/test_python_apps/test_depfail_propagation.py,sha256=3q3HlVWrOixFtXWBvR_ypKtbdAHAJcKndXQ5drwrBQU,1488
|
@@ -421,6 +424,8 @@ parsl/tests/test_serialization/test_htex_code_cache.py,sha256=YW9ab4GCpeZWRdYsVE
|
|
421
424
|
parsl/tests/test_serialization/test_pack_resource_spec.py,sha256=eZ_gykB4uTDyEEF1HkExTUn98j9pTljxAnn6ucFhdvo,640
|
422
425
|
parsl/tests/test_serialization/test_proxystore_configured.py,sha256=_JbMzeUgcR-1Ss2hGAb2v0LBA0fzKpNpfO-HaUCR7Yo,2293
|
423
426
|
parsl/tests/test_serialization/test_proxystore_impl.py,sha256=Pn_4ulwCd7Tc6Qlmypq2ImT4DtErGDIfqHHmPTr7aOI,1226
|
427
|
+
parsl/tests/test_shutdown/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
428
|
+
parsl/tests/test_shutdown/test_kill_monitoring.py,sha256=5uzhcOPyvS9u4xzlweC5jV2Inl7eWZ1zFzcev73LWwY,1941
|
424
429
|
parsl/tests/test_staging/__init__.py,sha256=WZl9EHSkfYiSoE3Gbulcq2ifmn7IFGUkasJIobL5T5A,208
|
425
430
|
parsl/tests/test_staging/staging_provider.py,sha256=DEONOOGrYgDZimE3rkrbzFl_4KxhzmNJledh8Hho9fo,3242
|
426
431
|
parsl/tests/test_staging/test_1316.py,sha256=pj1QbmOJSRES1R4Ov380MmVe6xXvPUXh4FB48nE6vjI,2687
|
@@ -438,12 +443,12 @@ parsl/tests/test_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
|
|
438
443
|
parsl/tests/test_utils/test_representation_mixin.py,sha256=kUZeIDwA2rlbJ3-beGzLLwf3dOplTMCrWJN87etHcyY,1633
|
439
444
|
parsl/usage_tracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
440
445
|
parsl/usage_tracking/usage.py,sha256=pSADeogWqvkYI_n2pojv4IWDEFAQ3KwXNx6LDTohMHQ,6684
|
441
|
-
parsl-2024.3.
|
442
|
-
parsl-2024.3.
|
443
|
-
parsl-2024.3.
|
444
|
-
parsl-2024.3.
|
445
|
-
parsl-2024.3.
|
446
|
-
parsl-2024.3.
|
447
|
-
parsl-2024.3.
|
448
|
-
parsl-2024.3.
|
449
|
-
parsl-2024.3.
|
446
|
+
parsl-2024.3.25.data/scripts/exec_parsl_function.py,sha256=NtWNeBvRqksej38eRPw8zPBJ1CeW6vgaitve0tfz_qc,7801
|
447
|
+
parsl-2024.3.25.data/scripts/parsl_coprocess.py,sha256=Y7Tc-h9WGui-YDe3w_h91w2Sm1JNL1gJ9kAV4PE_gw8,5722
|
448
|
+
parsl-2024.3.25.data/scripts/process_worker_pool.py,sha256=iVrw160CpTAVuX9PH-ezU4ebm9C1_U6IMrkcdyTQJ58,41095
|
449
|
+
parsl-2024.3.25.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
|
450
|
+
parsl-2024.3.25.dist-info/METADATA,sha256=9FTVzNNJqpfn1NqveTatneCqEQhMu3Oo9kA-M7fx40k,3974
|
451
|
+
parsl-2024.3.25.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
452
|
+
parsl-2024.3.25.dist-info/entry_points.txt,sha256=XqnsWDYoEcLbsMcpnYGKLEnSBmaIe1YoM5YsBdJG2tI,176
|
453
|
+
parsl-2024.3.25.dist-info/top_level.txt,sha256=PIheYoUFQtF2icLsgOykgU-Cjuwr2Oi6On2jo5RYgRM,6
|
454
|
+
parsl-2024.3.25.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|