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.
- parsl/__init__.py +0 -4
- parsl/app/bash.py +1 -1
- parsl/benchmark/perf.py +73 -17
- parsl/concurrent/__init__.py +95 -14
- parsl/curvezmq.py +0 -16
- parsl/data_provider/globus.py +3 -1
- parsl/dataflow/dflow.py +107 -207
- parsl/dataflow/memoization.py +144 -31
- parsl/dataflow/states.py +5 -5
- parsl/executors/base.py +2 -2
- parsl/executors/execute_task.py +2 -8
- parsl/executors/flux/executor.py +4 -6
- parsl/executors/globus_compute.py +0 -4
- parsl/executors/high_throughput/executor.py +86 -25
- parsl/executors/high_throughput/interchange.py +55 -42
- parsl/executors/high_throughput/mpi_executor.py +1 -2
- parsl/executors/high_throughput/mpi_resource_management.py +7 -14
- parsl/executors/high_throughput/process_worker_pool.py +32 -7
- parsl/executors/high_throughput/zmq_pipes.py +36 -67
- parsl/executors/radical/executor.py +2 -6
- parsl/executors/radical/rpex_worker.py +2 -2
- parsl/executors/taskvine/executor.py +5 -1
- parsl/executors/threads.py +5 -2
- parsl/jobs/states.py +2 -2
- parsl/jobs/strategy.py +7 -6
- parsl/monitoring/db_manager.py +21 -23
- parsl/monitoring/monitoring.py +2 -2
- parsl/monitoring/radios/filesystem.py +2 -1
- parsl/monitoring/radios/htex.py +2 -1
- parsl/monitoring/radios/multiprocessing.py +2 -1
- parsl/monitoring/radios/udp.py +2 -1
- parsl/monitoring/radios/udp_router.py +2 -2
- parsl/monitoring/radios/zmq_router.py +2 -2
- parsl/multiprocessing.py +0 -49
- parsl/providers/base.py +24 -37
- parsl/providers/pbspro/pbspro.py +1 -1
- parsl/serialize/__init__.py +6 -9
- parsl/serialize/facade.py +0 -32
- parsl/tests/configs/local_threads_globus.py +18 -14
- parsl/tests/configs/taskvine_ex.py +1 -1
- parsl/tests/manual_tests/test_memory_limits.py +1 -1
- parsl/tests/sites/test_concurrent.py +51 -3
- parsl/tests/test_checkpointing/test_periodic.py +15 -9
- parsl/tests/test_checkpointing/test_python_checkpoint_1.py +6 -3
- parsl/tests/test_checkpointing/test_regression_233.py +0 -1
- parsl/tests/test_curvezmq.py +0 -42
- parsl/tests/test_execute_task.py +2 -11
- parsl/tests/test_htex/test_command_concurrency_regression_1321.py +54 -0
- parsl/tests/test_htex/test_htex.py +36 -1
- parsl/tests/test_htex/test_interchange_exit_bad_registration.py +2 -2
- parsl/tests/test_htex/test_priority_queue.py +26 -3
- parsl/tests/test_htex/test_zmq_binding.py +2 -1
- parsl/tests/test_mpi_apps/test_mpi_scheduler.py +18 -43
- parsl/tests/test_python_apps/test_basic.py +0 -14
- parsl/tests/test_python_apps/test_depfail_propagation.py +11 -1
- parsl/tests/test_python_apps/test_exception.py +19 -0
- parsl/tests/test_python_apps/test_garbage_collect.py +1 -6
- parsl/tests/test_python_apps/test_memoize_2.py +11 -1
- parsl/tests/test_python_apps/test_memoize_exception.py +41 -0
- parsl/tests/test_regression/test_3874.py +47 -0
- parsl/tests/test_scaling/test_regression_3696_oscillation.py +1 -0
- parsl/tests/test_staging/test_staging_globus.py +2 -2
- parsl/tests/test_utils/test_representation_mixin.py +53 -0
- parsl/tests/unit/test_globus_compute_executor.py +11 -2
- parsl/utils.py +11 -3
- parsl/version.py +1 -1
- {parsl-2025.8.4.data → parsl-2025.11.10.data}/scripts/interchange.py +55 -42
- {parsl-2025.8.4.data → parsl-2025.11.10.data}/scripts/process_worker_pool.py +32 -7
- {parsl-2025.8.4.dist-info → parsl-2025.11.10.dist-info}/METADATA +64 -50
- {parsl-2025.8.4.dist-info → parsl-2025.11.10.dist-info}/RECORD +76 -81
- {parsl-2025.8.4.dist-info → parsl-2025.11.10.dist-info}/WHEEL +1 -1
- parsl/tests/configs/local_threads_checkpoint_periodic.py +0 -11
- parsl/tests/configs/local_threads_no_cache.py +0 -11
- parsl/tests/site_tests/test_provider.py +0 -88
- parsl/tests/site_tests/test_site.py +0 -70
- parsl/tests/test_aalst_patterns.py +0 -474
- parsl/tests/test_docs/test_workflow2.py +0 -42
- parsl/tests/test_error_handling/test_rand_fail.py +0 -171
- parsl/tests/test_regression/test_854.py +0 -62
- parsl/tests/test_serialization/test_pack_resource_spec.py +0 -23
- {parsl-2025.8.4.data → parsl-2025.11.10.data}/scripts/exec_parsl_function.py +0 -0
- {parsl-2025.8.4.data → parsl-2025.11.10.data}/scripts/parsl_coprocess.py +0 -0
- {parsl-2025.8.4.dist-info → parsl-2025.11.10.dist-info}/entry_points.txt +0 -0
- {parsl-2025.8.4.dist-info → parsl-2025.11.10.dist-info/licenses}/LICENSE +0 -0
- {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
|
-
|
|
29
|
+
parsl.dfk().checkpoint()
|
|
29
30
|
|
|
30
|
-
|
|
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}"
|
parsl/tests/test_curvezmq.py
CHANGED
|
@@ -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(
|
parsl/tests/test_execute_task.py
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
#
|
|
44
|
-
|
|
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 = {
|
|
@@ -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
|
|
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
|
-
|
|
46
|
-
|
|
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
|
|
82
|
+
for trip in range(1, 9):
|
|
86
83
|
assert scheduler._free_node_counter.value == 8
|
|
87
84
|
|
|
88
|
-
|
|
89
|
-
|
|
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 -
|
|
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":
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
6
|
+
from parsl.tests.configs.local_threads_globus import fresh_config, remote_writeable
|
|
7
7
|
|
|
8
|
-
local_config =
|
|
8
|
+
local_config = fresh_config
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@python_app
|