parsl 2024.4.1__py3-none-any.whl → 2024.4.15__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/data_provider/data_manager.py +2 -1
- parsl/data_provider/zip.py +104 -0
- parsl/dataflow/dflow.py +57 -48
- parsl/dataflow/futures.py +0 -7
- parsl/executors/base.py +12 -9
- parsl/executors/high_throughput/executor.py +14 -19
- parsl/executors/high_throughput/process_worker_pool.py +3 -1
- parsl/executors/status_handling.py +82 -9
- parsl/executors/taskvine/executor.py +7 -2
- parsl/executors/workqueue/executor.py +8 -3
- parsl/jobs/job_status_poller.py +27 -107
- parsl/jobs/strategy.py +31 -32
- parsl/monitoring/monitoring.py +14 -23
- parsl/monitoring/radios.py +15 -0
- parsl/monitoring/remote.py +2 -1
- parsl/monitoring/router.py +7 -6
- parsl/providers/local/local.py +1 -1
- parsl/tests/configs/htex_local_alternate.py +2 -1
- parsl/tests/configs/taskvine_ex.py +1 -2
- parsl/tests/configs/workqueue_ex.py +1 -2
- parsl/tests/conftest.py +6 -7
- parsl/tests/test_bash_apps/test_basic.py +5 -4
- parsl/tests/test_bash_apps/test_error_codes.py +0 -3
- parsl/tests/test_bash_apps/test_kwarg_storage.py +0 -1
- parsl/tests/test_bash_apps/test_memoize.py +0 -2
- parsl/tests/test_bash_apps/test_memoize_ignore_args.py +0 -1
- parsl/tests/test_bash_apps/test_memoize_ignore_args_regr.py +0 -1
- parsl/tests/test_bash_apps/test_multiline.py +0 -1
- parsl/tests/test_bash_apps/test_stdout.py +11 -6
- parsl/tests/test_monitoring/test_basic.py +46 -21
- parsl/tests/test_monitoring/test_fuzz_zmq.py +10 -1
- parsl/tests/test_python_apps/test_outputs.py +0 -1
- parsl/tests/test_scaling/test_scale_down_htex_unregistered.py +74 -0
- parsl/tests/test_staging/test_zip_out.py +113 -0
- parsl/version.py +1 -1
- {parsl-2024.4.1.data → parsl-2024.4.15.data}/scripts/process_worker_pool.py +3 -1
- {parsl-2024.4.1.dist-info → parsl-2024.4.15.dist-info}/METADATA +3 -2
- {parsl-2024.4.1.dist-info → parsl-2024.4.15.dist-info}/RECORD +44 -41
- {parsl-2024.4.1.data → parsl-2024.4.15.data}/scripts/exec_parsl_function.py +0 -0
- {parsl-2024.4.1.data → parsl-2024.4.15.data}/scripts/parsl_coprocess.py +0 -0
- {parsl-2024.4.1.dist-info → parsl-2024.4.15.dist-info}/LICENSE +0 -0
- {parsl-2024.4.1.dist-info → parsl-2024.4.15.dist-info}/WHEEL +0 -0
- {parsl-2024.4.1.dist-info → parsl-2024.4.15.dist-info}/entry_points.txt +0 -0
- {parsl-2024.4.1.dist-info → parsl-2024.4.15.dist-info}/top_level.txt +0 -0
@@ -12,7 +12,6 @@ def fail_on_presence(outputs=()):
|
|
12
12
|
# This test is an oddity that requires a shared-FS and simply
|
13
13
|
# won't work if there's a staging provider.
|
14
14
|
# @pytest.mark.sharedFS_required
|
15
|
-
@pytest.mark.issue363
|
16
15
|
def test_bash_memoization(tmpd_cwd, n=2):
|
17
16
|
"""Testing bash memoization
|
18
17
|
"""
|
@@ -33,7 +32,6 @@ def fail_on_presence_kw(outputs=(), foo=None):
|
|
33
32
|
# This test is an oddity that requires a shared-FS and simply
|
34
33
|
# won't work if there's a staging provider.
|
35
34
|
# @pytest.mark.sharedFS_required
|
36
|
-
@pytest.mark.issue363
|
37
35
|
def test_bash_memoization_keywords(tmpd_cwd, n=2):
|
38
36
|
"""Testing bash memoization
|
39
37
|
"""
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import logging
|
1
2
|
import os
|
2
3
|
|
3
4
|
import pytest
|
@@ -35,7 +36,6 @@ testids = [
|
|
35
36
|
]
|
36
37
|
|
37
38
|
|
38
|
-
@pytest.mark.issue363
|
39
39
|
@pytest.mark.parametrize('spec', speclist, ids=testids)
|
40
40
|
def test_bad_stdout_specs(spec):
|
41
41
|
"""Testing bad stdout spec cases"""
|
@@ -54,7 +54,7 @@ def test_bad_stdout_specs(spec):
|
|
54
54
|
assert False, "Did not raise expected exception"
|
55
55
|
|
56
56
|
|
57
|
-
@pytest.mark.
|
57
|
+
@pytest.mark.issue3328
|
58
58
|
def test_bad_stderr_file():
|
59
59
|
"""Testing bad stderr file"""
|
60
60
|
|
@@ -72,8 +72,8 @@ def test_bad_stderr_file():
|
|
72
72
|
return
|
73
73
|
|
74
74
|
|
75
|
-
@pytest.mark.
|
76
|
-
def test_stdout_truncate(tmpd_cwd):
|
75
|
+
@pytest.mark.executor_supports_std_stream_tuples
|
76
|
+
def test_stdout_truncate(tmpd_cwd, caplog):
|
77
77
|
"""Testing truncation of prior content of stdout"""
|
78
78
|
|
79
79
|
out = (str(tmpd_cwd / 't1.out'), 'w')
|
@@ -88,9 +88,11 @@ def test_stdout_truncate(tmpd_cwd):
|
|
88
88
|
assert len1 == 1
|
89
89
|
assert len1 == len2
|
90
90
|
|
91
|
+
for record in caplog.records:
|
92
|
+
assert record.levelno < logging.ERROR
|
91
93
|
|
92
|
-
|
93
|
-
def test_stdout_append(tmpd_cwd):
|
94
|
+
|
95
|
+
def test_stdout_append(tmpd_cwd, caplog):
|
94
96
|
"""Testing appending to prior content of stdout (default open() mode)"""
|
95
97
|
|
96
98
|
out = str(tmpd_cwd / 't1.out')
|
@@ -103,3 +105,6 @@ def test_stdout_append(tmpd_cwd):
|
|
103
105
|
len2 = len(open(out).readlines())
|
104
106
|
|
105
107
|
assert len1 == 1 and len2 == 2
|
108
|
+
|
109
|
+
for record in caplog.records:
|
110
|
+
assert record.levelno < logging.ERROR
|
@@ -1,10 +1,13 @@
|
|
1
|
-
import logging
|
2
1
|
import os
|
3
2
|
import parsl
|
4
3
|
import pytest
|
5
4
|
import time
|
6
5
|
|
7
|
-
|
6
|
+
from parsl import HighThroughputExecutor
|
7
|
+
from parsl.config import Config
|
8
|
+
from parsl.executors.taskvine import TaskVineExecutor
|
9
|
+
from parsl.executors.taskvine import TaskVineManagerConfig
|
10
|
+
from parsl.monitoring import MonitoringHub
|
8
11
|
|
9
12
|
|
10
13
|
@parsl.python_app
|
@@ -18,34 +21,56 @@ def this_app():
|
|
18
21
|
return 5
|
19
22
|
|
20
23
|
|
24
|
+
# The below fresh configs are for use in parametrization, and should return
|
25
|
+
# a configuration that is suitably configured for monitoring.
|
26
|
+
|
27
|
+
def htex_config():
|
28
|
+
from parsl.tests.configs.htex_local_alternate import fresh_config
|
29
|
+
return fresh_config()
|
30
|
+
|
31
|
+
|
32
|
+
def workqueue_config():
|
33
|
+
from parsl.tests.configs.workqueue_ex import fresh_config
|
34
|
+
c = fresh_config()
|
35
|
+
c.monitoring = MonitoringHub(
|
36
|
+
hub_address="localhost",
|
37
|
+
resource_monitoring_interval=1)
|
38
|
+
return c
|
39
|
+
|
40
|
+
|
41
|
+
def taskvine_config():
|
42
|
+
c = Config(executors=[TaskVineExecutor(manager_config=TaskVineManagerConfig(port=9000),
|
43
|
+
worker_launch_method='provider')],
|
44
|
+
|
45
|
+
monitoring=MonitoringHub(hub_address="localhost",
|
46
|
+
resource_monitoring_interval=1))
|
47
|
+
return c
|
48
|
+
|
49
|
+
|
21
50
|
@pytest.mark.local
|
22
|
-
|
51
|
+
@pytest.mark.parametrize("fresh_config", [htex_config, workqueue_config, taskvine_config])
|
52
|
+
def test_row_counts(tmpd_cwd, fresh_config):
|
23
53
|
# this is imported here rather than at module level because
|
24
54
|
# it isn't available in a plain parsl install, so this module
|
25
55
|
# would otherwise fail to import and break even a basic test
|
26
56
|
# run.
|
27
57
|
import sqlalchemy
|
28
58
|
from sqlalchemy import text
|
29
|
-
from parsl.tests.configs.htex_local_alternate import fresh_config
|
30
59
|
|
31
|
-
|
32
|
-
logger.info("Monitoring database already exists - deleting")
|
33
|
-
os.remove("runinfo/monitoring.db")
|
60
|
+
db_url = f"sqlite:///{tmpd_cwd}/monitoring.db"
|
34
61
|
|
35
|
-
|
36
|
-
|
62
|
+
config = fresh_config()
|
63
|
+
config.run_dir = tmpd_cwd
|
64
|
+
config.monitoring.logging_endpoint = db_url
|
37
65
|
|
38
|
-
|
39
|
-
|
66
|
+
with parsl.load(config):
|
67
|
+
assert this_app().result() == 5
|
40
68
|
|
41
|
-
logger.info("cleaning up parsl")
|
42
|
-
parsl.dfk().cleanup()
|
43
69
|
parsl.clear()
|
44
70
|
|
45
71
|
# at this point, we should find one row in the monitoring database.
|
46
72
|
|
47
|
-
|
48
|
-
engine = sqlalchemy.create_engine("sqlite:///runinfo/monitoring.db")
|
73
|
+
engine = sqlalchemy.create_engine(db_url)
|
49
74
|
with engine.begin() as connection:
|
50
75
|
|
51
76
|
result = connection.execute(text("SELECT COUNT(*) FROM workflow"))
|
@@ -67,10 +92,12 @@ def test_row_counts():
|
|
67
92
|
(c, ) = result.first()
|
68
93
|
assert c == 0
|
69
94
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
95
|
+
if isinstance(config.executors[0], HighThroughputExecutor):
|
96
|
+
# The node table is specific to the HighThroughputExecutor
|
97
|
+
# Two entries: one showing manager active, one inactive
|
98
|
+
result = connection.execute(text("SELECT COUNT(*) FROM node"))
|
99
|
+
(c, ) = result.first()
|
100
|
+
assert c == 2
|
74
101
|
|
75
102
|
# There should be one block polling status
|
76
103
|
# local provider has a status_polling_interval of 5s
|
@@ -81,5 +108,3 @@ def test_row_counts():
|
|
81
108
|
result = connection.execute(text("SELECT COUNT(*) FROM resource"))
|
82
109
|
(c, ) = result.first()
|
83
110
|
assert c >= 1
|
84
|
-
|
85
|
-
logger.info("all done")
|
@@ -4,6 +4,7 @@ import parsl
|
|
4
4
|
import pytest
|
5
5
|
import socket
|
6
6
|
import time
|
7
|
+
import zmq
|
7
8
|
|
8
9
|
logger = logging.getLogger(__name__)
|
9
10
|
|
@@ -48,8 +49,16 @@ def test_row_counts():
|
|
48
49
|
s.connect((hub_address, hub_zmq_port))
|
49
50
|
s.sendall(b'fuzzing\r')
|
50
51
|
|
52
|
+
context = zmq.Context()
|
53
|
+
channel_timeout = 10000 # in milliseconds
|
54
|
+
hub_channel = context.socket(zmq.DEALER)
|
55
|
+
hub_channel.setsockopt(zmq.LINGER, 0)
|
56
|
+
hub_channel.set_hwm(0)
|
57
|
+
hub_channel.setsockopt(zmq.SNDTIMEO, channel_timeout)
|
58
|
+
hub_channel.connect("tcp://{}:{}".format(hub_address, hub_zmq_port))
|
59
|
+
|
51
60
|
# this will send a non-object down the DFK's existing ZMQ connection
|
52
|
-
|
61
|
+
hub_channel.send(b'FuzzyByte\rSTREAM')
|
53
62
|
|
54
63
|
# This following attack is commented out, because monitoring is not resilient
|
55
64
|
# to this.
|
@@ -0,0 +1,74 @@
|
|
1
|
+
import logging
|
2
|
+
import time
|
3
|
+
|
4
|
+
import pytest
|
5
|
+
|
6
|
+
import parsl
|
7
|
+
|
8
|
+
from parsl import File, python_app
|
9
|
+
from parsl.jobs.states import JobState, TERMINAL_STATES
|
10
|
+
from parsl.providers import LocalProvider
|
11
|
+
from parsl.channels import LocalChannel
|
12
|
+
from parsl.launchers import SingleNodeLauncher
|
13
|
+
from parsl.config import Config
|
14
|
+
from parsl.executors import HighThroughputExecutor
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
_max_blocks = 1
|
19
|
+
_min_blocks = 0
|
20
|
+
|
21
|
+
|
22
|
+
def local_config():
|
23
|
+
return Config(
|
24
|
+
executors=[
|
25
|
+
HighThroughputExecutor(
|
26
|
+
heartbeat_period=1,
|
27
|
+
heartbeat_threshold=2,
|
28
|
+
poll_period=100,
|
29
|
+
label="htex_local",
|
30
|
+
address="127.0.0.1",
|
31
|
+
max_workers=1,
|
32
|
+
encrypted=True,
|
33
|
+
launch_cmd="sleep inf",
|
34
|
+
provider=LocalProvider(
|
35
|
+
channel=LocalChannel(),
|
36
|
+
init_blocks=1,
|
37
|
+
max_blocks=_max_blocks,
|
38
|
+
min_blocks=_min_blocks,
|
39
|
+
launcher=SingleNodeLauncher(),
|
40
|
+
),
|
41
|
+
)
|
42
|
+
],
|
43
|
+
max_idletime=0.5,
|
44
|
+
strategy='htex_auto_scale',
|
45
|
+
strategy_period=0.1
|
46
|
+
)
|
47
|
+
|
48
|
+
|
49
|
+
# see issue #1885 for details of failures of this test.
|
50
|
+
# at the time of issue #1885 this test was failing frequently
|
51
|
+
# in CI.
|
52
|
+
@pytest.mark.local
|
53
|
+
def test_scaledown_with_register(try_assert):
|
54
|
+
dfk = parsl.dfk()
|
55
|
+
htex = dfk.executors['htex_local']
|
56
|
+
|
57
|
+
num_managers = len(htex.connected_managers())
|
58
|
+
assert num_managers == 0, "Expected 0 managers at start"
|
59
|
+
|
60
|
+
try_assert(lambda: len(htex.status()),
|
61
|
+
fail_msg="Expected 1 block at start")
|
62
|
+
|
63
|
+
s = htex.status()
|
64
|
+
assert s['0'].state == JobState.RUNNING, "Expected block to be in RUNNING"
|
65
|
+
|
66
|
+
def check_zero_blocks():
|
67
|
+
s = htex.status()
|
68
|
+
return len(s) == 1 and s['0'].state in TERMINAL_STATES
|
69
|
+
|
70
|
+
try_assert(
|
71
|
+
check_zero_blocks,
|
72
|
+
fail_msg="Expected 0 blocks after idle scaledown",
|
73
|
+
timeout_ms=15000,
|
74
|
+
)
|
@@ -0,0 +1,113 @@
|
|
1
|
+
import parsl
|
2
|
+
import pytest
|
3
|
+
import zipfile
|
4
|
+
|
5
|
+
from parsl.data_provider.files import File
|
6
|
+
from parsl.data_provider.data_manager import default_staging
|
7
|
+
from parsl.data_provider.zip import ZipAuthorityError, ZipFileStaging
|
8
|
+
|
9
|
+
from parsl.providers import LocalProvider
|
10
|
+
from parsl.channels import LocalChannel
|
11
|
+
from parsl.launchers import SimpleLauncher
|
12
|
+
|
13
|
+
from parsl.config import Config
|
14
|
+
from parsl.executors import HighThroughputExecutor
|
15
|
+
|
16
|
+
from parsl.tests.configs.htex_local import fresh_config as local_config
|
17
|
+
|
18
|
+
|
19
|
+
@pytest.mark.local
|
20
|
+
def test_zip_path_split():
|
21
|
+
from parsl.data_provider.zip import zip_path_split
|
22
|
+
assert zip_path_split("/tmp/foo/this.zip/inside/here.txt") == ("/tmp/foo/this.zip", "inside/here.txt")
|
23
|
+
|
24
|
+
|
25
|
+
@parsl.bash_app
|
26
|
+
def output_something(outputs=()):
|
27
|
+
"""This should output something into every specified output file:
|
28
|
+
the position in the output sequence will be written into the
|
29
|
+
corresponding output file.
|
30
|
+
"""
|
31
|
+
cmds = []
|
32
|
+
for n in range(len(outputs)):
|
33
|
+
cmds.append(f"echo {n} > {outputs[n]}")
|
34
|
+
|
35
|
+
return "; ".join(cmds)
|
36
|
+
|
37
|
+
|
38
|
+
@pytest.mark.local
|
39
|
+
def test_zip_out(tmpd_cwd):
|
40
|
+
# basic test of zip file stage-out
|
41
|
+
zip_path = tmpd_cwd / "container.zip"
|
42
|
+
file_base = "data.txt"
|
43
|
+
of = File(f"zip:{zip_path / file_base}")
|
44
|
+
|
45
|
+
app_future = output_something(outputs=[of])
|
46
|
+
output_file_future = app_future.outputs[0]
|
47
|
+
|
48
|
+
app_future.result()
|
49
|
+
output_file_future.result()
|
50
|
+
|
51
|
+
assert zipfile.is_zipfile(zip_path)
|
52
|
+
|
53
|
+
with zipfile.ZipFile(zip_path) as z:
|
54
|
+
assert file_base in z.namelist()
|
55
|
+
assert len(z.namelist()) == 1
|
56
|
+
with z.open(file_base) as f:
|
57
|
+
assert f.readlines() == [b'0\n']
|
58
|
+
|
59
|
+
|
60
|
+
@pytest.mark.local
|
61
|
+
def test_zip_out_multi(tmpd_cwd):
|
62
|
+
# tests multiple files, multiple zip files and multiple
|
63
|
+
# sub-paths
|
64
|
+
|
65
|
+
zip_path_1 = tmpd_cwd / "container1.zip"
|
66
|
+
zip_path_2 = tmpd_cwd / "container2.zip"
|
67
|
+
|
68
|
+
relative_file_path_1 = "a/b/c/data.txt"
|
69
|
+
relative_file_path_2 = "something.txt"
|
70
|
+
relative_file_path_3 = "a/d/other.txt"
|
71
|
+
of1 = File(f"zip:{zip_path_1 / relative_file_path_1}")
|
72
|
+
of2 = File(f"zip:{zip_path_1 / relative_file_path_2}")
|
73
|
+
of3 = File(f"zip:{zip_path_2 / relative_file_path_3}")
|
74
|
+
|
75
|
+
app_future = output_something(outputs=[of1, of2, of3])
|
76
|
+
|
77
|
+
for f in app_future.outputs:
|
78
|
+
f.result()
|
79
|
+
|
80
|
+
app_future.result()
|
81
|
+
|
82
|
+
assert zipfile.is_zipfile(zip_path_1)
|
83
|
+
|
84
|
+
with zipfile.ZipFile(zip_path_1) as z:
|
85
|
+
assert relative_file_path_1 in z.namelist()
|
86
|
+
assert relative_file_path_2 in z.namelist()
|
87
|
+
assert len(z.namelist()) == 2
|
88
|
+
with z.open(relative_file_path_1) as f:
|
89
|
+
assert f.readlines() == [b'0\n']
|
90
|
+
with z.open(relative_file_path_2) as f:
|
91
|
+
assert f.readlines() == [b'1\n']
|
92
|
+
|
93
|
+
assert zipfile.is_zipfile(zip_path_2)
|
94
|
+
|
95
|
+
with zipfile.ZipFile(zip_path_2) as z:
|
96
|
+
assert relative_file_path_3 in z.namelist()
|
97
|
+
assert len(z.namelist()) == 1
|
98
|
+
with z.open(relative_file_path_3) as f:
|
99
|
+
assert f.readlines() == [b'2\n']
|
100
|
+
|
101
|
+
|
102
|
+
@pytest.mark.local
|
103
|
+
def test_zip_bad_authority(tmpd_cwd):
|
104
|
+
# tests that there's an exception when staging a ZIP url with an authority
|
105
|
+
# section specified, rather than silently ignoring it. This simulates a
|
106
|
+
# user who misunderstands what that piece of what a zip: URL means.
|
107
|
+
|
108
|
+
zip_path = tmpd_cwd / "container.zip"
|
109
|
+
file_base = "data.txt"
|
110
|
+
of = File(f"zip://someauthority/{zip_path / file_base}")
|
111
|
+
|
112
|
+
with pytest.raises(ZipAuthorityError):
|
113
|
+
output_something(outputs=[of])
|
parsl/version.py
CHANGED
@@ -361,7 +361,9 @@ class Manager:
|
|
361
361
|
kill_event.set()
|
362
362
|
else:
|
363
363
|
task_recv_counter += len(tasks)
|
364
|
-
logger.debug("Got executor tasks: {}, cumulative count of tasks: {}".format(
|
364
|
+
logger.debug("Got executor tasks: {}, cumulative count of tasks: {}".format(
|
365
|
+
[t['task_id'] for t in tasks], task_recv_counter
|
366
|
+
))
|
365
367
|
|
366
368
|
for task in tasks:
|
367
369
|
self.task_scheduler.put_task(task)
|
@@ -1,9 +1,9 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: parsl
|
3
|
-
Version: 2024.4.
|
3
|
+
Version: 2024.4.15
|
4
4
|
Summary: Simple data dependent workflows in Python
|
5
5
|
Home-page: https://github.com/Parsl/parsl
|
6
|
-
Download-URL: https://github.com/Parsl/parsl/archive/2024.04.
|
6
|
+
Download-URL: https://github.com/Parsl/parsl/archive/2024.04.15.tar.gz
|
7
7
|
Author: The Parsl Team
|
8
8
|
Author-email: parsl@googlegroups.com
|
9
9
|
License: Apache 2.0
|
@@ -28,6 +28,7 @@ Requires-Dist: requests
|
|
28
28
|
Requires-Dist: paramiko
|
29
29
|
Requires-Dist: psutil >=5.5.1
|
30
30
|
Requires-Dist: setproctitle
|
31
|
+
Requires-Dist: filelock <4,>=3.13
|
31
32
|
Provides-Extra: all
|
32
33
|
Requires-Dist: sqlalchemy <2,>=1.4 ; extra == 'all'
|
33
34
|
Requires-Dist: pydot ; extra == 'all'
|