parsl 2025.1.20__py3-none-any.whl → 2025.2.3__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
@@ -28,7 +28,7 @@ from parsl.config import Config
28
28
  from parsl.data_provider.data_manager import DataManager
29
29
  from parsl.data_provider.files import File
30
30
  from parsl.dataflow.dependency_resolvers import SHALLOW_DEPENDENCY_RESOLVER
31
- from parsl.dataflow.errors import BadCheckpoint, DependencyError, JoinError
31
+ from parsl.dataflow.errors import DependencyError, JoinError
32
32
  from parsl.dataflow.futures import AppFuture
33
33
  from parsl.dataflow.memoization import Memoizer
34
34
  from parsl.dataflow.rundirs import make_rundir
@@ -161,13 +161,13 @@ class DataFlowKernel:
161
161
  workflow_info))
162
162
 
163
163
  if config.checkpoint_files is not None:
164
- checkpoints = self.load_checkpoints(config.checkpoint_files)
164
+ checkpoint_files = config.checkpoint_files
165
165
  elif config.checkpoint_files is None and config.checkpoint_mode is not None:
166
- checkpoints = self.load_checkpoints(get_all_checkpoints(self.run_dir))
166
+ checkpoint_files = get_all_checkpoints(self.run_dir)
167
167
  else:
168
- checkpoints = {}
168
+ checkpoint_files = []
169
169
 
170
- self.memoizer = Memoizer(self, memoize=config.app_cache, checkpoint=checkpoints)
170
+ self.memoizer = Memoizer(self, memoize=config.app_cache, checkpoint_files=checkpoint_files)
171
171
  self.checkpointed_tasks = 0
172
172
  self._checkpoint_timer = None
173
173
  self.checkpoint_mode = config.checkpoint_mode
@@ -1263,7 +1263,7 @@ class DataFlowKernel:
1263
1263
  Returns:
1264
1264
  Checkpoint dir if checkpoints were written successfully.
1265
1265
  By default the checkpoints are written to the RUNDIR of the current
1266
- run under RUNDIR/checkpoints/{tasks.pkl, dfk.pkl}
1266
+ run under RUNDIR/checkpoints/tasks.pkl
1267
1267
  """
1268
1268
  with self.checkpoint_lock:
1269
1269
  if tasks:
@@ -1273,18 +1273,11 @@ class DataFlowKernel:
1273
1273
  self.checkpointable_tasks = []
1274
1274
 
1275
1275
  checkpoint_dir = '{0}/checkpoint'.format(self.run_dir)
1276
- checkpoint_dfk = checkpoint_dir + '/dfk.pkl'
1277
1276
  checkpoint_tasks = checkpoint_dir + '/tasks.pkl'
1278
1277
 
1279
1278
  if not os.path.exists(checkpoint_dir):
1280
1279
  os.makedirs(checkpoint_dir, exist_ok=True)
1281
1280
 
1282
- with open(checkpoint_dfk, 'wb') as f:
1283
- state = {'rundir': self.run_dir,
1284
- 'task_count': self.task_count
1285
- }
1286
- pickle.dump(state, f)
1287
-
1288
1281
  count = 0
1289
1282
 
1290
1283
  with open(checkpoint_tasks, 'ab') as f:
@@ -1317,74 +1310,6 @@ class DataFlowKernel:
1317
1310
 
1318
1311
  return checkpoint_dir
1319
1312
 
1320
- def _load_checkpoints(self, checkpointDirs: Sequence[str]) -> Dict[str, Future[Any]]:
1321
- """Load a checkpoint file into a lookup table.
1322
-
1323
- The data being loaded from the pickle file mostly contains input
1324
- attributes of the task: func, args, kwargs, env...
1325
- To simplify the check of whether the exact task has been completed
1326
- in the checkpoint, we hash these input params and use it as the key
1327
- for the memoized lookup table.
1328
-
1329
- Args:
1330
- - checkpointDirs (list) : List of filepaths to checkpoints
1331
- Eg. ['runinfo/001', 'runinfo/002']
1332
-
1333
- Returns:
1334
- - memoized_lookup_table (dict)
1335
- """
1336
- memo_lookup_table = {}
1337
-
1338
- for checkpoint_dir in checkpointDirs:
1339
- logger.info("Loading checkpoints from {}".format(checkpoint_dir))
1340
- checkpoint_file = os.path.join(checkpoint_dir, 'tasks.pkl')
1341
- try:
1342
- with open(checkpoint_file, 'rb') as f:
1343
- while True:
1344
- try:
1345
- data = pickle.load(f)
1346
- # Copy and hash only the input attributes
1347
- memo_fu: Future = Future()
1348
- assert data['exception'] is None
1349
- memo_fu.set_result(data['result'])
1350
- memo_lookup_table[data['hash']] = memo_fu
1351
-
1352
- except EOFError:
1353
- # Done with the checkpoint file
1354
- break
1355
- except FileNotFoundError:
1356
- reason = "Checkpoint file was not found: {}".format(
1357
- checkpoint_file)
1358
- logger.error(reason)
1359
- raise BadCheckpoint(reason)
1360
- except Exception:
1361
- reason = "Failed to load checkpoint: {}".format(
1362
- checkpoint_file)
1363
- logger.error(reason)
1364
- raise BadCheckpoint(reason)
1365
-
1366
- logger.info("Completed loading checkpoint: {0} with {1} tasks".format(checkpoint_file,
1367
- len(memo_lookup_table.keys())))
1368
- return memo_lookup_table
1369
-
1370
- @typeguard.typechecked
1371
- def load_checkpoints(self, checkpointDirs: Optional[Sequence[str]]) -> Dict[str, Future]:
1372
- """Load checkpoints from the checkpoint files into a dictionary.
1373
-
1374
- The results are used to pre-populate the memoizer's lookup_table
1375
-
1376
- Kwargs:
1377
- - checkpointDirs (list) : List of run folder to use as checkpoints
1378
- Eg. ['runinfo/001', 'runinfo/002']
1379
-
1380
- Returns:
1381
- - dict containing, hashed -> future mappings
1382
- """
1383
- if checkpointDirs:
1384
- return self._load_checkpoints(checkpointDirs)
1385
- else:
1386
- return {}
1387
-
1388
1313
  @staticmethod
1389
1314
  def _log_std_streams(task_record: TaskRecord) -> None:
1390
1315
  tid = task_record['id']
parsl/dataflow/errors.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import Optional, Sequence, Tuple
1
+ from typing import List, Sequence, Tuple
2
2
 
3
3
  from parsl.errors import ParslError
4
4
 
@@ -29,35 +29,77 @@ class BadCheckpoint(DataFlowException):
29
29
  return self.reason
30
30
 
31
31
 
32
- class DependencyError(DataFlowException):
33
- """Error raised if an app cannot run because there was an error
34
- in a dependency.
32
+ class PropagatedException(DataFlowException):
33
+ """Error raised if an app fails because there was an error
34
+ in a related task. This is intended to be subclassed for
35
+ dependency and join_app errors.
35
36
 
36
37
  Args:
37
- - dependent_exceptions_tids: List of exceptions and identifiers for
38
- dependencies which failed. The identifier might be a task ID or
39
- the repr of a non-DFK Future.
38
+ - dependent_exceptions_tids: List of exceptions and brief descriptions
39
+ for dependencies which failed. The description might be a task ID or
40
+ the repr of a non-AppFuture.
40
41
  - task_id: Task ID of the task that failed because of the dependency error
41
42
  """
