parsl 2025.8.4__py3-none-any.whl → 2025.11.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.
Files changed (85) hide show
  1. parsl/__init__.py +0 -4
  2. parsl/app/bash.py +1 -1
  3. parsl/benchmark/perf.py +73 -17
  4. parsl/concurrent/__init__.py +95 -14
  5. parsl/curvezmq.py +0 -16
  6. parsl/data_provider/globus.py +3 -1
  7. parsl/dataflow/dflow.py +107 -207
  8. parsl/dataflow/memoization.py +144 -31
  9. parsl/dataflow/states.py +5 -5
  10. parsl/executors/base.py +2 -2
  11. parsl/executors/execute_task.py +2 -8
  12. parsl/executors/flux/executor.py +4 -6
  13. parsl/executors/globus_compute.py +0 -4
  14. parsl/executors/high_throughput/executor.py +86 -25
  15. parsl/executors/high_throughput/interchange.py +55 -42
  16. parsl/executors/high_throughput/mpi_executor.py +1 -2
  17. parsl/executors/high_throughput/mpi_resource_management.py +7 -14
  18. parsl/executors/high_throughput/process_worker_pool.py +32 -7
  19. parsl/executors/high_throughput/zmq_pipes.py +36 -67
  20. parsl/executors/radical/executor.py +2 -6
  21. parsl/executors/radical/rpex_worker.py +2 -2
  22. parsl/executors/taskvine/executor.py +5 -1
  23. parsl/executors/threads.py +5 -2
  24. parsl/jobs/states.py +2 -2
  25. parsl/jobs/strategy.py +7 -6
  26. parsl/monitoring/db_manager.py +21 -23
  27. parsl/monitoring/monitoring.py +2 -2
  28. parsl/monitoring/radios/filesystem.py +2 -1
  29. parsl/monitoring/radios/htex.py +2 -1
  30. parsl/monitoring/radios/multiprocessing.py +2 -1
  31. parsl/monitoring/radios/udp.py +2 -1
  32. parsl/monitoring/radios/udp_router.py +2 -2
  33. parsl/monitoring/radios/zmq_router.py +2 -2
  34. parsl/multiprocessing.py +0 -49
  35. parsl/providers/base.py +24 -37
  36. parsl/providers/pbspro/pbspro.py +1 -1
  37. parsl/serialize/__init__.py +6 -9
  38. parsl/serialize/facade.py +0 -32
  39. parsl/tests/configs/local_threads_globus.py +18 -14
  40. parsl/tests/configs/taskvine_ex.py +1 -1
  41. parsl/tests/manual_tests/test_memory_limits.py +1 -1
  42. parsl/tests/sites/test_concurrent.py +51 -3
  43. parsl/tests/test_checkpointing/test_periodic.py +15 -9
  44. parsl/tests/test_checkpointing/test_python_checkpoint_1.py +6 -3
  45. parsl/tests/test_checkpointing/test_regression_233.py +0 -1
  46. parsl/tests/test_curvezmq.py +0 -42
  47. parsl/tests/test_execute_task.py +2 -11
  48. parsl/tests/test_htex/test_command_concurrency_regression_1321.py +54 -0
  49. parsl/tests/test_htex/test_htex.py +36 -1
  50. parsl/tests/test_htex/test_interchange_exit_bad_registration.py +2 -2
  51. parsl/tests/test_htex/test_priority_queue.py +26 -3
  52. parsl/tests/test_htex/test_zmq_binding.py +2 -1
  53. parsl/tests/test_mpi_apps/test_mpi_scheduler.py +18 -43
  54. parsl/tests/test_python_apps/test_basic.py +0 -14
  55. parsl/tests/test_python_apps/test_depfail_propagation.py +11 -1
  56. parsl/tests/test_python_apps/test_exception.py +19 -0
  57. parsl/tests/test_python_apps/test_garbage_collect.py +1 -6
  58. parsl/tests/test_python_apps/test_memoize_2.py +11 -1
  59. parsl/tests/test_python_apps/test_memoize_exception.py +41 -0
  60. parsl/tests/test_regression/test_3874.py +47 -0
  61. parsl/tests/test_scaling/test_regression_3696_oscillation.py +1 -0
  62. parsl/tests/test_staging/test_staging_globus.py +2 -2
  63. parsl/tests/test_utils/test_representation_mixin.py +53 -0
  64. parsl/tests/unit/test_globus_compute_executor.py +11 -2
  65. parsl/utils.py +11 -3
  66. parsl/version.py +1 -1
  67. {parsl-2025.8.4.data → parsl-2025.11.10.data}/scripts/interchange.py +55 -42
  68. {parsl-2025.8.4.data → parsl-2025.11.10.data}/scripts/process_worker_pool.py +32 -7
  69. {parsl-2025.8.4.dist-info → parsl-2025.11.10.dist-info}/METADATA +64 -50
  70. {parsl-2025.8.4.dist-info → parsl-2025.11.10.dist-info}/RECORD +76 -81
  71. {parsl-2025.8.4.dist-info → parsl-2025.11.10.dist-info}/WHEEL +1 -1
  72. parsl/tests/configs/local_threads_checkpoint_periodic.py +0 -11
  73. parsl/tests/configs/local_threads_no_cache.py +0 -11
  74. parsl/tests/site_tests/test_provider.py +0 -88
  75. parsl/tests/site_tests/test_site.py +0 -70
  76. parsl/tests/test_aalst_patterns.py +0 -474
  77. parsl/tests/test_docs/test_workflow2.py +0 -42
  78. parsl/tests/test_error_handling/test_rand_fail.py +0 -171
  79. parsl/tests/test_regression/test_854.py +0 -62
  80. parsl/tests/test_serialization/test_pack_resource_spec.py +0 -23
  81. {parsl-2025.8.4.data → parsl-2025.11.10.data}/scripts/exec_parsl_function.py +0 -0
  82. {parsl-2025.8.4.data → parsl-2025.11.10.data}/scripts/parsl_coprocess.py +0 -0
  83. {parsl-2025.8.4.dist-info → parsl-2025.11.10.dist-info}/entry_points.txt +0 -0
  84. {parsl-2025.8.4.dist-info → parsl-2025.11.10.dist-info/licenses}/LICENSE +0 -0
  85. {parsl-2025.8.4.dist-info → parsl-2025.11.10.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,5 @@
1
1
  import os
2
+ from pathlib import Path
2
3
 
3
4
  import pytest
4
5
 
@@ -20,12 +21,14 @@ def uuid_app():
20
21
 
21
22
 
22
23
  @pytest.mark.local
23
- def test_initial_checkpoint_write():
24
+ def test_initial_checkpoint_write() -> None:
24
25
  """1. Launch a few apps and write the checkpoint once a few have completed
25
26
  """
26
27
  uuid_app().result()
27
28
 
28
- cpt_dir = parsl.dfk().checkpoint()
29
+ parsl.dfk().checkpoint()
29
30
 
30
- cptpath = cpt_dir + '/tasks.pkl'
31
+ cpt_dir = Path(parsl.dfk().run_dir) / 'checkpoint'
32
+
33
+ cptpath = cpt_dir / 'tasks.pkl'
31
34
  assert os.path.exists(cptpath), f"Tasks checkpoint missing: {cptpath}"
@@ -5,7 +5,6 @@ from parsl.dataflow.dflow import DataFlowKernel
5
5
 
6
6
 
7
7
  def run_checkpointed(checkpoints):
8
- # set_stream_logger()
9
8
  from parsl.tests.configs.local_threads_checkpoint_task_exit import config
10
9
  config.checkpoint_files = checkpoints
11
10
  dfk = DataFlowKernel(config=config)
@@ -296,48 +296,6 @@ def test_server_context_destroy(server_ctx: curvezmq.ServerContext, encrypted: b
296
296
  assert not server_ctx.auth_thread.pipe
297
297
 
298
298
 
299
- @pytest.mark.local
300
- @pytest.mark.parametrize("encrypted", (True, False), indirect=True)
301
- def test_client_context_recreate(client_ctx: curvezmq.ClientContext):
302
- hidden_ctx = client_ctx._ctx
303
- sock = client_ctx.socket(zmq.REQ)
304
-
305
- assert not sock.closed
306
- assert not client_ctx.closed
307
-
308
- client_ctx.recreate()
309
-
310
- assert sock.closed
311
- assert not client_ctx.closed
312
- assert hidden_ctx != client_ctx._ctx
313
- assert hidden_ctx.closed
314
-
315
-
316
- @pytest.mark.local
317
- @pytest.mark.parametrize("encrypted", (True, False), indirect=True)
318
- def test_server_context_recreate(server_ctx: curvezmq.ServerContext, encrypted: bool):
319
- hidden_ctx = server_ctx._ctx
320
- sock = server_ctx.socket(zmq.REP)
321
-
322
- assert not sock.closed
323
- assert not server_ctx.closed
324
- if encrypted:
325
- assert server_ctx.auth_thread
326
- auth_thread = server_ctx.auth_thread
327
- assert auth_thread.pipe
328
-
329
- server_ctx.recreate()
330
-
331
- assert sock.closed
332
- assert not server_ctx.closed
333
- assert hidden_ctx.closed
334
- assert hidden_ctx != server_ctx._ctx
335
- if encrypted:
336
- assert server_ctx.auth_thread
337
- assert auth_thread != server_ctx.auth_thread
338
- assert server_ctx.auth_thread.pipe
339
-
340
-
341
299
  @pytest.mark.local
342
300
  @pytest.mark.parametrize("encrypted", (True, False), indirect=True)
343
301
  def test_connection(
@@ -3,7 +3,7 @@ import os
3
3
  import pytest
4
4
 
5
5
  from parsl.executors.execute_task import execute_task
6
- from parsl.serialize.facade import pack_res_spec_apply_message
6
+ from parsl.serialize.facade import pack_apply_message
7
7
 
8
8
 
9
9
  def addemup(*args: int, name: str = "apples"):
@@ -15,15 +15,6 @@ def addemup(*args: int, name: str = "apples"):
15
15
  def test_execute_task():
16
16
  args = (1, 2, 3)
17
17
  kwargs = {"name": "boots"}
18
- buff = pack_res_spec_apply_message(addemup, args, kwargs, {})
18
+ buff = pack_apply_message(addemup, args, kwargs)
19
19
  res = execute_task(buff)
20
20
  assert res == addemup(*args, **kwargs)
21
-
22
-
23
- @pytest.mark.local
24
- def test_execute_task_resource_spec():
25
- resource_spec = {"num_nodes": 2, "ranks_per_node": 2, "num_ranks": 4}
26
- buff = pack_res_spec_apply_message(addemup, (1, 2), {}, resource_spec)
27
- execute_task(buff)
28
- for key, val in resource_spec.items():
29
- assert os.environ[f"PARSL_{key.upper()}"] == str(val)
@@ -0,0 +1,54 @@
1
+ import time
2
+ from threading import Event, Thread
3
+
4
+ import pytest
5
+
6
+ import parsl
7
+ from parsl.tests.configs.htex_local import fresh_config as local_config
8
+
9
+ N_THREADS = 50
10
+ DURATION_S = 10
11
+
12
+
13
+ @pytest.mark.local
14
+ def test_concurrency_blast():
15
+ """Blast interchange command channel from many threads.
16
+ """
17
+
18
+ cc = parsl.dfk().executors['htex_local'].command_client
19
+
20
+ threads = []
21
+
22
+ ok_so_far = True
23
+
24
+ for _ in range(N_THREADS):
25
+
26
+ # This event will be set if the thread reaches the end of its body.
27
+ event = Event()
28
+
29
+ thread = Thread(target=blast, args=(cc, event))
30
+ threads.append((thread, event))
31
+
32
+ for thread, event in threads:
33
+ thread.start()
34
+
35
+ for thread, event in threads:
36
+ thread.join()
37
+ if not event.is_set():
38
+ ok_so_far = False
39
+
40
+ assert ok_so_far, "at least one thread did not exit normally"
41
+
42
+
43
+ def blast(cc, e):
44
+ target_end = time.monotonic() + DURATION_S
45
+
46
+ while time.monotonic() < target_end:
47
+ cc.run("WORKERS")
48
+ cc.run("MANGERs_PACKAGES")
49
+ cc.run("CONNECTED_BLOCKS")
50
+ cc.run("WORKER_BINDS")
51
+
52
+ # If any of the preceeding cc.run calls raises an exception, the thread
53
+ # will not set its successful completion event.
54
+ e.set()
@@ -7,6 +7,7 @@ from unittest import mock
7
7
  import pytest
8
8
 
9
9
  from parsl import HighThroughputExecutor, curvezmq
10
+ from parsl.serialize.facade import pack_apply_message, unpack_apply_message
10
11
 
11
12
  _MOCK_BASE = "parsl.executors.high_throughput.executor"
12
13
 
@@ -19,11 +20,16 @@ def encrypted(request: pytest.FixtureRequest):
19
20
 
20
21
 
21
22
  @pytest.fixture
22
- def htex(encrypted: bool):
23
+ def htex(encrypted: bool, tmpd_cwd):
23
24
  htex = HighThroughputExecutor(encrypted=encrypted)
25
+ htex.max_workers_per_node = 1
26
+ htex.run_dir = tmpd_cwd
27
+ htex.provider.script_dir = tmpd_cwd
24
28
 
25
29
  yield htex
26
30
 
31
+ if hasattr(htex, "outgoing_q"):
32
+ htex.scale_in(blocks=1000)
27
33
  htex.shutdown()
28
34
 
29
35
 
@@ -146,3 +152,32 @@ def test_htex_interchange_launch_cmd(cmd: Optional[Sequence[str]]):
146
152
  else:
147
153
  htex = HighThroughputExecutor()
148
154
  assert htex.interchange_launch_cmd == ["interchange.py"]
155
+
156
+
157
+ def dyn_exec(buf, *vec_y):
158
+ f, a, _ = unpack_apply_message(buf)
159
+ custom_args = [a, vec_y]
160
+ return f(*custom_args)
161
+
162
+
163
+ @pytest.mark.local
164
+ def test_worker_dynamic_import(htex: HighThroughputExecutor):
165
+ def _dot_prod(vec_x, vec_y):
166
+ return sum(x * y for x, y in zip(vec_x, vec_y))
167
+
168
+ htex.start()
169
+ htex.scale_out_facade(1)
170
+
171
+ num_array = tuple(range(10))
172
+
173
+ fn_buf = pack_apply_message(_dot_prod, num_array, {})
174
+ ctxt = {
175
+ "task_executor": {
176
+ "f": f"{dyn_exec.__module__}.{dyn_exec.__name__}",
177
+ "a": num_array, # prove "custom" dyn_exec
178
+ }
179
+ }
180
+ val = htex.submit_payload(ctxt, fn_buf).result()
181
+ exp_val = _dot_prod(num_array, num_array)
182
+
183
+ assert val == exp_val
@@ -1,4 +1,3 @@
1
- import json
2
1
  import logging
3
2
  import os
4
3
  import pickle
@@ -50,7 +49,8 @@ def test_exit_with_bad_registration(tmpd_cwd, try_assert):
50
49
  "logging_level": logging.DEBUG,
51
50
  "cert_dir": None,
52
51
  "manager_selector": RandomManagerSelector(),
53
- "run_id": "test"
52
+ "run_id": "test",
53
+ "_check_python_mismatch": True,
54
54
  }
55
55
 
56
56
  config_pickle = pickle.dumps(interchange_config)
@@ -1,3 +1,5 @@
1
+ from random import randint
2
+
1
3
  import pytest
2
4
 
3
5
  import parsl
@@ -16,7 +18,7 @@ def fake_task(parsl_resource_specification=None):
16
18
 
17
19
 
18
20
  @pytest.mark.local
19
- def test_priority_queue():
21
+ def test_priority_queue(try_assert):
20
22
  provider = LocalProvider(
21
23
  init_blocks=0,
22
24
  max_blocks=0,
@@ -28,6 +30,7 @@ def test_priority_queue():
28
30
  max_workers_per_node=1,
29
31
  manager_selector=RandomManagerSelector(),
30
32
  provider=provider,
33
+ worker_debug=True, # needed to instrospect interchange logs
31
34
  )
32
35
 
33
36
  config = Config(
@@ -40,12 +43,32 @@ def test_priority_queue():
40
43
  futures = {}
41
44
 
42
45
  # Submit tasks with mixed priorities
43
- # Priorities: [10, 10, 5, 5, 1, 1] to test fallback behavior
44
- for i, priority in enumerate([10, 10, 5, 5, 1, 1]):
46
+ # Test fallback behavior with a guaranteed-unsorted priorities
47
+ priorities = [randint(2, 9) for _ in range(randint(1, 10))]
48
+ priorities.insert(0, 10)
49
+ priorities.extend((1, 10, 1))
50
+ for i, priority in enumerate(priorities):
45
51
  spec = {'priority': priority}
46
52
  futures[(priority, i)] = fake_task(parsl_resource_specification=spec)
47
53
 
54
+ # wait for the interchange to have received all tasks
55
+ # (which happens asynchronously to the main thread, and is otherwise
56
+ # a race condition which can cause this test to fail)
57
+
58
+ n = len(priorities)
59
+
60
+ def interchange_logs_task_count():
61
+ with open(htex.worker_logdir + "/interchange.log", "r") as f:
62
+ lines = f.readlines()
63
+ for line in lines:
64
+ if f"Put task {n} onto pending_task_queue" in line:
65
+ return True
66
+ return False
67
+
68
+ try_assert(interchange_logs_task_count)
69
+
48
70
  provider.max_blocks = 1
71
+ htex.scale_out_facade(1) # don't wait for the JSP to catch up
49
72
 
50
73
  # Wait for completion
51
74
  results = {
@@ -29,7 +29,8 @@ def make_interchange(*,
29
29
  logging_level=logging.INFO,
30
30
  manager_selector=RandomManagerSelector(),
31
31
  poll_period=10,
32
- run_id="test_run_id")
32
+ run_id="test_run_id",
33
+ _check_python_mismatch=True)
33
34
 
34
35
 
35
36
  @pytest.fixture
@@ -1,4 +1,3 @@
1
- import logging
2
1
  import os
3
2
  import pickle
4
3
  from unittest import mock
@@ -10,7 +9,9 @@ from parsl.executors.high_throughput.mpi_resource_management import (
10
9
  TaskScheduler,
11
10
  )
12
11
  from parsl.multiprocessing import SpawnContext
13
- from parsl.serialize import pack_res_spec_apply_message, unpack_res_spec_apply_message
12
+ from parsl.serialize import pack_apply_message
13
+
14
+ mock_task_buffer = pack_apply_message("func", "args", "kwargs")
14
15
 
15
16
 
16
17
  @pytest.fixture(autouse=True)
@@ -42,12 +43,8 @@ def test_MPISched_put_task():
42
43
  assert len(scheduler.available_nodes) == 8
43
44
  assert scheduler._free_node_counter.value == 8
44
45
 
45
- mock_task_buffer = pack_res_spec_apply_message("func",
46
- "args",
47
- "kwargs",
48
- resource_specification={"num_nodes": 2,
49
- "ranks_per_node": 2})
50
- task_package = {"task_id": 1, "buffer": mock_task_buffer}
46
+ ctxt = {"resource_spec": {"num_nodes": 2, "ranks_per_node": 2}}
47
+ task_package = {"task_id": 1, "buffer": mock_task_buffer, "context": ctxt}
51
48
  scheduler.put_task(task_package)
52
49
 
53
50
  assert scheduler._free_node_counter.value == 6
@@ -82,21 +79,17 @@ def test_MPISched_roundtrip():
82
79
  assert scheduler.available_nodes
83
80
  assert len(scheduler.available_nodes) == 8
84
81
 
85
- for round in range(1, 9):
82
+ for trip in range(1, 9):
86
83
  assert scheduler._free_node_counter.value == 8
87
84
 
88
- mock_task_buffer = pack_res_spec_apply_message("func",
89
- "args",
90
- "kwargs",
91
- resource_specification={"num_nodes": round,
92
- "ranks_per_node": 2})
93
- task_package = {"task_id": round, "buffer": mock_task_buffer}
85
+ ctxt = {"resource_spec": {"num_nodes": trip, "ranks_per_node": 2}}
86
+ task_package = {"task_id": trip, "buffer": mock_task_buffer, "context": ctxt}
94
87
  scheduler.put_task(task_package)
95
88
 
96
- assert scheduler._free_node_counter.value == 8 - round
89
+ assert scheduler._free_node_counter.value == 8 - trip
97
90
 
98
91
  # Pop in a mock result
99
- result_pkl = pickle.dumps({"task_id": round, "type": "result", "buffer": "RESULT BUF"})
92
+ result_pkl = pickle.dumps({"task_id": trip, "type": "result", "buffer": "RESULT BUF"})
100
93
  result_q.put(result_pkl)
101
94
 
102
95
  got_result = scheduler.get_result(True, 1)
@@ -114,27 +107,15 @@ def test_MPISched_contention():
114
107
 
115
108
  assert scheduler._free_node_counter.value == 8
116
109
 
117
- mock_task_buffer = pack_res_spec_apply_message("func",
118
- "args",
119
- "kwargs",
120
- resource_specification={
121
- "num_nodes": 8,
122
- "ranks_per_node": 2
123
- })
124
- task_package = {"task_id": 1, "buffer": mock_task_buffer}
110
+ ctxt_1 = {"resource_spec": {"num_nodes": 8, "ranks_per_node": 2}}
111
+ task_package = {"task_id": 1, "buffer": mock_task_buffer, "context": ctxt_1}
125
112
  scheduler.put_task(task_package)
126
113
 
127
114
  assert scheduler._free_node_counter.value == 0
128
115
  assert scheduler._backlog_queue.empty()
129
116
 
130
- mock_task_buffer = pack_res_spec_apply_message("func",
131
- "args",
132
- "kwargs",
133
- resource_specification={
134
- "num_nodes": 8,
135
- "ranks_per_node": 2
136
- })
137
- task_package = {"task_id": 2, "buffer": mock_task_buffer}
117
+ ctxt_2 = {"resource_spec": {"num_nodes": 8, "ranks_per_node": 2}}
118
+ task_package = {"task_id": 2, "buffer": mock_task_buffer, "context": ctxt_2}
138
119
  scheduler.put_task(task_package)
139
120
 
140
121
  # Second task should now be in the backlog_queue
@@ -143,8 +124,7 @@ def test_MPISched_contention():
143
124
  # Confirm that the first task is available and has all 8 nodes provisioned
144
125
  task_on_worker_side = task_q.get()
145
126
  assert task_on_worker_side['task_id'] == 1
146
- _, _, _, resource_spec = unpack_res_spec_apply_message(task_on_worker_side['buffer'])
147
- assert len(resource_spec['MPI_NODELIST'].split(',')) == 8
127
+ assert len(ctxt_1["resource_spec"]["MPI_NODELIST"].split(",")) == 8
148
128
  assert task_q.empty() # Confirm that task 2 is not yet scheduled
149
129
 
150
130
  # Simulate worker returning result and the scheduler picking up result
@@ -159,8 +139,7 @@ def test_MPISched_contention():
159
139
  # Pop in a mock result
160
140
  task_on_worker_side = task_q.get()
161
141
  assert task_on_worker_side['task_id'] == 2
162
- _, _, _, resource_spec = unpack_res_spec_apply_message(task_on_worker_side['buffer'])
163
- assert len(resource_spec['MPI_NODELIST'].split(',')) == 8
142
+ assert len(ctxt_2["resource_spec"]["MPI_NODELIST"].split(",")) == 8
164
143
 
165
144
 
166
145
  @pytest.mark.local
@@ -178,11 +157,7 @@ def test_hashable_backlog_queue():
178
157
  assert scheduler._free_node_counter.value == 8
179
158
 
180
159
  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}
160
+ ctxt = {"resource_spec": {"num_nodes": 8, "ranks_per_node": 2}}
161
+ task_package = {"task_id": i, "buffer": mock_task_buffer, "context": ctxt}
187
162
  scheduler.put_task(task_package)
188
163
  assert scheduler._backlog_queue.qsize() == 2, "Expected 2 backlogged tasks"
@@ -14,12 +14,6 @@ def import_square(x):
14
14
  return math.pow(x, 2)
15
15
 
16
16
 
17
- @python_app
18
- def custom_exception():
19
- from globus_sdk import GlobusError
20
- raise GlobusError('foobar')
21
-
22
-
23
17
  def test_simple(n=2):
24
18
  x = double(n)
25
19
  assert x.result() == n * 2
@@ -38,11 +32,3 @@ def test_parallel_for(n):
38
32
 
39
33
  for i in d:
40
34
  assert d[i].result() == 2 * i
41
-
42
-
43
- def test_custom_exception():
44
- from globus_sdk import GlobusError
45
-
46
- x = custom_exception()
47
- with pytest.raises(GlobusError):
48
- x.result()
@@ -1,5 +1,7 @@
1
+ import parsl
1
2
  from parsl import python_app
2
3
  from parsl.dataflow.errors import DependencyError
4
+ from parsl.dataflow.states import States
3
5
 
4
6
 
5
7
  @python_app
@@ -14,6 +16,7 @@ def depends(parent):
14
16
 
15
17
  def test_depfail_once():
16
18
  """Test the simplest dependency failure case"""
19
+ start_dep_fail_count = parsl.dfk().task_state_counts[States.dep_fail]
17
20
  f1 = fails()
18
21
  f2 = depends(f1)
19
22
 
@@ -25,9 +28,12 @@ def test_depfail_once():
25
28
  # in the DependencyError message
26
29
  assert ("task " + str(f1.task_record['id'])) in str(f2.exception())
27
30
 
31
+ assert parsl.dfk().task_state_counts[States.dep_fail] == start_dep_fail_count + 1
32
+
28
33
 
29
34
  def test_depfail_chain():
30
35
  """Test that dependency failures chain"""
36
+ start_dep_fail_count = parsl.dfk().task_state_counts[States.dep_fail]
31
37
  f1 = fails()
32
38
  f2 = depends(f1)
33
39
  f3 = depends(f2)
@@ -39,11 +45,13 @@ def test_depfail_chain():
39
45
  assert isinstance(f3.exception(), DependencyError)
40
46
  assert isinstance(f4.exception(), DependencyError)
41
47
 
48
+ assert parsl.dfk().task_state_counts[States.dep_fail] == start_dep_fail_count + 3
49
+
42
50
 
43
51
  def test_depfail_branches():
44
52
  """Test that dependency failures propagate in the
45
53
  presence of multiple downstream tasks."""
46
-
54
+ start_dep_fail_count = parsl.dfk().task_state_counts[States.dep_fail]
47
55
  f1 = fails()
48
56
  f2 = depends(f1)
49
57
  f3 = depends(f1)
@@ -52,3 +60,5 @@ def test_depfail_branches():
52
60
  assert not isinstance(f1.exception(), DependencyError)
53
61
  assert isinstance(f2.exception(), DependencyError)
54
62
  assert isinstance(f3.exception(), DependencyError)
63
+
64
+ assert parsl.dfk().task_state_counts[States.dep_fail] == start_dep_fail_count + 2
@@ -0,0 +1,19 @@
1
+ import pytest
2
+
3
+ from parsl.app.app import python_app
4
+
5
+
6
+ class CustomException(Exception):
7
+ pass
8
+
9
+
10
+ @python_app
11
+ def custom_exception():
12
+ from parsl.tests.test_python_apps.test_exception import CustomException
13
+ raise CustomException('foobar')
14
+
15
+
16
+ def test_custom_exception():
17
+ x = custom_exception()
18
+ with pytest.raises(CustomException):
19
+ x.result()
@@ -27,10 +27,5 @@ def test_garbage_collect():
27
27
 
28
28
  evt.set()
29
29
  assert x.result() == 10 * 4
30
- if parsl.dfk().checkpoint_mode is not None:
31
- # We explicit call checkpoint if checkpoint_mode is enabled covering
32
- # cases like manual/periodic where checkpointing may be deferred.
33
- parsl.dfk().checkpoint()
34
-
35
- time.sleep(0.01) # Give enough time for task wipes to work
30
+ time.sleep(0.01) # Give enough time for task wipes to work - see issue #1279
36
31
  assert x.tid not in parsl.dfk().tasks, "Task record should be wiped after task completion"
@@ -4,7 +4,17 @@ import pytest
4
4
 
5
5
  import parsl
6
6
  from parsl.app.app import python_app
7
- from parsl.tests.configs.local_threads_no_cache import fresh_config as local_config
7
+ from parsl.config import Config
8
+ from parsl.executors.threads import ThreadPoolExecutor
9
+
10
+
11
+ def local_config():
12
+ return Config(
13
+ executors=[
14
+ ThreadPoolExecutor(max_threads=4),
15
+ ],
16
+ app_cache=False
17
+ )
8
18
 
9
19
 
10
20
  @python_app
@@ -0,0 +1,41 @@
1
+ import parsl
2
+ from parsl.app.app import python_app
3
+
4
+
5
+ @python_app(cache=True)
6
+ def raise_exception_cache(x, cache=True):
7
+ raise RuntimeError("exception from raise_exception_cache")
8
+
9
+
10
+ @python_app(cache=False)
11
+ def raise_exception_nocache(x, cache=True):
12
+ raise RuntimeError("exception from raise_exception_nocache")
13
+
14
+
15
+ def test_python_memoization(n=2):
16
+ """Test Python memoization of exceptions, with cache=True"""
17
+ x = raise_exception_cache(0)
18
+
19
+ # wait for x to be done
20
+ x.exception()
21
+
22
+ for i in range(0, n):
23
+ fut = raise_exception_cache(0)
24
+
25
+ # check that we get back the same exception object, rather than
26
+ # a new one from a second invocation of raise_exception().
27
+ assert fut.exception() is x.exception(), "Memoized exception should have been memoized"
28
+
29
+
30
+ def test_python_no_memoization(n=2):
31
+ """Test Python non-memoization of exceptions, with cache=False"""
32
+ x = raise_exception_nocache(0)
33
+
34
+ # wait for x to be done
35
+ x.exception()
36
+
37
+ for i in range(0, n):
38
+ fut = raise_exception_nocache(0)
39
+
40
+ # check that we get back a different exception object each time
41
+ assert fut.exception() is not x.exception(), "Memoized exception should have been memoized"
@@ -0,0 +1,47 @@
1
+ import shutil
2
+
3
+ import pytest
4
+
5
+ import parsl
6
+ from parsl.app.app import python_app
7
+ from parsl.config import Config
8
+ from parsl.executors import HighThroughputExecutor
9
+
10
+
11
+ @python_app
12
+ def noop():
13
+ pass
14
+
15
+
16
+ @pytest.mark.local
17
+ def test_regression_3874(tmpd_cwd_session):
18
+ # HTEX run 1
19
+
20
+ rundir_1 = str(tmpd_cwd_session / "1")
21
+
22
+ config = Config(executors=[HighThroughputExecutor()], strategy_period=0.5)
23
+ config.run_dir = rundir_1
24
+
25
+ with parsl.load(config):
26
+ noop().result()
27
+
28
+ # It is necessary to delete this rundir to exercise the bug. Otherwise,
29
+ # the next run will be able to continue looking at this directory - the
30
+ # bug manifests when it cannot.
31
+
32
+ shutil.rmtree(rundir_1)
33
+
34
+ # HTEX run 2
35
+ # In the case of issue 3874, this run hangs (rather than failing) as the
36
+ # JobStatusPoller fails to collect status of all of its managed tasks
37
+ # every iteration, without converging towards failure.
38
+
39
+ rundir_2 = str(tmpd_cwd_session / "2")
40
+
41
+ config = Config(executors=[HighThroughputExecutor()], strategy_period=0.5)
42
+ config.run_dir = rundir_2
43
+
44
+ with parsl.load(config):
45
+ noop().result()
46
+
47
+ shutil.rmtree(rundir_2)
@@ -51,6 +51,7 @@ def test_htex_strategy_does_not_oscillate(ns):
51
51
  executor.outstanding = lambda: n_tasks
52
52
  executor.status_facade = statuses
53
53
  executor.workers_per_node = n_workers
54
+ executor.bad_state_is_set = False
54
55
 
55
56
  provider.parallelism = 1
56
57
  provider.init_blocks = 0
@@ -3,9 +3,9 @@ import pytest
3
3
  import parsl
4
4
  from parsl.app.app import python_app
5
5
  from parsl.data_provider.files import File
6
- from parsl.tests.configs.local_threads_globus import config, remote_writeable
6
+ from parsl.tests.configs.local_threads_globus import fresh_config, remote_writeable
7
7
 
8
- local_config = config
8
+ local_config = fresh_config
9
9
 
10
10
 
11
11
  @python_app