parsl 2025.3.3__py3-none-any.whl → 2025.3.10__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.
@@ -536,7 +536,7 @@ class HighThroughputExecutor(BlockProviderExecutor, RepresentationMixin, UsageIn
536
536
  "interchange_address": self.address,
537
537
  "worker_ports": self.worker_ports,
538
538
  "worker_port_range": self.worker_port_range,
539
- "hub_address": self.hub_address,
539
+ "hub_address": self.loopback_address,
540
540
  "hub_zmq_port": self.hub_zmq_port,
541
541
  "logdir": self.logdir,
542
542
  "heartbeat_threshold": self.heartbeat_threshold,
@@ -4,6 +4,7 @@ import os
4
4
  import pickle
5
5
  import queue
6
6
  import subprocess
7
+ from dataclasses import dataclass, field
7
8
  from enum import Enum
8
9
  from typing import Dict, List, Optional
9
10
 
@@ -69,6 +70,14 @@ class MPINodesUnavailable(Exception):
69
70
  return f"MPINodesUnavailable(requested={self.requested} available={self.available})"
70
71
 
71
72
 
73
+ @dataclass(order=True)
74
+ class PrioritizedTask:
75
+ # Comparing dict will fail since they are unhashable
76
+ # This dataclass limits comparison to the priority field
77
+ priority: int
78
+ task: Dict = field(compare=False)
79
+
80
+
72
81
  class TaskScheduler:
73
82
  """Default TaskScheduler that does no taskscheduling
74
83
 
@@ -111,7 +120,7 @@ class MPITaskScheduler(TaskScheduler):
111
120
  super().__init__(pending_task_q, pending_result_q)
112
121
  self.scheduler = identify_scheduler()
113
122
  # PriorityQueue is threadsafe
114
- self._backlog_queue: queue.PriorityQueue = queue.PriorityQueue()
123
+ self._backlog_queue: queue.PriorityQueue[PrioritizedTask] = queue.PriorityQueue()
115
124
  self._map_tasks_to_nodes: Dict[str, List[str]] = {}
116
125
  self.available_nodes = get_nodes_in_batchjob(self.scheduler)
117
126
  self._free_node_counter = SpawnContext.Value("i", len(self.available_nodes))
@@ -169,7 +178,7 @@ class MPITaskScheduler(TaskScheduler):
169
178
  allocated_nodes = self._get_nodes(nodes_needed)
170
179
  except MPINodesUnavailable:
171
180
  logger.info(f"Not enough resources, placing task {tid} into backlog")
172
- self._backlog_queue.put((nodes_needed, task_package))
181
+ self._backlog_queue.put(PrioritizedTask(nodes_needed, task_package))
173
182
  return
174
183
  else:
175
184
  resource_spec["MPI_NODELIST"] = ",".join(allocated_nodes)
@@ -183,8 +192,8 @@ class MPITaskScheduler(TaskScheduler):
183
192
  def _schedule_backlog_tasks(self):
184
193
  """Attempt to schedule backlogged tasks"""
185
194
  try:
186
- _nodes_requested, task_package = self._backlog_queue.get(block=False)
187
- self.put_task(task_package)
195
+ prioritized_task = self._backlog_queue.get(block=False)
196
+ self.put_task(prioritized_task.task)
188
197
  except queue.Empty:
189
198
  return
190
199
  else:
@@ -3,23 +3,21 @@ from __future__ import annotations
3
3
  import logging
4
4
  import multiprocessing.synchronize as ms
5
5
  import os
6
- import pickle
7
6
  import queue
8
- import time
9
7
  from multiprocessing import Event
10
8
  from multiprocessing.queues import Queue
11
- from typing import TYPE_CHECKING, Literal, Optional, Tuple, Union, cast
9
+ from typing import TYPE_CHECKING, Literal, Optional, Tuple, Union
12
10
 
13
11
  import typeguard
14
12
 
15
- from parsl.log_utils import set_file_logger
16
13
  from parsl.monitoring.errors import MonitoringHubStartError
14
+ from parsl.monitoring.radios.filesystem_router import filesystem_router_starter
17
15
  from parsl.monitoring.radios.multiprocessing import MultiprocessingQueueRadioSender
18
- from parsl.monitoring.router import router_starter
16
+ from parsl.monitoring.radios.udp_router import udp_router_starter
17
+ from parsl.monitoring.radios.zmq_router import zmq_router_starter
19
18
  from parsl.monitoring.types import TaggedMonitoringMessage
20
19
  from parsl.multiprocessing import ForkProcess, SizedQueue
21
- from parsl.process_loggers import wrap_with_logs
22
- from parsl.utils import RepresentationMixin, setproctitle
20
+ from parsl.utils import RepresentationMixin
23
21
 
24
22
  _db_manager_excepts: Optional[Exception]
25
23
 
@@ -121,11 +119,14 @@ class MonitoringHub(RepresentationMixin):
121
119
  # in the future, Queue will allow runtime subscripts.
122
120
 
123
121
  if TYPE_CHECKING:
124
- comm_q: Queue[Union[Tuple[int, int], str]]
122
+ zmq_comm_q: Queue[Union[int, str]]
123
+ udp_comm_q: Queue[Union[int, str]]
125
124
  else:
126
- comm_q: Queue
125
+ zmq_comm_q: Queue
126
+ udp_comm_q: Queue
127
127
 
128
- comm_q = SizedQueue(maxsize=10)
128
+ zmq_comm_q = SizedQueue(maxsize=10)
129
+ udp_comm_q = SizedQueue(maxsize=10)
129
130
 
130
131
  self.exception_q: Queue[Tuple[str, str]]
131
132
  self.exception_q = SizedQueue(maxsize=10)
@@ -136,21 +137,35 @@ class MonitoringHub(RepresentationMixin):
136
137
  self.router_exit_event: ms.Event
137
138
  self.router_exit_event = Event()
138
139
 
139
- self.router_proc = ForkProcess(target=router_starter,
140
- kwargs={"comm_q": comm_q,
141
- "exception_q": self.exception_q,
142
- "resource_msgs": self.resource_msgs,
143
- "exit_event": self.router_exit_event,
144
- "hub_address": self.hub_address,
145
- "udp_port": self.hub_port,
146
- "zmq_port_range": self.hub_port_range,
147
- "run_dir": dfk_run_dir,
148
- "logging_level": logging.DEBUG if self.monitoring_debug else logging.INFO,
149
- },
150
- name="Monitoring-Router-Process",
151
- daemon=True,
152
- )
153
- self.router_proc.start()
140
+ self.zmq_router_proc = ForkProcess(target=zmq_router_starter,
141
+ kwargs={"comm_q": zmq_comm_q,
142
+ "exception_q": self.exception_q,
143
+ "resource_msgs": self.resource_msgs,
144
+ "exit_event": self.router_exit_event,
145
+ "hub_address": self.hub_address,
146
+ "zmq_port_range": self.hub_port_range,
147
+ "run_dir": dfk_run_dir,
148
+ "logging_level": logging.DEBUG if self.monitoring_debug else logging.INFO,
149
+ },
150
+ name="Monitoring-ZMQ-Router-Process",
151
+ daemon=True,
152
+ )
153
+ self.zmq_router_proc.start()
154
+
155
+ self.udp_router_proc = ForkProcess(target=udp_router_starter,
156
+ kwargs={"comm_q": udp_comm_q,
157
+ "exception_q": self.exception_q,
158
+ "resource_msgs": self.resource_msgs,
159
+ "exit_event": self.router_exit_event,
160
+ "hub_address": self.hub_address,
161
+ "udp_port": self.hub_port,
162
+ "run_dir": dfk_run_dir,
163
+ "logging_level": logging.DEBUG if self.monitoring_debug else logging.INFO,
164
+ },
165
+ name="Monitoring-UDP-Router-Process",
166
+ daemon=True,
167
+ )
168
+ self.udp_router_proc.start()
154
169
 
155
170
  self.dbm_proc = ForkProcess(target=dbm_starter,
156
171
  args=(self.exception_q, self.resource_msgs,),
@@ -162,9 +177,10 @@ class MonitoringHub(RepresentationMixin):
162
177
  daemon=True,
163
178
  )
164
179
  self.dbm_proc.start()
165
- logger.info("Started the router process %s and DBM process %s", self.router_proc.pid, self.dbm_proc.pid)
180
+ logger.info("Started ZMQ router process %s, UDP router process %s and DBM process %s",
181
+ self.zmq_router_proc.pid, self.udp_router_proc.pid, self.dbm_proc.pid)
166
182
 
167
- self.filesystem_proc = ForkProcess(target=filesystem_receiver,
183
+ self.filesystem_proc = ForkProcess(target=filesystem_router_starter,
168
184
  args=(self.resource_msgs, dfk_run_dir),
169
185
  name="Monitoring-Filesystem-Process",
170
186
  daemon=True
@@ -175,25 +191,36 @@ class MonitoringHub(RepresentationMixin):
175
191
  self.radio = MultiprocessingQueueRadioSender(self.resource_msgs)
176
192
 
177
193
  try:
178
- comm_q_result = comm_q.get(block=True, timeout=120)
179
- comm_q.close()
180
- comm_q.join_thread()
194
+ zmq_comm_q_result = zmq_comm_q.get(block=True, timeout=120)
195
+ zmq_comm_q.close()
196
+ zmq_comm_q.join_thread()
181
197
  except queue.Empty:
182
- logger.error("Hub has not completed initialization in 120s. Aborting")
198
+ logger.error("Monitoring ZMQ Router has not reported port in 120s. Aborting")
183
199
  raise MonitoringHubStartError()
184
200
 
185
- if isinstance(comm_q_result, str):
186
- logger.error("MonitoringRouter sent an error message: %s", comm_q_result)
187
- raise RuntimeError(f"MonitoringRouter failed to start: {comm_q_result}")
201
+ if isinstance(zmq_comm_q_result, str):
202
+ logger.error("MonitoringRouter sent an error message: %s", zmq_comm_q_result)
203
+ raise RuntimeError(f"MonitoringRouter failed to start: {zmq_comm_q_result}")
204
+
205
+ self.hub_zmq_port = zmq_comm_q_result
206
+
207
+ try:
208
+ udp_comm_q_result = udp_comm_q.get(block=True, timeout=120)
209
+ udp_comm_q.close()
210
+ udp_comm_q.join_thread()
211
+ except queue.Empty:
212
+ logger.error("Monitoring UDP router has not reported port in 120s. Aborting")
213
+ raise MonitoringHubStartError()
188
214
 
189
- udp_port, zmq_port = comm_q_result
215
+ if isinstance(udp_comm_q_result, str):
216
+ logger.error("MonitoringRouter sent an error message: %s", udp_comm_q_result)
217
+ raise RuntimeError(f"MonitoringRouter failed to start: {udp_comm_q_result}")
190
218
 
219
+ udp_port = udp_comm_q_result
191
220
  self.monitoring_hub_url = "udp://{}:{}".format(self.hub_address, udp_port)
192
221
 
193
222
  logger.info("Monitoring Hub initialized")
194
223
 
195
- self.hub_zmq_port = zmq_port
196
-
197
224
  def send(self, message: TaggedMonitoringMessage) -> None:
198
225
  logger.debug("Sending message type %s", message[0])
199
226
  self.radio.send(message)
@@ -216,14 +243,21 @@ class MonitoringHub(RepresentationMixin):
216
243
  exception_msg[0],
217
244
  exception_msg[1]
218
245
  )
219
- self.router_proc.terminate()
246
+ self.zmq_router_proc.terminate()
247
+ self.udp_router_proc.terminate()
220
248
  self.dbm_proc.terminate()
221
249
  self.filesystem_proc.terminate()
222
250
  logger.info("Setting router termination event")
223
251
  self.router_exit_event.set()
224
- logger.info("Waiting for router to terminate")
225
- self.router_proc.join()
226
- self.router_proc.close()
252
+
253
+ logger.info("Waiting for ZMQ router to terminate")
254
+ self.zmq_router_proc.join()
255
+ self.zmq_router_proc.close()
256
+
257
+ logger.info("Waiting for UDP router to terminate")
258
+ self.udp_router_proc.join()
259
+ self.udp_router_proc.close()
260
+
227
261
  logger.debug("Finished waiting for router termination")
228
262
  if len(exception_msgs) == 0:
229
263
  logger.debug("Sending STOP to DBM")
@@ -248,41 +282,3 @@ class MonitoringHub(RepresentationMixin):
248
282
  self.resource_msgs.close()
249
283
  self.resource_msgs.join_thread()
250
284
  logger.info("Closed monitoring multiprocessing queues")
251
-
252
-
253
- @wrap_with_logs
254
- def filesystem_receiver(q: Queue[TaggedMonitoringMessage], run_dir: str) -> None:
255
- logger = set_file_logger(f"{run_dir}/monitoring_filesystem_radio.log",
256
- name="monitoring_filesystem_radio",
257
- level=logging.INFO)
258
-
259
- logger.info("Starting filesystem radio receiver")
260
- setproctitle("parsl: monitoring filesystem receiver")
261
- base_path = f"{run_dir}/monitor-fs-radio/"
262
- tmp_dir = f"{base_path}/tmp/"
263
- new_dir = f"{base_path}/new/"
264
- logger.debug("Creating new and tmp paths under %s", base_path)
265
-
266
- target_radio = MultiprocessingQueueRadioSender(q)
267
-
268
- os.makedirs(tmp_dir, exist_ok=True)
269
- os.makedirs(new_dir, exist_ok=True)
270
-
271
- while True: # this loop will end on process termination
272
- logger.debug("Start filesystem radio receiver loop")
273
-
274
- # iterate over files in new_dir
275
- for filename in os.listdir(new_dir):
276
- try:
277
- logger.info("Processing filesystem radio file %s", filename)
278
- full_path_filename = f"{new_dir}/{filename}"
279
- with open(full_path_filename, "rb") as f:
280
- message = pickle.load(f)
281
- logger.debug("Message received is: %s", message)
282
- assert isinstance(message, tuple)
283
- target_radio.send(cast(TaggedMonitoringMessage, message))
284
- os.remove(full_path_filename)
285
- except Exception:
286
- logger.exception("Exception processing %s - probably will be retried next iteration", filename)
287
-
288
- time.sleep(1) # whats a good time for this poll?
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import pickle
6
+ import time
7
+ from multiprocessing.queues import Queue
8
+ from typing import cast
9
+
10
+ from parsl.log_utils import set_file_logger
11
+ from parsl.monitoring.radios.multiprocessing import MultiprocessingQueueRadioSender
12
+ from parsl.monitoring.types import TaggedMonitoringMessage
13
+ from parsl.process_loggers import wrap_with_logs
14
+ from parsl.utils import setproctitle
15
+
16
+
17
+ @wrap_with_logs
18
+ def filesystem_router_starter(q: Queue[TaggedMonitoringMessage], run_dir: str) -> None:
19
+ logger = set_file_logger(f"{run_dir}/monitoring_filesystem_radio.log",
20
+ name="monitoring_filesystem_radio",
21
+ level=logging.INFO)
22
+
23
+ logger.info("Starting filesystem radio receiver")
24
+ setproctitle("parsl: monitoring filesystem receiver")
25
+ base_path = f"{run_dir}/monitor-fs-radio/"
26
+ tmp_dir = f"{base_path}/tmp/"
27
+ new_dir = f"{base_path}/new/"
28
+ logger.debug("Creating new and tmp paths under %s", base_path)
29
+
30
+ target_radio = MultiprocessingQueueRadioSender(q)
31
+
32
+ os.makedirs(tmp_dir, exist_ok=True)
33
+ os.makedirs(new_dir, exist_ok=True)
34
+
35
+ while True: # this loop will end on process termination
36
+ logger.debug("Start filesystem radio receiver loop")
37
+
38
+ # iterate over files in new_dir
39
+ for filename in os.listdir(new_dir):
40
+ try:
41
+ logger.info("Processing filesystem radio file %s", filename)
42
+ full_path_filename = f"{new_dir}/{filename}"
43
+ with open(full_path_filename, "rb") as f:
44
+ message = pickle.load(f)
45
+ logger.debug("Message received is: %s", message)
46
+ assert isinstance(message, tuple)
47
+ target_radio.send(cast(TaggedMonitoringMessage, message))
48
+ os.remove(full_path_filename)
49
+ except Exception:
50
+ logger.exception("Exception processing %s - probably will be retried next iteration", filename)
51
+
52
+ time.sleep(1) # whats a good time for this poll?
@@ -5,17 +5,14 @@ import multiprocessing.queues as mpq
5
5
  import os
6
6
  import pickle
7
7
  import socket
8
- import threading
9
8
  import time
10
9
  from multiprocessing.synchronize import Event
11
- from typing import Optional, Tuple
10
+ from typing import Optional
12
11
 
13
12
  import typeguard
14
- import zmq
15
13
 
16
14
  from parsl.log_utils import set_file_logger
17
15
  from parsl.monitoring.radios.multiprocessing import MultiprocessingQueueRadioSender
18
- from parsl.monitoring.types import TaggedMonitoringMessage
19
16
  from parsl.process_loggers import wrap_with_logs
20
17
  from parsl.utils import setproctitle
21
18
 
@@ -28,7 +25,6 @@ class MonitoringRouter:
28
25
  *,
29
26
  hub_address: str,
30
27
  udp_port: Optional[int] = None,
31
- zmq_port_range: Tuple[int, int] = (55050, 56000),
32
28
 
33
29
  monitoring_hub_address: str = "127.0.0.1",
34
30
  run_dir: str = ".",
@@ -45,9 +41,6 @@ class MonitoringRouter:
45
41
  The ip address at which the workers will be able to reach the Hub.
46
42
  udp_port : int
47
43
  The specific port at which workers will be able to reach the Hub via UDP. Default: None
48
- zmq_port_range : tuple(int, int)
49
- The MonitoringHub picks ports at random from the range which will be used by Hub.
50
- Default: (55050, 56000)
51
44
  run_dir : str
52
45
  Parsl log directory paths. Logs and temp files go here. Default: '.'
53
46
  logging_level : int
@@ -60,7 +53,7 @@ class MonitoringRouter:
60
53
  An event that the main Parsl process will set to signal that the monitoring router should shut down.
61
54
  """
62
55
  os.makedirs(run_dir, exist_ok=True)
63
- self.logger = set_file_logger(f"{run_dir}/monitoring_router.log",
56
+ self.logger = set_file_logger(f"{run_dir}/monitoring_udp_router.log",
64
57
  name="monitoring_router",
65
58
  level=logging_level)
66
59
  self.logger.debug("Monitoring router starting")
@@ -88,37 +81,12 @@ class MonitoringRouter:
88
81
  self.udp_sock.settimeout(self.loop_freq / 1000)
89
82
  self.logger.info("Initialized the UDP socket on 0.0.0.0:{}".format(self.udp_port))
90
83
 
91
- self._context = zmq.Context()
92
- self.zmq_receiver_channel = self._context.socket(zmq.DEALER)
93
- self.zmq_receiver_channel.setsockopt(zmq.LINGER, 0)
94
- self.zmq_receiver_channel.set_hwm(0)
95
- self.zmq_receiver_channel.RCVTIMEO = int(self.loop_freq) # in milliseconds
96
- self.logger.debug("hub_address: {}. zmq_port_range {}".format(hub_address, zmq_port_range))
97
- self.zmq_receiver_port = self.zmq_receiver_channel.bind_to_random_port("tcp://*",
98
- min_port=zmq_port_range[0],
99
- max_port=zmq_port_range[1])
100
-
101
84
  self.target_radio = MultiprocessingQueueRadioSender(resource_msgs)
102
85
  self.exit_event = exit_event
103
86
 
104
87
  @wrap_with_logs(target="monitoring_router")
105
88
  def start(self) -> None:
106
- self.logger.info("Starting UDP listener thread")
107
- udp_radio_receiver_thread = threading.Thread(target=self.start_udp_listener, daemon=True)
108
- udp_radio_receiver_thread.start()
109
-
110
- self.logger.info("Starting ZMQ listener thread")
111
- zmq_radio_receiver_thread = threading.Thread(target=self.start_zmq_listener, daemon=True)
112
- zmq_radio_receiver_thread.start()
113
-
114
- self.logger.info("Joining on ZMQ listener thread")
115
- zmq_radio_receiver_thread.join()
116
- self.logger.info("Joining on UDP listener thread")
117
- udp_radio_receiver_thread.join()
118
- self.logger.info("Joined on both ZMQ and UDP listener threads")
119
-
120
- @wrap_with_logs(target="monitoring_router")
121
- def start_udp_listener(self) -> None:
89
+ self.logger.info("Starting UDP listener")
122
90
  try:
123
91
  while not self.exit_event.is_set():
124
92
  try:
@@ -145,55 +113,24 @@ class MonitoringRouter:
145
113
  finally:
146
114
  self.logger.info("UDP listener finished")
147
115
 
148
- @wrap_with_logs(target="monitoring_router")
149
- def start_zmq_listener(self) -> None:
150
- try:
151
- while not self.exit_event.is_set():
152
- try:
153
- dfk_loop_start = time.time()
154
- while time.time() - dfk_loop_start < 1.0: # TODO make configurable
155
- # note that nothing checks that msg really is of the annotated type
156
- msg: TaggedMonitoringMessage
157
- msg = self.zmq_receiver_channel.recv_pyobj()
158
-
159
- assert isinstance(msg, tuple), "ZMQ Receiver expects only tuples, got {}".format(msg)
160
- assert len(msg) >= 1, "ZMQ Receiver expects tuples of length at least 1, got {}".format(msg)
161
- assert len(msg) == 2, "ZMQ Receiver expects message tuples of exactly length 2, got {}".format(msg)
162
-
163
- self.target_radio.send(msg)
164
- except zmq.Again:
165
- pass
166
- except Exception:
167
- # This will catch malformed messages. What happens if the
168
- # channel is broken in such a way that it always raises
169
- # an exception? Looping on this would maybe be the wrong
170
- # thing to do.
171
- self.logger.warning("Failure processing a ZMQ message", exc_info=True)
172
-
173
- self.logger.info("ZMQ listener finishing normally")
174
- finally:
175
- self.logger.info("ZMQ listener finished")
176
-
177
116
 
178
117
  @wrap_with_logs
179
118
  @typeguard.typechecked
180
- def router_starter(*,
181
- comm_q: mpq.Queue,
182
- exception_q: mpq.Queue,
183
- resource_msgs: mpq.Queue,
184
- exit_event: Event,
185
-
186
- hub_address: str,
187
- udp_port: Optional[int],
188
- zmq_port_range: Tuple[int, int],
189
-
190
- run_dir: str,
191
- logging_level: int) -> None:
192
- setproctitle("parsl: monitoring router")
119
+ def udp_router_starter(*,
120
+ comm_q: mpq.Queue,
121
+ exception_q: mpq.Queue,
122
+ resource_msgs: mpq.Queue,
123
+ exit_event: Event,
124
+
125
+ hub_address: str,
126
+ udp_port: Optional[int],
127
+
128
+ run_dir: str,
129
+ logging_level: int) -> None:
130
+ setproctitle("parsl: monitoring UDP router")
193
131
  try:
194
132
  router = MonitoringRouter(hub_address=hub_address,
195
133
  udp_port=udp_port,
196
- zmq_port_range=zmq_port_range,
197
134
  run_dir=run_dir,
198
135
  logging_level=logging_level,
199
136
  resource_msgs=resource_msgs,
@@ -202,11 +139,11 @@ def router_starter(*,
202
139
  logger.error("MonitoringRouter construction failed.", exc_info=True)
203
140
  comm_q.put(f"Monitoring router construction failed: {e}")
204
141
  else:
205
- comm_q.put((router.udp_port, router.zmq_receiver_port))
142
+ comm_q.put(router.udp_port)
206
143
 
207
144
  router.logger.info("Starting MonitoringRouter in router_starter")
208
145
  try:
209
146
  router.start()
210
147
  except Exception as e:
211
- router.logger.exception("router.start exception")
148
+ router.logger.exception("UDP router start exception")
212
149
  exception_q.put(('Hub', str(e)))
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import multiprocessing.queues as mpq
5
+ import os
6
+ import time
7
+ from multiprocessing.synchronize import Event
8
+ from typing import Tuple
9
+
10
+ import typeguard
11
+ import zmq
12
+
13
+ from parsl.log_utils import set_file_logger
14
+ from parsl.monitoring.radios.multiprocessing import MultiprocessingQueueRadioSender
15
+ from parsl.monitoring.types import TaggedMonitoringMessage
16
+ from parsl.process_loggers import wrap_with_logs
17
+ from parsl.utils import setproctitle
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class MonitoringRouter:
23
+
24
+ def __init__(self,
25
+ *,
26
+ hub_address: str,
27
+ zmq_port_range: Tuple[int, int] = (55050, 56000),
28
+
29
+ run_dir: str = ".",
30
+ logging_level: int = logging.INFO,
31
+ resource_msgs: mpq.Queue,
32
+ exit_event: Event,
33
+ ):
34
+ """ Initializes a monitoring configuration class.
35
+
36
+ Parameters
37
+ ----------
38
+ hub_address : str
39
+ The ip address at which the workers will be able to reach the Hub.
40
+ zmq_port_range : tuple(int, int)
41
+ The MonitoringHub picks ports at random from the range which will be used by Hub.
42
+ Default: (55050, 56000)
43
+ run_dir : str
44
+ Parsl log directory paths. Logs and temp files go here. Default: '.'
45
+ logging_level : int
46
+ Logging level as defined in the logging module. Default: logging.INFO
47
+ resource_msgs : multiprocessing.Queue
48
+ A multiprocessing queue to receive messages to be routed onwards to the database process
49
+ exit_event : Event
50
+ An event that the main Parsl process will set to signal that the monitoring router should shut down.
51
+ """
52
+ os.makedirs(run_dir, exist_ok=True)
53
+ self.logger = set_file_logger(f"{run_dir}/monitoring_zmq_router.log",
54
+ name="monitoring_router",
55
+ level=logging_level)
56
+ self.logger.debug("Monitoring router starting")
57
+
58
+ self.hub_address = hub_address
59
+
60
+ self.loop_freq = 10.0 # milliseconds
61
+
62
+ self._context = zmq.Context()
63
+ self.zmq_receiver_channel = self._context.socket(zmq.DEALER)
64
+ self.zmq_receiver_channel.setsockopt(zmq.LINGER, 0)
65
+ self.zmq_receiver_channel.set_hwm(0)
66
+ self.zmq_receiver_channel.RCVTIMEO = int(self.loop_freq) # in milliseconds
67
+ self.logger.debug("hub_address: {}. zmq_port_range {}".format(hub_address, zmq_port_range))
68
+ self.zmq_receiver_port = self.zmq_receiver_channel.bind_to_random_port("tcp://*",
69
+ min_port=zmq_port_range[0],
70
+ max_port=zmq_port_range[1])
71
+
72
+ self.target_radio = MultiprocessingQueueRadioSender(resource_msgs)
73
+ self.exit_event = exit_event
74
+
75
+ @wrap_with_logs(target="monitoring_router")
76
+ def start(self) -> None:
77
+ self.logger.info("Starting ZMQ listener")
78
+ try:
79
+ while not self.exit_event.is_set():
80
+ try:
81
+ dfk_loop_start = time.time()
82
+ while time.time() - dfk_loop_start < 1.0: # TODO make configurable
83
+ # note that nothing checks that msg really is of the annotated type
84
+ msg: TaggedMonitoringMessage
85
+ msg = self.zmq_receiver_channel.recv_pyobj()
86
+
87
+ assert isinstance(msg, tuple), "ZMQ Receiver expects only tuples, got {}".format(msg)
88
+ assert len(msg) >= 1, "ZMQ Receiver expects tuples of length at least 1, got {}".format(msg)
89
+ assert len(msg) == 2, "ZMQ Receiver expects message tuples of exactly length 2, got {}".format(msg)
90
+
91
+ self.target_radio.send(msg)
92
+ except zmq.Again:
93
+ pass
94
+ except Exception:
95
+ # This will catch malformed messages. What happens if the
96
+ # channel is broken in such a way that it always raises
97
+ # an exception? Looping on this would maybe be the wrong
98
+ # thing to do.
99
+ self.logger.warning("Failure processing a ZMQ message", exc_info=True)
100
+
101
+ self.logger.info("ZMQ listener finishing normally")
102
+ finally:
103
+ self.logger.info("ZMQ listener finished")
104
+
105
+
106
+ @wrap_with_logs
107
+ @typeguard.typechecked
108
+ def zmq_router_starter(*,
109
+ comm_q: mpq.Queue,
110
+ exception_q: mpq.Queue,
111
+ resource_msgs: mpq.Queue,
112
+ exit_event: Event,
113
+
114
+ hub_address: str,
115
+ zmq_port_range: Tuple[int, int],
116
+
117
+ run_dir: str,
118
+ logging_level: int) -> None:
119
+ setproctitle("parsl: monitoring zmq router")
120
+ try:
121
+ router = MonitoringRouter(hub_address=hub_address,
122
+ zmq_port_range=zmq_port_range,
123
+ run_dir=run_dir,
124
+ logging_level=logging_level,
125
+ resource_msgs=resource_msgs,
126
+ exit_event=exit_event)
127
+ except Exception as e:
128
+ logger.error("MonitoringRouter construction failed.", exc_info=True)
129
+ comm_q.put(f"Monitoring router construction failed: {e}")
130
+ else:
131
+ comm_q.put(router.zmq_receiver_port)
132
+
133
+ router.logger.info("Starting MonitoringRouter in router_starter")
134
+ try:
135
+ router.start()
136
+ except Exception as e:
137
+ router.logger.exception("ZMQ router start exception")
138
+ exception_q.put(('Hub', str(e)))
@@ -161,3 +161,28 @@ def test_MPISched_contention():
161
161
  assert task_on_worker_side['task_id'] == 2
162
162
  _, _, _, resource_spec = unpack_res_spec_apply_message(task_on_worker_side['buffer'])
163
163
  assert len(resource_spec['MPI_NODELIST'].split(',')) == 8
164
+
165
+
166
+ @pytest.mark.local
167
+ def test_hashable_backlog_queue():
168
+ """Run multiple large tasks that to force entry into backlog_queue
169
+ where queue.PriorityQueue expects hashability/comparability
170
+ """
171
+
172
+ task_q, result_q = SpawnContext.Queue(), SpawnContext.Queue()
173
+ scheduler = MPITaskScheduler(task_q, result_q)
174
+
175
+ assert scheduler.available_nodes
176
+ assert len(scheduler.available_nodes) == 8
177
+
178
+ assert scheduler._free_node_counter.value == 8
179
+
180
+ for i in range(3):
181
+ mock_task_buffer = pack_res_spec_apply_message("func", "args", "kwargs",
182
+ resource_specification={
183
+ "num_nodes": 8,
184
+ "ranks_per_node": 2
185
+ })
186
+ task_package = {"task_id": i, "buffer": mock_task_buffer}
187
+ scheduler.put_task(task_package)
188
+ assert scheduler._backlog_queue.qsize() == 2, "Expected 2 backlogged tasks"
@@ -14,6 +14,9 @@ from parsl.providers import LocalProvider
14
14
  # timeout later on.
15
15
  BLOCK_COUNT = 3
16
16
 
17
+ # the try_assert timeout for the above number of blocks to get started
18
+ PERMITTED_STARTUP_TIME_S = 30
19
+
17
20
 
18
21
  class AccumulatingLocalProvider(LocalProvider):
19
22
  def __init__(self, *args, **kwargs):
@@ -67,7 +70,7 @@ def test_shutdown_scalein_blocks(tmpd_cwd, try_assert):
67
70
 
68
71
  with parsl.load(config):
69
72
  # this will wait for everything to be scaled out fully
70
- try_assert(lambda: len(htex.connected_managers()) == BLOCK_COUNT)
73
+ try_assert(lambda: len(htex.connected_managers()) == BLOCK_COUNT, timeout_ms=PERMITTED_STARTUP_TIME_S * 1000)
71
74
 
72
75
  assert len(accumulating_provider.submit_job_ids) == BLOCK_COUNT, f"Exactly {BLOCK_COUNT} blocks should have been launched"
73
76
  assert len(accumulating_provider.cancel_job_ids) == BLOCK_COUNT, f"Exactly {BLOCK_COUNT} blocks should have been scaled in"
@@ -30,7 +30,7 @@ def test_no_kills():
30
30
 
31
31
  @pytest.mark.local
32
32
  @pytest.mark.parametrize("sig", [signal.SIGINT, signal.SIGTERM, signal.SIGKILL, signal.SIGQUIT])
33
- @pytest.mark.parametrize("process_attr", ["router_proc", "dbm_proc"])
33
+ @pytest.mark.parametrize("process_attr", ["zmq_router_proc", "udp_router_proc", "dbm_proc", "filesystem_proc"])
34
34
  def test_kill_monitoring_helper_process(sig, process_attr, try_assert):
35
35
  """This tests that we can kill a monitoring process and still have successful shutdown.
36
36
  SIGINT emulates some racy behaviour when ctrl-C is pressed: that
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 = '2025.03.03'
6
+ VERSION = '2025.03.10'
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: parsl
3
- Version: 2025.3.3
3
+ Version: 2025.3.10
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/2025.03.03.tar.gz
6
+ Download-URL: https://github.com/Parsl/parsl/archive/2025.03.10.tar.gz
7
7
  Author: The Parsl Team
8
8
  Author-email: parsl@googlegroups.com
9
9
  License: Apache 2.0
@@ -8,7 +8,7 @@ parsl/multiprocessing.py,sha256=MyaEcEq-Qf860u7V98u-PZrPNdtzOZL_NW6EhIJnmfQ,1937
8
8
  parsl/process_loggers.py,sha256=uQ7Gd0W72Jz7rrcYlOMfLsAEhkRltxXJL2MgdduJjEw,1136
9
9
  parsl/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  parsl/utils.py,sha256=codTX6_KLhgeTwNkRzc1lo4bgc1M93eJ-lkqOO98fvk,14331
11
- parsl/version.py,sha256=JZR2YCezBq1F5cw4-KEsJxZK4DKSK_Po-wBKZDC4T7o,131
11
+ parsl/version.py,sha256=3cSnT_xfCul6H60imXWe7VlUXG29OzzoAFknr7Fc3TQ,131
12
12
  parsl/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  parsl/app/app.py,sha256=0gbM4AH2OtFOLsv07I5nglpElcwMSOi-FzdZZfrk7So,8532
14
14
  parsl/app/bash.py,sha256=jm2AvePlCT9DZR7H_4ANDWxatp5dN_22FUlT_gWhZ-g,5528
@@ -73,14 +73,14 @@ parsl/executors/flux/executor.py,sha256=8_xakLUu5zNJAHL0LbeTCFEWqWzRK1eE-3ep4GII
73
73
  parsl/executors/flux/flux_instance_manager.py,sha256=5T3Rp7ZM-mlT0Pf0Gxgs5_YmnaPrSF9ec7zvRfLfYJw,2129
74
74
  parsl/executors/high_throughput/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
75
  parsl/executors/high_throughput/errors.py,sha256=k2XuvvFdUfNs2foHFnxmS-BToRMfdXpYEa4EF3ELKq4,1554
76
- parsl/executors/high_throughput/executor.py,sha256=Otf0k9Ia_ZX9mFBs6HOfF4x1LALlwG-i_ga_RsrUBJY,38747
76
+ parsl/executors/high_throughput/executor.py,sha256=esMYMgPHmgD0wPTb0U61vBX96DaPrWj9sQLzpKvB06k,38752
77
77
  parsl/executors/high_throughput/interchange.py,sha256=7sKIvxP3a7HSzqEq25ZCpABx-1Q2f585pFDGzUvo7_4,29459
78
78
  parsl/executors/high_throughput/manager_record.py,sha256=ZMsqFxvreGLRXAw3N-JnODDa9Qfizw2tMmcBhm4lco4,490
79
79
  parsl/executors/high_throughput/manager_selector.py,sha256=UKcUE6v0tO7PDMTThpKSKxVpOpOUilxDL7UbNgpZCxo,2116
80
80
  parsl/executors/high_throughput/monitoring_info.py,sha256=HC0drp6nlXQpAop5PTUKNjdXMgtZVvrBL0JzZJebPP4,298
81
81
  parsl/executors/high_throughput/mpi_executor.py,sha256=U-aatbLF_Mu1p6lP0HmT7Yn1Swn3cc7hPmDfuUb9TpI,4797
82
82
  parsl/executors/high_throughput/mpi_prefix_composer.py,sha256=DmpKugANNa1bdYlqQBLHkrFc15fJpefPPhW9hkAlh1s,4308
83
- parsl/executors/high_throughput/mpi_resource_management.py,sha256=d3NSt3-isdr7pj-oXg9XGRX9D9VsK5e9zSpp7-nyybc,7854
83
+ parsl/executors/high_throughput/mpi_resource_management.py,sha256=xeJp4h4LysG8KuBLKqy1sgFahL1eqiG7XLpr09VLwy4,8144
84
84
  parsl/executors/high_throughput/probe.py,sha256=QOEaliO3x5cB6ltMOZMsZQ-ath9AAuFqXcBzRgWOM60,2754
85
85
  parsl/executors/high_throughput/process_worker_pool.py,sha256=YOJvTUMg3eIHr9fYfBWFHRiI1QQ898IGiuXyj5VRQNo,41084
86
86
  parsl/executors/high_throughput/zmq_pipes.py,sha256=NUK25IEh0UkxzdqQQyM8tMtuZmjSiTeWu1DzkkAIOhA,8980
@@ -117,19 +117,21 @@ parsl/monitoring/__init__.py,sha256=0ywNz6i0lM1xo_7_BIxhETDGeVd2C_0wwD7qgeaMR4c,
117
117
  parsl/monitoring/db_manager.py,sha256=ra5PqmbUstfDx0o_bkBYI8GIUi461-GV3b4A-Q6DVVE,33300
118
118
  parsl/monitoring/errors.py,sha256=D6jpYzEzp0d6FmVKGqhvjAxr4ztZfJX2s-aXemH9bBU,148
119
119
  parsl/monitoring/message_type.py,sha256=Khn88afNxcOIciKiCK4GLnn90I5BlRTiOL3zK-P07yQ,401
120
- parsl/monitoring/monitoring.py,sha256=fkBZU4fWp7qBQUcKYtWjd4d-SsFlJUZNMZacFOh0IoA,12687
120
+ parsl/monitoring/monitoring.py,sha256=p79F982lyPsplXeTVxqlvNuB8G1p3PAI8nTMHcZJ5UE,13113
121
121
  parsl/monitoring/remote.py,sha256=t0qCTUMCzeJ_JOARFpjqlTNrAWdEb20BxhmZh9X7kEM,13728
122
- parsl/monitoring/router.py,sha256=GUNdvixMcVGITk5LHEfgtbKBE7DqRtQIO-fkR4Z_VYM,9224
123
122
  parsl/monitoring/types.py,sha256=oOCrzv-ab-_rv4pb8o58Sdb8G_RGp1aZriRbdf9zBEk,339
124
123
  parsl/monitoring/queries/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
125
124
  parsl/monitoring/queries/pandas.py,sha256=0Z2r0rjTKCemf0eaDkF1irvVHn5g7KC5SYETvQPRxwU,2232
126
125
  parsl/monitoring/radios/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
127
126
  parsl/monitoring/radios/base.py,sha256=Ep5kHf07Sm-ApMBJVudRhoWRyuiu0udjO4NvEir5LEk,291
128
127
  parsl/monitoring/radios/filesystem.py,sha256=ioZ3jOKX5Qf0DYRtWmpCEorfuMVbS58OMS_QV7DOFOs,1765
128
+ parsl/monitoring/radios/filesystem_router.py,sha256=ZxPImntYHw-3arvCTMYOC67kUgkyk7I7XLDrCXnvkBw,2055
129
129
  parsl/monitoring/radios/htex.py,sha256=qBu4O5NYnSETHX0ptdwxSpqa2Pp3Z_V6a6lb3TbjKm4,1643
130
130
  parsl/monitoring/radios/multiprocessing.py,sha256=fsfaaoMDp6VJv1DSAl-P0R2ofO6jp13byx6NsPItV3Y,655
131
131
  parsl/monitoring/radios/udp.py,sha256=bTpt7JYp-5hyBBLzgiLj1_BlSTn28UVp39OYgVGLXCw,1613
132
+ parsl/monitoring/radios/udp_router.py,sha256=Dtat4lVNz4cpnzZmXTjo5VA1Xcri5VTSNNpyepSjIVE,5868
132
133
  parsl/monitoring/radios/zmq.py,sha256=fhoHp9ylhf-D3eTJb2aSHRsuic8-FJ_oRNGnniGkCAI,592
134
+ parsl/monitoring/radios/zmq_router.py,sha256=oKfMg_dc3UxJcSzDe1ZqkGJYQcOa4somvyGPzwOqQuA,5860
133
135
  parsl/monitoring/visualization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
134
136
  parsl/monitoring/visualization/app.py,sha256=xMeRlAnzl5lHddAOdSBcqY3D5lmOYw3Z3Z2_YyoVwnw,1425
135
137
  parsl/monitoring/visualization/models.py,sha256=C7CcF6w6PhtrdvDX9VgDH-aSrpLfvYU1fJ4-HDUeFVQ,5138
@@ -347,7 +349,7 @@ parsl/tests/test_mpi_apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
347
349
  parsl/tests/test_mpi_apps/test_bad_mpi_config.py,sha256=QKvEUSrHIBrvqu2fRj1MAqxsYxDfcrdQ7dzWdOZejuU,1320
348
350
  parsl/tests/test_mpi_apps/test_mpi_mode_enabled.py,sha256=_fpiaDq9yEUuBxTiuxLFsBt5r1oX9S-3S-YL5yRB13E,5423
349
351
  parsl/tests/test_mpi_apps/test_mpi_prefix.py,sha256=yJslZvYK3JeL9UgxMwF9DDPR9QD4zJLGVjubD0F-utc,1950
350
- parsl/tests/test_mpi_apps/test_mpi_scheduler.py,sha256=YdV8A-m67DHk9wxgNpj69wwGEKrFGL20KAC1TzLke3c,6332
352
+ parsl/tests/test_mpi_apps/test_mpi_scheduler.py,sha256=LPvk5wywYANQNCoQ8muwOLEznnZqwler4jJglinAT9I,7370
351
353
  parsl/tests/test_mpi_apps/test_mpiex.py,sha256=mlFdHK3A1B6NsEhxTQQX8lhs9qVza36FMG99vNrBRW4,2021
352
354
  parsl/tests/test_mpi_apps/test_resource_spec.py,sha256=5k6HM2jtb6sa7jetpI-Tl1nPQiN33VLaM7YT10c307E,3756
353
355
  parsl/tests/test_providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -409,7 +411,7 @@ parsl/tests/test_scaling/test_regression_3696_oscillation.py,sha256=7Xc3vgocXXUb
409
411
  parsl/tests/test_scaling/test_scale_down.py,sha256=vHJOMRUriW6xPtaY8GTKYXd5P0WJkQV6Q1IPui05aLU,2736
410
412
  parsl/tests/test_scaling/test_scale_down_htex_auto_scale.py,sha256=EnVNllKO2AGKkGa6927cLrzvvG6mpNQeFDzVktv6x08,4521
411
413
  parsl/tests/test_scaling/test_scale_down_htex_unregistered.py,sha256=OrdnYmd58n7UfkANPJ7mzha4WSCPdbgJRX1O1Zdu0tI,1954
412
- parsl/tests/test_scaling/test_shutdown_scalein.py,sha256=QMlby0g4SgRUqFYZy-d80a23L8FmYl_dwse67E86oVs,2325
414
+ parsl/tests/test_scaling/test_shutdown_scalein.py,sha256=sr40of5DwxeyQI97MDZxFqJILZSXZJb9Dv7qTf2gql8,2471
413
415
  parsl/tests/test_scaling/test_worker_interchange_bad_messages_3262.py,sha256=GaXmRli1srTal-JQmCGDTP4BAwAKI_daXMmrjULsZkY,2788
414
416
  parsl/tests/test_serialization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
415
417
  parsl/tests/test_serialization/test_2555_caching_deserializer.py,sha256=jEXJvbriaLVI7frV5t-iJRKYyeQ7a9_-t3X9lhhBWQo,767
@@ -420,7 +422,7 @@ parsl/tests/test_serialization/test_pack_resource_spec.py,sha256=-Vtyh8KyezZw8e7
420
422
  parsl/tests/test_serialization/test_proxystore_configured.py,sha256=lGWOSEWul16enDWhW-s7CK0d3eMDzm1324Fmj0cZMVU,2293
421
423
  parsl/tests/test_serialization/test_proxystore_impl.py,sha256=uGd45sfPm9rJhzqKV0rI3lqdSOAUddQf-diEpcJAlcY,1228
422
424
  parsl/tests/test_shutdown/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
423
- parsl/tests/test_shutdown/test_kill_monitoring.py,sha256=S9CnCziBk3sQMKgccqvNUEBHanf1hWMK1SLc2aF8uWs,1906
425
+ parsl/tests/test_shutdown/test_kill_monitoring.py,sha256=BycTDLwxhHbbV68Qkgrmn8UUzSr55SvbNvydp35UCTM,1948
424
426
  parsl/tests/test_staging/__init__.py,sha256=WZl9EHSkfYiSoE3Gbulcq2ifmn7IFGUkasJIobL5T5A,208
425
427
  parsl/tests/test_staging/staging_provider.py,sha256=6FDpImkWOLgysqM68NbCAoXZciZokI8dmBWRAxnggBk,3242
426
428
  parsl/tests/test_staging/test_1316.py,sha256=eS0e2BDM2vmPNF60aDr35wcuGgDPfXjTjRV6kyBZOQc,2652
@@ -455,13 +457,13 @@ parsl/usage_tracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
455
457
  parsl/usage_tracking/api.py,sha256=iaCY58Dc5J4UM7_dJzEEs871P1p1HdxBMtNGyVdzc9g,1821
456
458
  parsl/usage_tracking/levels.py,sha256=xbfzYEsd55KiZJ-mzNgPebvOH4rRHum04hROzEf41tU,291
457
459
  parsl/usage_tracking/usage.py,sha256=f9k6QcpbQxkGyP5WTC9PVyv0CA05s9NDpRe5wwRdBTM,9163
458
- parsl-2025.3.3.data/scripts/exec_parsl_function.py,sha256=YXKVVIa4zXmOtz-0Ca4E_5nQfN_3S2bh2tB75uZZB4w,7774
459
- parsl-2025.3.3.data/scripts/interchange.py,sha256=17MrOc7-FXxKBWTwkzIbUoa8fvvDfPelfjByd3ZD2Wk,29446
460
- parsl-2025.3.3.data/scripts/parsl_coprocess.py,sha256=zrVjEqQvFOHxsLufPi00xzMONagjVwLZbavPM7bbjK4,5722
461
- parsl-2025.3.3.data/scripts/process_worker_pool.py,sha256=BbVJ1PS7ZW2grz0iAPPV0BgJyRMyQ7bbXSzLzWCBkyU,41070
462
- parsl-2025.3.3.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
463
- parsl-2025.3.3.dist-info/METADATA,sha256=FiDSNMMf3JHayeZGotfxx9jG2XcVNwbF50cPkEtLBc8,4026
464
- parsl-2025.3.3.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
465
- parsl-2025.3.3.dist-info/entry_points.txt,sha256=XqnsWDYoEcLbsMcpnYGKLEnSBmaIe1YoM5YsBdJG2tI,176
466
- parsl-2025.3.3.dist-info/top_level.txt,sha256=PIheYoUFQtF2icLsgOykgU-Cjuwr2Oi6On2jo5RYgRM,6
467
- parsl-2025.3.3.dist-info/RECORD,,
460
+ parsl-2025.3.10.data/scripts/exec_parsl_function.py,sha256=YXKVVIa4zXmOtz-0Ca4E_5nQfN_3S2bh2tB75uZZB4w,7774
461
+ parsl-2025.3.10.data/scripts/interchange.py,sha256=17MrOc7-FXxKBWTwkzIbUoa8fvvDfPelfjByd3ZD2Wk,29446
462
+ parsl-2025.3.10.data/scripts/parsl_coprocess.py,sha256=zrVjEqQvFOHxsLufPi00xzMONagjVwLZbavPM7bbjK4,5722
463
+ parsl-2025.3.10.data/scripts/process_worker_pool.py,sha256=BbVJ1PS7ZW2grz0iAPPV0BgJyRMyQ7bbXSzLzWCBkyU,41070
464
+ parsl-2025.3.10.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
465
+ parsl-2025.3.10.dist-info/METADATA,sha256=rEtmq9LYtfBbXFR2JuX9DmmPQyDshdmj0GuakTMQeSM,4027
466
+ parsl-2025.3.10.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
467
+ parsl-2025.3.10.dist-info/entry_points.txt,sha256=XqnsWDYoEcLbsMcpnYGKLEnSBmaIe1YoM5YsBdJG2tI,176
468
+ parsl-2025.3.10.dist-info/top_level.txt,sha256=PIheYoUFQtF2icLsgOykgU-Cjuwr2Oi6On2jo5RYgRM,6
469
+ parsl-2025.3.10.dist-info/RECORD,,