42
43
 
43
- def __init__(self, dependent_exceptions_tids: Sequence[Tuple[Exception, str]], task_id: int) -> None:
44
+ def __init__(self,
45
+ dependent_exceptions_tids: Sequence[Tuple[BaseException, str]],
46
+ task_id: int,
47
+ *,
48
+ failure_description: str) -> None:
44
49
  self.dependent_exceptions_tids = dependent_exceptions_tids
45
50
  self.task_id = task_id
51
+ self._failure_description = failure_description
52
+
53
+ (cause, cause_sequence) = self._find_any_root_cause()
54
+ self.__cause__ = cause
55
+ self._cause_sequence = cause_sequence
46
56
 
47
57
  def __str__(self) -> str:
48
- deps = ", ".join(tid for _exc, tid in self.dependent_exceptions_tids)
49
- return f"Dependency failure for task {self.task_id} with failed dependencies from {deps}"
58
+ sequence_text = " <- ".join(self._cause_sequence)
59
+ return f"{self._failure_description} for task {self.task_id}. " \
60
+ f"The representative cause is via {sequence_text}"
61
+
62
+ def _find_any_root_cause(self) -> Tuple[BaseException, List[str]]:
63
+ """Looks recursively through self.dependent_exceptions_tids to find
64
+ an exception that caused this propagated error, that is not itself
65
+ a propagated error.
66
+ """
67
+ e: BaseException = self
68
+ dep_ids = []
69
+ while isinstance(e, PropagatedException) and len(e.dependent_exceptions_tids) >= 1:
70
+ id_txt = e.dependent_exceptions_tids[0][1]
71
+ assert isinstance(id_txt, str)
72
+ # if there are several causes for this exception, label that
73
+ # there are more so that we know that the representative fail
74
+ # sequence is not the full story.
75
+ if len(e.dependent_exceptions_tids) > 1:
76
+ id_txt += " (+ others)"
77
+ dep_ids.append(id_txt)
78
+ e = e.dependent_exceptions_tids[0][0]
79
+ return e, dep_ids
80
+
81
+
82
+ class DependencyError(PropagatedException):
83
+ """Error raised if an app cannot run because there was an error
84
+ in a dependency. There can be several exceptions (one from each
85
+ dependency) and DependencyError collects them all together.
50
86
 
87
+ Args:
88
+ - dependent_exceptions_tids: List of exceptions and brief descriptions
89
+ for dependencies which failed. The description might be a task ID or
90
+ the repr of a non-AppFuture.
91
+ - task_id: Task ID of the task that failed because of the dependency error
92
+ """
93
+ def __init__(self, dependent_exceptions_tids: Sequence[Tuple[BaseException, str]], task_id: int) -> None:
94
+ super().__init__(dependent_exceptions_tids, task_id,
95
+ failure_description="Dependency failure")
51
96
 
52
- class JoinError(DataFlowException):
97
+
98
+ class JoinError(PropagatedException):
53
99
  """Error raised if apps joining into a join_app raise exceptions.
54
100
  There can be several exceptions (one from each joining app),
55
101
  and JoinError collects them all together.
56
102
  """
57
- def __init__(self, dependent_exceptions_tids: Sequence[Tuple[BaseException, Optional[str]]], task_id: int) -> None:
58
- self.dependent_exceptions_tids = dependent_exceptions_tids
59
- self.task_id = task_id
60
-
61
- def __str__(self) -> str:
62
- dep_tids = [tid for (exception, tid) in self.dependent_exceptions_tids]
63
- return "Join failure for task {} with failed join dependencies from tasks {}".format(self.task_id, dep_tids)
103
+ def __init__(self, dependent_exceptions_tids: Sequence[Tuple[BaseException, str]], task_id: int) -> None:
104
+ super().__init__(dependent_exceptions_tids, task_id,
105
+ failure_description="Join failure")
@@ -2,10 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  import hashlib
4
4
  import logging
5
+ import os
5
6
  import pickle
6
7
  from functools import lru_cache, singledispatch
7
- from typing import TYPE_CHECKING, Any, Dict, List, Optional
8
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence
8
9
 
10
+ import typeguard
11
+
12
+ from parsl.dataflow.errors import BadCheckpoint
9
13
  from parsl.dataflow.taskrecord import TaskRecord
10
14
 
11
15
  if TYPE_CHECKING:
@@ -146,7 +150,7 @@ class Memoizer:
146
150
 
147
151
  """
148
152
 
149
- def __init__(self, dfk: DataFlowKernel, memoize: bool = True, checkpoint: Dict[str, Future[Any]] = {}):
153
+ def __init__(self, dfk: DataFlowKernel, *, memoize: bool = True, checkpoint_files: Sequence[str]):
150
154
  """Initialize the memoizer.
151
155
 
152
156
  Args:
@@ -159,6 +163,8 @@ class Memoizer:
159
163
  self.dfk = dfk
160
164
  self.memoize = memoize
161
165
 
166
+ checkpoint = self.load_checkpoints(checkpoint_files)
167
+
162
168
  if self.memoize:
163
169
  logger.info("App caching initialized")
164
170
  self.memo_lookup_table = checkpoint
@@ -274,3 +280,71 @@ class Memoizer:
274
280
  else:
275
281
  logger.debug(f"Storing app cache entry {task['hashsum']} with result from task {task_id}")
276
282
  self.memo_lookup_table[task['hashsum']] = r
