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 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.hub_interchange_port = None # type: Optional[int]
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.hub_interchange_port = self.monitoring.start(self.run_id, self.run_dir, self.config.run_dir)
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.hub_interchange_port
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[Any, JobStatus] = {}
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: Any, message: str):
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
- # helper scripts to prepare package tarballs for Parsl apps
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
 
@@ -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)
@@ -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.hub_interchange_port
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 = {}
@@ -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, TaggedMonitoringMessage
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
- "hub_port": self.hub_port,
170
- "hub_port_range": self.hub_port_range,
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
- self.logger.info("Started the router process {} and DBM process {}".format(self.router_proc.pid, self.dbm_proc.pid))
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
- self.logger.info(f"Started filesystem radio receiver process {self.filesystem_proc.pid}")
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
- self.logger.error("Hub has not completed initialization in 120s. Aborting")
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
- self.logger.error(f"MonitoringRouter sent an error message: {comm_q_result}")
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, ic_port = comm_q_result
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, ic_port))
216
+ self._dfk_channel.connect("tcp://{}:{}".format(self.hub_address, zmq_port))
221
217
 
222
- self.logger.info("Monitoring Hub initialized")
218
+ logger.info("Monitoring Hub initialized")
223
219
 
224
- return ic_port
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
- self.logger.debug("Sending message type {}".format(mtype))
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
- self.logger.exception(
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
- self.logger.info("Terminating Monitoring Hub")
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
- self.logger.error("There was a queued exception (Either router or DBM process got exception much earlier?)")
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
- self.logger.error(
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
- self.logger.info("Waiting for router to terminate")
254
+ logger.info("Waiting for router to terminate")
259
255
  self.router_proc.join()
260
- self.logger.debug("Finished waiting for router termination")
256
+ logger.debug("Finished waiting for router termination")
261
257
  if len(exception_msgs) == 0:
262
- self.logger.debug("Sending STOP to DBM")
258
+ logger.debug("Sending STOP to DBM")
263
259
  self.priority_msgs.put(("STOP", 0))
264
260
  else:
265
- self.logger.debug("Not sending STOP to DBM, because there were DBM exceptions")
266
- self.logger.debug("Waiting for DB termination")
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
- self.logger.debug("Finished waiting for DBM termination")
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
- self.logger.info("Terminating filesystem radio receiver process")
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
- hub_interchange_port = parsl.dfk().hub_interchange_port
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, hub_interchange_port))
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
@@ -3,4 +3,4 @@
3
3
  Year.Month.Day[alpha/beta/..]
4
4
  Alphas will be numbered like this -> 2024.12.10a0
5
5
  """
6
- VERSION = '2024.03.18'
6
+ VERSION = '2024.03.25'
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: parsl
3
- Version: 2024.3.18
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.18.tar.gz
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=dhmXAlQKRlQPsExiUS22Ca2clSFcL-0irfA1G-7kIyo,131
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=1xvZKXDleb1GMS7x4dg8BGhKJfD0wPq4bjbAtFbkkc4,64256
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=DyKzXZztPagh9xQykTUuJRMR9g3i6qkbZi8YpWrIfXM,4340
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=GNBYrAhOpHSnmJA0NouK9by_uJxXn8vlPgBhcp0pSFo,10940
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=KHiRGcLWHyVBm_6E4IowCV20Z15lsf2ZUnTR49IQxNg,31544
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=En8UJJbcQUY88bwYchASco71LTWb6YfXXlbxPlD46Sk,49107
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=dvfZFqTwAcFXrIx3sjFTcjtwOB0-xGn6QnPwJEG-kAI,2311
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=30W-KvzYGQfblFwe_6ZpRVvzerwZD76IC4Xvt9FBw1c,5349
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=N_g3ijGJK46y0k2CEWMGvGR4Ym1mR13yeBkW6TiHHMo,23488
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=Xi08Drt_DZKbo3Ihl83cgsTTMRok1bBpyrVOQQWlbf4,3145
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.18.data/scripts/exec_parsl_function.py,sha256=NtWNeBvRqksej38eRPw8zPBJ1CeW6vgaitve0tfz_qc,7801
442
- parsl-2024.3.18.data/scripts/parsl_coprocess.py,sha256=Y7Tc-h9WGui-YDe3w_h91w2Sm1JNL1gJ9kAV4PE_gw8,5722
443
- parsl-2024.3.18.data/scripts/process_worker_pool.py,sha256=iVrw160CpTAVuX9PH-ezU4ebm9C1_U6IMrkcdyTQJ58,41095
444
- parsl-2024.3.18.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
445
- parsl-2024.3.18.dist-info/METADATA,sha256=HywLgkSikpur2yGd42LiMsFytWwnayht8sQasJ6n5j8,3960
446
- parsl-2024.3.18.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
447
- parsl-2024.3.18.dist-info/entry_points.txt,sha256=XqnsWDYoEcLbsMcpnYGKLEnSBmaIe1YoM5YsBdJG2tI,176
448
- parsl-2024.3.18.dist-info/top_level.txt,sha256=PIheYoUFQtF2icLsgOykgU-Cjuwr2Oi6On2jo5RYgRM,6
449
- parsl-2024.3.18.dist-info/RECORD,,
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,,