283
+
284
+ def _load_checkpoints(self, checkpointDirs: Sequence[str]) -> Dict[str, Future[Any]]:
285
+ """Load a checkpoint file into a lookup table.
286
+
287
+ The data being loaded from the pickle file mostly contains input
288
+ attributes of the task: func, args, kwargs, env...
289
+ To simplify the check of whether the exact task has been completed
290
+ in the checkpoint, we hash these input params and use it as the key
291
+ for the memoized lookup table.
292
+
293
+ Args:
294
+ - checkpointDirs (list) : List of filepaths to checkpoints
295
+ Eg. ['runinfo/001', 'runinfo/002']
296
+
297
+ Returns:
298
+ - memoized_lookup_table (dict)
299
+ """
300
+ memo_lookup_table = {}
301
+
302
+ for checkpoint_dir in checkpointDirs:
303
+ logger.info("Loading checkpoints from {}".format(checkpoint_dir))
304
+ checkpoint_file = os.path.join(checkpoint_dir, 'tasks.pkl')
305
+ try:
306
+ with open(checkpoint_file, 'rb') as f:
307
+ while True:
308
+ try:
309
+ data = pickle.load(f)
310
+ # Copy and hash only the input attributes
311
+ memo_fu: Future = Future()
312
+ assert data['exception'] is None
313
+ memo_fu.set_result(data['result'])
314
+ memo_lookup_table[data['hash']] = memo_fu
315
+
316
+ except EOFError:
317
+ # Done with the checkpoint file
318
+ break
319
+ except FileNotFoundError:
320
+ reason = "Checkpoint file was not found: {}".format(
321
+ checkpoint_file)
322
+ logger.error(reason)
323
+ raise BadCheckpoint(reason)
324
+ except Exception:
325
+ reason = "Failed to load checkpoint: {}".format(
326
+ checkpoint_file)
327
+ logger.error(reason)
328
+ raise BadCheckpoint(reason)
329
+
330
+ logger.info("Completed loading checkpoint: {0} with {1} tasks".format(checkpoint_file,
331
+ len(memo_lookup_table.keys())))
332
+ return memo_lookup_table
333
+
334
+ @typeguard.typechecked
335
+ def load_checkpoints(self, checkpointDirs: Optional[Sequence[str]]) -> Dict[str, Future]:
336
+ """Load checkpoints from the checkpoint files into a dictionary.
337
+
338
+ The results are used to pre-populate the memoizer's lookup_table
339
+
340
+ Kwargs:
341
+ - checkpointDirs (list) : List of run folder to use as checkpoints
342
+ Eg. ['runinfo/001', 'runinfo/002']
343
+
344
+ Returns:
345
+ - dict containing, hashed -> future mappings
346
+ """
347
+ if checkpointDirs:
348
+ return self._load_checkpoints(checkpointDirs)
349
+ else:
350
+ return {}
@@ -431,8 +431,6 @@ class HighThroughputExecutor(BlockProviderExecutor, RepresentationMixin, UsageIn
431
431
  self._start_result_queue_thread()
432
432
  self._start_local_interchange_process()
433
433
 
434
- logger.debug("Created result queue thread: %s", self._result_queue_thread)
435
-
436
434
  self.initialize_scaling()
437
435
 
438
436
  @wrap_with_logs
@@ -529,6 +527,8 @@ class HighThroughputExecutor(BlockProviderExecutor, RepresentationMixin, UsageIn
529
527
  get the worker task and result ports that the interchange has bound to.
530
528
  """
531
529
 
530
+ assert self.interchange_proc is None, f"Already exists! {self.interchange_proc!r}"
531
+
532
532
  interchange_config = {"client_address": self.loopback_address,
533
533
  "client_ports": (self.outgoing_q.port,
534
534
  self.incoming_q.port,
@@ -563,7 +563,12 @@ class HighThroughputExecutor(BlockProviderExecutor, RepresentationMixin, UsageIn
563
563
  except CommandClientTimeoutError:
564
564
  logger.error("Interchange has not completed initialization. Aborting")
565
565
  raise Exception("Interchange failed to start")
566
- logger.debug("Got worker ports")
566
+ logger.debug(
567
+ "Interchange process started (%r). Worker ports: %d, %d",
568
+ self.interchange_proc,
569
+ self.worker_task_port,
570
+ self.worker_result_port
571
+ )
567
572
 
568
573
  def _start_result_queue_thread(self):
569
574
  """Method to start the result queue thread as a daemon.
@@ -571,15 +576,13 @@ class HighThroughputExecutor(BlockProviderExecutor, RepresentationMixin, UsageIn
571
576
  Checks if a thread already exists, then starts it.
572
577
  Could be used later as a restart if the result queue thread dies.
573
578
  """
574
- if self._result_queue_thread is None:
575
- logger.debug("Starting result queue thread")
576
- self._result_queue_thread = threading.Thread(target=self._result_queue_worker, name="HTEX-Result-Queue-Thread")
577
- self._result_queue_thread.daemon = True
578
- self._result_queue_thread.start()
579
- logger.debug("Started result queue thread")
579
+ assert self._result_queue_thread is None, f"Already exists! {self._result_queue_thread!r}"
580
580
 
581
- else:
582
- logger.error("Result queue thread already exists, returning")
581
+ logger.debug("Starting result queue thread")
582
+ self._result_queue_thread = threading.Thread(target=self._result_queue_worker, name="HTEX-Result-Queue-Thread")
583
+ self._result_queue_thread.daemon = True
584
+ self._result_queue_thread.start()
585
+ logger.debug("Started result queue thread: %r", self._result_queue_thread)
583
586
 
584
587
  def hold_worker(self, worker_id: str) -> None:
585
588
  """Puts a worker on hold, preventing scheduling of additional tasks to it.
@@ -9,7 +9,7 @@ import queue
9
9
  import sys
10
10
  import threading
11
11
  import time
12
- from typing import Any, Dict, List, NoReturn, Optional, Sequence, Set, Tuple, cast
12
+ from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, cast
13
13
 
14
14
  import zmq
15
15
 
@@ -132,6 +132,11 @@ class Interchange:
132
132
  self.hub_zmq_port = hub_zmq_port
133
133
 
134
134
  self.pending_task_queue: queue.Queue[Any] = queue.Queue(maxsize=10 ** 6)
135
+
136
+ # count of tasks that have been received from the submit side
137
+ self.task_counter = 0
138
+
139
+ # count of tasks that have been sent out to worker pools
135
140
  self.count = 0
136
141
 
137
142
  self.worker_ports = worker_ports
@@ -201,28 +206,6 @@ class Interchange:
201
206
 
202
207
  return tasks
203
208
 
204
- @wrap_with_logs(target="interchange")
205
- def task_puller(self) -> NoReturn:
206
- """Pull tasks from the incoming tasks zmq pipe onto the internal
207
- pending task queue
208
- """
209
- logger.info("Starting")
210
- task_counter = 0
211
-
212
- while True:
213
- logger.debug("launching recv_pyobj")
214
- try:
215
- msg = self.task_incoming.recv_pyobj()
216
- except zmq.Again:
217
- # We just timed out while attempting to receive
218
- logger.debug("zmq.Again with {} tasks in internal queue".format(self.pending_task_queue.qsize()))
219
- continue
220
-
221
- logger.debug("putting message onto pending_task_queue")
222
- self.pending_task_queue.put(msg)
223
- task_counter += 1
224
- logger.debug(f"Fetched {task_counter} tasks so far")
225
-
226
209
  def _send_monitoring_info(self, monitoring_radio: Optional[MonitoringRadioSender], manager: ManagerRecord) -> None:
227
210
  if monitoring_radio:
228
211
  logger.info("Sending message {} to MonitoringHub".format(manager))
@@ -234,79 +217,68 @@ class Interchange:
234
217
 
235
218
  monitoring_radio.send((MessageType.NODE_INFO, d))
236
219
 
237
- @wrap_with_logs(target="interchange")
238
- def _command_server(self) -> NoReturn:
220
+ def process_command(self, monitoring_radio: Optional[MonitoringRadioSender]) -> None:
239
221
  """ Command server to run async command to the interchange
240
222
  """
241
- logger.debug("Command Server Starting")
242
-
243
- if self.hub_address is not None and self.hub_zmq_port is not None:
244
- logger.debug("Creating monitoring radio to %s:%s", self.hub_address, self.hub_zmq_port)
245
- monitoring_radio = ZMQRadioSender(self.hub_address, self.hub_zmq_port)
246
- else:
247
- monitoring_radio = None
223
+ logger.debug("entering command_server section")
248
224
 
249
225
  reply: Any # the type of reply depends on the command_req received (aka this needs dependent types...)
250
226
 
251
- while True:
252
- try:
253
- command_req = self.command_channel.recv_pyobj()
254
- logger.debug("Received command request: {}".format(command_req))
255
- if command_req == "CONNECTED_BLOCKS":
256
- reply = self.connected_block_history
257
-
258
- elif command_req == "WORKERS":
259
- num_workers = 0
260
- for manager in self._ready_managers.values():
261
- num_workers += manager['worker_count']
262
- reply = num_workers
263
-
264
- elif command_req == "MANAGERS":
265
- reply = []
266
- for manager_id in self._ready_managers:
267
- m = self._ready_managers[manager_id]
268
- idle_since = m['idle_since']
269
- if idle_since is not None:
270
- idle_duration = time.time() - idle_since
271
- else:
272
- idle_duration = 0.0
273
- resp = {'manager': manager_id.decode('utf-8'),
274
- 'block_id': m['block_id'],
275
- 'worker_count': m['worker_count'],
276
- 'tasks': len(m['tasks']),
277
- 'idle_duration': idle_duration,
278
- 'active': m['active'],
279
- 'parsl_version': m['parsl_version'],
280
- 'python_version': m['python_version'],
281
- 'draining': m['draining']}
282
- reply.append(resp)
283
-
284
- elif command_req.startswith("HOLD_WORKER"):
285
- cmd, s_manager = command_req.split(';')
286
- manager_id = s_manager.encode('utf-8')
287
- logger.info("Received HOLD_WORKER for {!r}".format(manager_id))
288
- if manager_id in self._ready_managers:
289
- m = self._ready_managers[manager_id]
290
- m['active'] = False
291
- self._send_monitoring_info(monitoring_radio, m)
227
+ if self.command_channel in self.socks and self.socks[self.command_channel] == zmq.POLLIN:
228
+
229
+ command_req = self.command_channel.recv_pyobj()
230
+ logger.debug("Received command request: {}".format(command_req))
231
+ if command_req == "CONNECTED_BLOCKS":
232
+ reply = self.connected_block_history
233
+
234
+ elif command_req == "WORKERS":
235
+ num_workers = 0
236
+ for manager in self._ready_managers.values():
237
+ num_workers += manager['worker_count']
238
+ reply = num_workers
239
+
240
+ elif command_req == "MANAGERS":
241
+ reply = []
242
+ for manager_id in self._ready_managers:
243
+ m = self._ready_managers[manager_id]
244
+ idle_since = m['idle_since']
245
+ if idle_since is not None:
246
+ idle_duration = time.time() - idle_since
292
247
  else:
293
- logger.warning("Worker to hold was not in ready managers list")
294
-
295
- reply = None
248
+ idle_duration = 0.0
249
+ resp = {'manager': manager_id.decode('utf-8'),
250
+ 'block_id': m['block_id'],
251
+ 'worker_count': m['worker_count'],
252
+ 'tasks': len(m['tasks']),
253
+ 'idle_duration': idle_duration,
254
+ 'active': m['active'],
255
+ 'parsl_version': m['parsl_version'],
256
+ 'python_version': m['python_version'],
257
+ 'draining': m['draining']}
258
+ reply.append(resp)
259
+
260
+ elif command_req.startswith("HOLD_WORKER"):
261
+ cmd, s_manager = command_req.split(';')
262
+ manager_id = s_manager.encode('utf-8')
263
+ logger.info("Received HOLD_WORKER for {!r}".format(manager_id))
264
+ if manager_id in self._ready_managers:
265
+ m = self._ready_managers[manager_id]
266
+ m['active'] = False
267
+ self._send_monitoring_info(monitoring_radio, m)
268
+ else:
269
+ logger.warning("Worker to hold was not in ready managers list")
296
270
 
297
- elif command_req == "WORKER_PORTS":
298
- reply = (self.worker_task_port, self.worker_result_port)
271
+ reply = None
299
272
 
300
- else:
301
- logger.error(f"Received unknown command: {command_req}")
302
- reply = None
273
+ elif command_req == "WORKER_PORTS":
274
+ reply = (self.worker_task_port, self.worker_result_port)
303
275
 
304
- logger.debug("Reply: {}".format(reply))
305
- self.command_channel.send_pyobj(reply)
276
+ else:
277
+ logger.error(f"Received unknown command: {command_req}")
278
+ reply = None
306
279
 
307
- except zmq.Again:
308
- logger.debug("Command thread is alive")
309
- continue
280
+ logger.debug("Reply: {}".format(reply))
281
+ self.command_channel.send_pyobj(reply)
310
282
 
311
283
  @wrap_with_logs
312
284
  def start(self) -> None:
@@ -326,21 +298,13 @@ class Interchange:
326
298
 
327
299
  start = time.time()
328
300
 
329
- self._task_puller_thread = threading.Thread(target=self.task_puller,
330
- name="Interchange-Task-Puller",
331
- daemon=True)
332
- self._task_puller_thread.start()
333
-
334
- self._command_thread = threading.Thread(target=self._command_server,
335
- name="Interchange-Command",
336
- daemon=True)
337
- self._command_thread.start()
338
-
339
301
  kill_event = threading.Event()
340
302
 
341
303
  poller = zmq.Poller()
342
304
  poller.register(self.task_outgoing, zmq.POLLIN)
343
305
  poller.register(self.results_incoming, zmq.POLLIN)
306
+ poller.register(self.task_incoming, zmq.POLLIN)
307
+ poller.register(self.command_channel, zmq.POLLIN)
344
308
 
345
309
  # These are managers which we should examine in an iteration
346
310
  # for scheduling a job (or maybe any other attention?).
@@ -351,6 +315,8 @@ class Interchange:
351
315
  while not kill_event.is_set():
352
316
  self.socks = dict(poller.poll(timeout=poll_period))
353
317
 
318
+ self.process_command(monitoring_radio)
319
+ self.process_task_incoming()
354
320
  self.process_task_outgoing_incoming(interesting_managers, monitoring_radio, kill_event)
355
321
  self.process_results_incoming(interesting_managers, monitoring_radio)
356
322
  self.expire_bad_managers(interesting_managers, monitoring_radio)
@@ -362,6 +328,18 @@ class Interchange:
362
328
  logger.info(f"Processed {self.count} tasks in {delta} seconds")
363
329
  logger.warning("Exiting")
364
330
 
331
+ def process_task_incoming(self) -> None:
332
+ """Process incoming task message(s).
333
+ """
334
+
335
+ if self.task_incoming in self.socks and self.socks[self.task_incoming] == zmq.POLLIN:
336
+ logger.debug("start task_incoming section")
337
+ msg = self.task_incoming.recv_pyobj()
338
+ logger.debug("putting message onto pending_task_queue")
339
+ self.pending_task_queue.put(msg)
340
+ self.task_counter += 1
341
+ logger.debug(f"Fetched {self.task_counter} tasks so far")
342
+
365
343
  def process_task_outgoing_incoming(
366
344
  self,
367
345
  interesting_managers: Set[bytes],
@@ -27,8 +27,5 @@ def test_initial_checkpoint_write():
27
27
 
28
28
  cpt_dir = parsl.dfk().checkpoint()
29
29
 
30
- cptpath = cpt_dir + '/dfk.pkl'
31
- assert os.path.exists(cptpath), f"DFK checkpoint missing: {cptpath}"
32
-
33
30
  cptpath = cpt_dir + '/tasks.pkl'
34
31
  assert os.path.exists(cptpath), f"Tasks checkpoint missing: {cptpath}"
@@ -39,6 +39,9 @@ def test_fail_sequence_first():
39
39
  assert isinstance(t_final.exception().dependent_exceptions_tids[0][0], DependencyError)
40
40
  assert t_final.exception().dependent_exceptions_tids[0][1].startswith("task ")
41
41
 
42
+ assert hasattr(t_final.exception(), '__cause__')
43
+ assert t_final.exception().__cause__ == t1.exception()
44
+
42
45
 
43
46
  def test_fail_sequence_middle():
44
47
  t1 = random_fail(fail_prob=0)
@@ -50,3 +53,6 @@ def test_fail_sequence_middle():
50
53
 
51
54
  assert len(t_final.exception().dependent_exceptions_tids) == 1
52
55
  assert isinstance(t_final.exception().dependent_exceptions_tids[0][0], ManufacturedTestFailure)
56
+
57
+ assert hasattr(t_final.exception(), '__cause__')
58
+ assert t_final.exception().__cause__ == t2.exception()
@@ -2,7 +2,6 @@ import argparse
2
2
 
3
3
  import parsl
4
4
  from parsl.app.app import python_app
5
- from parsl.tests.configs.local_threads import config
6
5
 
7
6
 
8
7
  @python_app(cache=True)
@@ -12,8 +11,7 @@ def random_uuid(x, cache=True):
12
11
 
13
12
 
14
13
  def test_python_memoization(n=2):
15
- """Testing python memoization disable
16
- """
14
+ """Testing python memoization."""
17
15
  x = random_uuid(0)
18
16
  print(x.result())
19
17
 
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.01.20'
6
+ VERSION = '2025.02.03'
@@ -9,7 +9,7 @@ import queue
9
9
  import sys
10
10
  import threading
11
11
  import time
12
- from typing import Any, Dict, List, NoReturn, Optional, Sequence, Set, Tuple, cast
12
+ from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, cast
13
13
 
14
14
  import zmq
15
15
 
@@ -132,6 +132,11 @@ class Interchange:
132
132
  self.hub_zmq_port = hub_zmq_port
133
133
 
134
134
  self.pending_task_queue: queue.Queue[Any] = queue.Queue(maxsize=10 ** 6)
135
+
136
+ # count of tasks that have been received from the submit side
137
+ self.task_counter = 0
138
+
139
+ # count of tasks that have been sent out to worker pools
135
140
  self.count = 0
136
141
 
137
142
  self.worker_ports = worker_ports
@@ -201,28 +206,6 @@ class Interchange:
201
206
 
202
207
  return tasks
203
208
 
204
- @wrap_with_logs(target="interchange")
205
- def task_puller(self) -> NoReturn:
206
- """Pull tasks from the incoming tasks zmq pipe onto the internal
207
- pending task queue
208
- """
209
- logger.info("Starting")
210
- task_counter = 0
211
-
212
- while True:
213
- logger.debug("launching recv_pyobj")
214
- try:
215
- msg = self.task_incoming.recv_pyobj()
216
- except zmq.Again:
217
- # We just timed out while attempting to receive
218
- logger.debug("zmq.Again with {} tasks in internal queue".format(self.pending_task_queue.qsize()))
219
- continue
220
-
221
- logger.debug("putting message onto pending_task_queue")
222
- self.pending_task_queue.put(msg)
223
- task_counter += 1
224
- logger.debug(f"Fetched {task_counter} tasks so far")
225
-
226
209
  def _send_monitoring_info(self, monitoring_radio: Optional[MonitoringRadioSender], manager: ManagerRecord) -> None:
227
210
  if monitoring_radio:
228
211
  logger.info("Sending message {} to MonitoringHub".format(manager))
@@ -234,79 +217,68 @@ class Interchange:
234
217
 
235
218
  monitoring_radio.send((MessageType.NODE_INFO, d))
236
219
 
237
- @wrap_with_logs(target="interchange")
238
- def _command_server(self) -> NoReturn:
220
+ def process_command(self, monitoring_radio: Optional[MonitoringRadioSender]) -> None:
239
221
  """ Command server to run async command to the interchange
240
222
  """
241
- logger.debug("Command Server Starting")
242
-
243
- if self.hub_address is not None and self.hub_zmq_port is not None:
244
- logger.debug("Creating monitoring radio to %s:%s", self.hub_address, self.hub_zmq_port)
245
- monitoring_radio = ZMQRadioSender(self.hub_address, self.hub_zmq_port)
246
- else:
247
- monitoring_radio = None
223
+ logger.debug("entering command_server section")
248
224
 
249
225
  reply: Any # the type of reply depends on the command_req received (aka this needs dependent types...)
250
226
 
251
- while True:
252
- try:
253
- command_req = self.command_channel.recv_pyobj()
254
- logger.debug("Received command request: {}".format(command_req))
255
- if command_req == "CONNECTED_BLOCKS":
256
- reply = self.connected_block_history
257
-
258
- elif command_req == "WORKERS":
259
- num_workers = 0
260
- for manager in self._ready_managers.values():
261
- num_workers += manager['worker_count']
262
- reply = num_workers
263
-
264
- elif command_req == "MANAGERS":
265
- reply = []
266
- for manager_id in self._ready_managers:
267
- m = self._ready_managers[manager_id]
268
- idle_since = m['idle_since']
269
- if idle_since is not None:
270
- idle_duration = time.time() - idle_since
271
- else:
272
- idle_duration = 0.0
273
- resp = {'manager': manager_id.decode('utf-8'),
274
- 'block_id': m['block_id'],
275
- 'worker_count': m['worker_count'],
276
- 'tasks': len(m['tasks']),
277
- 'idle_duration': idle_duration,
278
- 'active': m['active'],
279
- 'parsl_version': m['parsl_version'],
280
- 'python_version': m['python_version'],
281
- 'draining': m['draining']}
282
- reply.append(resp)
283
-
284
- elif command_req.startswith("HOLD_WORKER"):
285
- cmd, s_manager = command_req.split(';')
286
- manager_id = s_manager.encode('utf-8')
287
- logger.info("Received HOLD_WORKER for {!r}".format(manager_id))
288
- if manager_id in self._ready_managers:
289
- m = self._ready_managers[manager_id]
290
- m['active'] = False
291
- self._send_monitoring_info(monitoring_radio, m)
227
+ if self.command_channel in self.socks and self.socks[self.command_channel] == zmq.POLLIN:
228
+
229
+ command_req = self.command_channel.recv_pyobj()
230
+ logger.debug("Received command request: {}".format(command_req))
231
+ if command_req == "CONNECTED_BLOCKS":
232
+ reply = self.connected_block_history
233
+
234
+ elif command_req == "WORKERS":
235
+ num_workers = 0
236
+ for manager in self._ready_managers.values():
237
+ num_workers += manager['worker_count']
238
+ reply = num_workers
239
+
240
+ elif command_req == "MANAGERS":
241
+ reply = []
242
+ for manager_id in self._ready_managers:
243
+ m = self._ready_managers[manager_id]
244
+ idle_since = m['idle_since']
245
+ if idle_since is not None:
246
+ idle_duration = time.time() - idle_since
292
247
  else:
293
- logger.warning("Worker to hold was not in ready managers list")
294
-
295
- reply = None
248
+ idle_duration = 0.0
249
+ resp = {'manager': manager_id.decode('utf-8'),
250
+ 'block_id': m['block_id'],
251
+ 'worker_count': m['worker_count'],
252
+ 'tasks': len(m['tasks']),
253
+ 'idle_duration': idle_duration,
254
+ 'active': m['active'],
255
+ 'parsl_version': m['parsl_version'],
256
+ 'python_version': m['python_version'],
257
+ 'draining': m['draining']}
258
+ reply.append(resp)
259
+
260
+ elif command_req.startswith("HOLD_WORKER"):
261
+ cmd, s_manager = command_req.split(';')
262
+ manager_id = s_manager.encode('utf-8')
263
+ logger.info("Received HOLD_WORKER for {!r}".format(manager_id))
264
+ if manager_id in self._ready_managers:
265
+ m = self._ready_managers[manager_id]
266
+ m['active'] = False
267
+ self._send_monitoring_info(monitoring_radio, m)
268
+ else:
269
+ logger.warning("Worker to hold was not in ready managers list")
296
270
 
297
- elif command_req == "WORKER_PORTS":
298
- reply = (self.worker_task_port, self.worker_result_port)
271
+ reply = None
299
272
 
300
- else:
301
- logger.error(f"Received unknown command: {command_req}")
302
- reply = None
273
+ elif command_req == "WORKER_PORTS":
274
+ reply = (self.worker_task_port, self.worker_result_port)
303
275
 
304
- logger.debug("Reply: {}".format(reply))
305
- self.command_channel.send_pyobj(reply)
276
+ else:
277
+ logger.error(f"Received unknown command: {command_req}")
278
+ reply = None
306
279
 
307
- except zmq.Again:
308
- logger.debug("Command thread is alive")
309
- continue
280
+ logger.debug("Reply: {}".format(reply))
281
+ self.command_channel.send_pyobj(reply)
310
282
 
311
283
  @wrap_with_logs
312
284
  def start(self) -> None:
@@ -326,21 +298,13 @@ class Interchange:
326
298
 
327
299
  start = time.time()
328
300
 
329
- self._task_puller_thread = threading.Thread(target=self.task_puller,
330
- name="Interchange-Task-Puller",
331
- daemon=True)
332
- self._task_puller_thread.start()
333
-
334
- self._command_thread = threading.Thread(target=self._command_server,
335
- name="Interchange-Command",
336
- daemon=True)
337
- self._command_thread.start()
338
-
339
301
  kill_event = threading.Event()
340
302
 
341
303
  poller = zmq.Poller()
342
304
  poller.register(self.task_outgoing, zmq.POLLIN)
343
305
  poller.register(self.results_incoming, zmq.POLLIN)
306
+ poller.register(self.task_incoming, zmq.POLLIN)
307
+ poller.register(self.command_channel, zmq.POLLIN)
344
308
 
345
309
  # These are managers which we should examine in an iteration
346
310
  # for scheduling a job (or maybe any other attention?).
@@ -351,6 +315,8 @@ class Interchange:
351
315
  while not kill_event.is_set():
352
316
  self.socks = dict(poller.poll(timeout=poll_period))
353
317
 
318
+ self.process_command(monitoring_radio)
319
+ self.process_task_incoming()
354
320
  self.process_task_outgoing_incoming(interesting_managers, monitoring_radio, kill_event)
355
321
  self.process_results_incoming(interesting_managers, monitoring_radio)
356
322
  self.expire_bad_managers(interesting_managers, monitoring_radio)
@@ -362,6 +328,18 @@ class Interchange:
362
328
  logger.info(f"Processed {self.count} tasks in {delta} seconds")
363
329
  logger.warning("Exiting")
364
330
 
331
+ def process_task_incoming(self) -> None:
332
+ """Process incoming task message(s).
333
+ """
334
+
335
+ if self.task_incoming in self.socks and self.socks[self.task_incoming] == zmq.POLLIN:
336
+ logger.debug("start task_incoming section")
337
+ msg = self.task_incoming.recv_pyobj()
338
+ logger.debug("putting message onto pending_task_queue")
339
+ self.pending_task_queue.put(msg)
340
+ self.task_counter += 1
341
+ logger.debug(f"Fetched {self.task_counter} tasks so far")
342
+
365
343
  def process_task_outgoing_incoming(
366
344
  self,
367
345
  interesting_managers: Set[bytes],
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: parsl
3
- Version: 2025.1.20
3
+ Version: 2025.2.3
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.01.20.tar.gz
6
+ Download-URL: https://github.com/Parsl/parsl/archive/2025.02.03.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=5FvHIMao3Ik0Rm2p2ieL1KQcQcYXc5K83Jrx5csi-B4,14301
11
- parsl/version.py,sha256=B68kDKT369JEJ5yXX5Ue1dD455x8WPmdaddBARy1bBc,131
11
+ parsl/version.py,sha256=Zl1Frad6LRWH7vPgll3ZZU1ODYvo9dMwEa4jBjGYQkg,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
@@ -53,10 +53,10 @@ parsl/data_provider/staging.py,sha256=ZDZuuFg38pjUStegKPcvPsfGp3iMeReMzfU6DSwtJj
53
53
  parsl/data_provider/zip.py,sha256=S4kVuH9lxAegRURYbvIUR7EYYBOccyslaqyCrVWUBhw,4497
54
54
  parsl/dataflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
55
  parsl/dataflow/dependency_resolvers.py,sha256=Om8Dgh7a0ZwgXAc6TlhxLSzvxXHDlNNV1aBNiD3JTNY,3325
56
- parsl/dataflow/dflow.py,sha256=tbAFEcLgKQp4IUwMhH1fYVxYU_e3_jw8ENElEA9kdeE,64928
57
- parsl/dataflow/errors.py,sha256=vzgEEFqIfIQ3_QGvqDCcsACZZlUKNAKaMchM30TGHyY,2114
56
+ parsl/dataflow/dflow.py,sha256=jNxrAd2xmxesS3fR6eZyDN9f6I0BIBQbhL63zv51lkk,61752
57
+ parsl/dataflow/errors.py,sha256=daVfr2BWs1zRsGD6JtosEMttWHvK1df1Npiu_MUvFKg,3998
58
58
  parsl/dataflow/futures.py,sha256=08LuP-HFiHBIZmeKCjlsazw_WpQ5fwevrU2_WbidkYw,6080
59
- parsl/dataflow/memoization.py,sha256=l9uw1Bu50GucBF70M5relpGKFkE4dIM9T3R1KrxW0v0,9583
59
+ parsl/dataflow/memoization.py,sha256=QUkTduZ_gdr8i08VWNWrqhfEvoMGsPDZegWUE2_7sGQ,12579
60
60
  parsl/dataflow/rundirs.py,sha256=JZdzybVGubY35jL2YiKcDo65ZmRl1WyOApc8ajYxztc,1087
61
61
  parsl/dataflow/states.py,sha256=hV6mfv-y4A6xrujeQglcomnfEs7y3Xm2g6JFwC6dvgQ,2612
62
62
  parsl/dataflow/taskrecord.py,sha256=qIW7T6hn9dYTuNPdUura3HQwwUpUJACwPP5REm5COf4,3042
@@ -73,8 +73,8 @@ 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=xzNW4X9Zn10o0FsY8rnIuHCuaBu0vaKEXqMpe-2jlaA,38341
77
- parsl/executors/high_throughput/interchange.py,sha256=i0TYgAQzcnzRnSdKal9M9d6TcWRoIWK77kuxMsiYxXE,30142
76
+ parsl/executors/high_throughput/executor.py,sha256=4WKp0ZqAz036WcaXq-4DDwLyu8W6xGPunnvLAVSeaiQ,38493
77
+ parsl/executors/high_throughput/interchange.py,sha256=kVz7LTx6ThcYrQOSq-TsWHOlDwgyQk4w9Qyd3egQTuY,29143
78
78
  parsl/executors/high_throughput/manager_record.py,sha256=yn3L8TUJFkgm2lX1x0SeS9mkvJowC0s2VIMCFiU7ThM,455
79
79
  parsl/executors/high_throughput/manager_selector.py,sha256=UKcUE6v0tO7PDMTThpKSKxVpOpOUilxDL7UbNgpZCxo,2116
80
80
  parsl/executors/high_throughput/monitoring_info.py,sha256=HC0drp6nlXQpAop5PTUKNjdXMgtZVvrBL0JzZJebPP4,298
@@ -290,9 +290,8 @@ parsl/tests/test_bash_apps/test_std_uri.py,sha256=CvAt8BUhNl2pA5chq9YyhkD6eo2IUH
290
290
  parsl/tests/test_bash_apps/test_stdout.py,sha256=lNBzCJGst0IhKaSl8CM8-mTJ5eaK7hTlZ8gY-M2TDBU,3244
291
291
  parsl/tests/test_checkpointing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
292
292
  parsl/tests/test_checkpointing/test_periodic.py,sha256=nfMgrG7sZ8rkMu6iOHS6lp_iTU4IsOyQLQ2Gur_FMmE,1509
293
- parsl/tests/test_checkpointing/test_python_checkpoint_1.py,sha256=k7_Zy4CV9OQt4ORYFCdyX53c4B0YPiwEIi35LhvLB2w,746
293
+ parsl/tests/test_checkpointing/test_python_checkpoint_1.py,sha256=TP6kSK_0qpCqecpp7O50AbTsnLKU6wvTvNG89hh4LQw,637
294
294
  parsl/tests/test_checkpointing/test_python_checkpoint_2.py,sha256=Q_cXeAVz_dJuDDeiemUIGd-wmb7aCY3ggpqYjRRhHRc,1089
295
- parsl/tests/test_checkpointing/test_python_checkpoint_3.py,sha256=y4esbbp1h9FP-_rBfnFhohB6a7o3VOpjqKdTovV8HbA,805
296
295
  parsl/tests/test_checkpointing/test_regression_232.py,sha256=AsI6AJ0DcFaefAbEY9qWa41ER0VX-4yLuIdlgvBw360,2637
297
296
  parsl/tests/test_checkpointing/test_regression_233.py,sha256=jii7BKuygK6KMIGtg4IeBjix7Z28cYhv57rE9ixoXMU,1774
298
297
  parsl/tests/test_checkpointing/test_regression_239.py,sha256=xycW1_IwVC55L25oMES_OzJU58TN5BoMvRUZ_xB69jU,2441
@@ -366,7 +365,7 @@ parsl/tests/test_python_apps/test_dep_standard_futures.py,sha256=kMOMZLaxJMmpABC
366
365
  parsl/tests/test_python_apps/test_dependencies.py,sha256=IRiTI_lPoWBSFSFnaBlE6Bv08PKEaf-qj5dfqO2RjT0,272
367
366
  parsl/tests/test_python_apps/test_dependencies_deep.py,sha256=Cuow2LLGY7zffPFj89AOIwKlXxHtsin3v_UIhfdwV_w,1542
368
367
  parsl/tests/test_python_apps/test_depfail_propagation.py,sha256=3q3HlVWrOixFtXWBvR_ypKtbdAHAJcKndXQ5drwrBQU,1488
369
- parsl/tests/test_python_apps/test_fail.py,sha256=CgZq_ByzX6YLhBg71nWGXwaOaesYXwE6TWwslwrVuq4,1466
368
+ parsl/tests/test_python_apps/test_fail.py,sha256=gMuZwxZNaUCaonlUX-7SOBvXg8kidkBcEeqKLEvqpYM,1692
370
369
  parsl/tests/test_python_apps/test_fibonacci_iterative.py,sha256=ly2s5HuB9R53Z2FM_zy0WWdOk01iVhgcwSpQyK6ErIY,573
371
370
  parsl/tests/test_python_apps/test_fibonacci_recursive.py,sha256=q7LMFcu_pJSNPdz8iY0UiRoIweEWIBGwMjQffHWAuDc,592
372
371
  parsl/tests/test_python_apps/test_futures.py,sha256=EWnzmPn5sVCgeMxc0Uz2ieaaVYr98tFZ7g8YJFqYuC8,2355
@@ -376,7 +375,7 @@ parsl/tests/test_python_apps/test_inputs_default.py,sha256=J2GR1NgdvEucNSJkfO6GC
376
375
  parsl/tests/test_python_apps/test_join.py,sha256=OWd6_A0Cf-1Xpjr0OT3HaJ1IMYcJ0LFL1VnmL0cZkL8,2988
377
376
  parsl/tests/test_python_apps/test_lifted.py,sha256=Na6qC_dZSeYJcZdkGn-dCjgYkQV267HmGFfaqFcRVcQ,3408
378
377
  parsl/tests/test_python_apps/test_mapred.py,sha256=C7nTl0NsP_2TCtcmZXWFMpvAG4pwGswrIJKr-5sRUNY,786
379
- parsl/tests/test_python_apps/test_memoize_1.py,sha256=PXazRnekBe_KScUdbh8P3I7Vu_1Tc-nGssWBpfcic7M,514
378
+ parsl/tests/test_python_apps/test_memoize_1.py,sha256=E_VQAaykFKT_G7yRUWOhXxfOICj07qLq2R7onZ4oY9g,449
380
379
  parsl/tests/test_python_apps/test_memoize_2.py,sha256=uG9zG9j3ap1FqeJ8aB0Gj_dX191pN3dxWXeQ-asxPgU,553
381
380
  parsl/tests/test_python_apps/test_memoize_4.py,sha256=CdK_vHW5s-phi5KPqcAQm_BRh8xek91GVGeQRjfJ4Bk,569
382
381
  parsl/tests/test_python_apps/test_memoize_bad_id_for_memo.py,sha256=5v25zdU6koXexRTkccj_3sSSdXqHdsU8ZdNrnZ3ONZU,1436
@@ -456,13 +455,13 @@ parsl/usage_tracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
456
455
  parsl/usage_tracking/api.py,sha256=iaCY58Dc5J4UM7_dJzEEs871P1p1HdxBMtNGyVdzc9g,1821
457
456
  parsl/usage_tracking/levels.py,sha256=xbfzYEsd55KiZJ-mzNgPebvOH4rRHum04hROzEf41tU,291
458
457
  parsl/usage_tracking/usage.py,sha256=f9k6QcpbQxkGyP5WTC9PVyv0CA05s9NDpRe5wwRdBTM,9163
459
- parsl-2025.1.20.data/scripts/exec_parsl_function.py,sha256=YXKVVIa4zXmOtz-0Ca4E_5nQfN_3S2bh2tB75uZZB4w,7774
460
- parsl-2025.1.20.data/scripts/interchange.py,sha256=rUhF_Bwk5NOqLhh-HgP-ei_gclKnPIJJ7uS32p0j-XI,30129
461
- parsl-2025.1.20.data/scripts/parsl_coprocess.py,sha256=zrVjEqQvFOHxsLufPi00xzMONagjVwLZbavPM7bbjK4,5722
462
- parsl-2025.1.20.data/scripts/process_worker_pool.py,sha256=82FoJTye2SysJzPg-N8BpenuHGU7hOI8-Bedq8HV9C0,41851
463
- parsl-2025.1.20.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
464
- parsl-2025.1.20.dist-info/METADATA,sha256=7DsaLRVRoBAcLHQbTggqANCocVk-5c4G6T2b-1Hd3go,4027
465
- parsl-2025.1.20.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
466
- parsl-2025.1.20.dist-info/entry_points.txt,sha256=XqnsWDYoEcLbsMcpnYGKLEnSBmaIe1YoM5YsBdJG2tI,176
467
- parsl-2025.1.20.dist-info/top_level.txt,sha256=PIheYoUFQtF2icLsgOykgU-Cjuwr2Oi6On2jo5RYgRM,6
468
- parsl-2025.1.20.dist-info/RECORD,,
458
+ parsl-2025.2.3.data/scripts/exec_parsl_function.py,sha256=YXKVVIa4zXmOtz-0Ca4E_5nQfN_3S2bh2tB75uZZB4w,7774
459
+ parsl-2025.2.3.data/scripts/interchange.py,sha256=dAo8zl82gUFaeu_vNz-OXjkbeORZmwnqj4CMv8DahKY,29130
460
+ parsl-2025.2.3.data/scripts/parsl_coprocess.py,sha256=zrVjEqQvFOHxsLufPi00xzMONagjVwLZbavPM7bbjK4,5722
461
+ parsl-2025.2.3.data/scripts/process_worker_pool.py,sha256=82FoJTye2SysJzPg-N8BpenuHGU7hOI8-Bedq8HV9C0,41851
462
+ parsl-2025.2.3.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
463
+ parsl-2025.2.3.dist-info/METADATA,sha256=nqLV9gMKl_5N-QYsDt-PJZ2W-9W6pqJEzWKvJ3LNf8g,4026
464
+ parsl-2025.2.3.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
465
+ parsl-2025.2.3.dist-info/entry_points.txt,sha256=XqnsWDYoEcLbsMcpnYGKLEnSBmaIe1YoM5YsBdJG2tI,176
466
+ parsl-2025.2.3.dist-info/top_level.txt,sha256=PIheYoUFQtF2icLsgOykgU-Cjuwr2Oi6On2jo5RYgRM,6
467
+ parsl-2025.2.3.dist-info/RECORD,,
@@ -1,42 +0,0 @@
1
- import os
2
-
3
- import pytest
4
-
5
- import parsl
6
- from parsl.app.app import python_app
7
- from parsl.tests.configs.local_threads import config
8
-
9
-
10
- def local_setup():
11
- global dfk
12
- dfk = parsl.load(config)
13
-
14
-
15
- def local_teardown():
16
- parsl.dfk().cleanup()
17
-
18
-
19
- @python_app
20
- def slow_double(x, sleep_dur=1, cache=True):
21
- import time
22
- time.sleep(sleep_dur)
23
- return x * 2
24
-
25
-
26
- @pytest.mark.local
27
- def test_checkpointing():
28
- """Testing code snippet from documentation
29
- """
30
-
31
- N = 5 # Number of calls to slow_double
32
- d = [] # List to store the futures
33
- for i in range(0, N):
34
- d.append(slow_double(i))
35
-
36
- # Wait for the results
37
- [i.result() for i in d]
38
-
39
- checkpoint_dir = dfk.checkpoint()
40
- print(checkpoint_dir)
41
-
42
- assert os.path.exists(checkpoint_dir), "Checkpoint dir does not exist"