parsl 2024.4.8__py3-none-any.whl → 2024.4.22__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/addresses.py +2 -2
- parsl/app/bash.py +10 -2
- parsl/app/errors.py +3 -5
- parsl/data_provider/data_manager.py +2 -1
- parsl/data_provider/zip.py +104 -0
- parsl/dataflow/dflow.py +92 -43
- parsl/dataflow/futures.py +26 -12
- parsl/executors/base.py +28 -9
- parsl/executors/high_throughput/executor.py +14 -19
- parsl/executors/high_throughput/process_worker_pool.py +3 -1
- parsl/executors/status_handling.py +81 -1
- parsl/executors/taskvine/executor.py +13 -2
- parsl/executors/workqueue/executor.py +14 -3
- parsl/jobs/job_status_poller.py +19 -113
- parsl/jobs/strategy.py +22 -27
- parsl/monitoring/monitoring.py +29 -23
- parsl/monitoring/radios.py +15 -0
- 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 +7 -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_checkpointing/test_task_exit.py +1 -1
- parsl/tests/test_htex/test_zmq_binding.py +1 -0
- parsl/tests/test_monitoring/test_basic.py +46 -21
- parsl/tests/test_monitoring/test_fuzz_zmq.py +10 -1
- parsl/tests/test_monitoring/test_stdouterr.py +137 -0
- parsl/tests/test_python_apps/test_context_manager.py +3 -3
- parsl/tests/test_python_apps/test_outputs.py +0 -1
- parsl/tests/test_scaling/test_regression_1621.py +11 -11
- parsl/tests/test_scaling/test_scale_down_htex_unregistered.py +74 -0
- parsl/tests/test_staging/test_staging_stdout.py +61 -0
- parsl/tests/test_staging/test_zip_out.py +113 -0
- parsl/utils.py +11 -2
- parsl/version.py +1 -1
- {parsl-2024.4.8.data → parsl-2024.4.22.data}/scripts/process_worker_pool.py +3 -1
- {parsl-2024.4.8.dist-info → parsl-2024.4.22.dist-info}/METADATA +5 -4
- {parsl-2024.4.8.dist-info → parsl-2024.4.22.dist-info}/RECORD +53 -48
- {parsl-2024.4.8.data → parsl-2024.4.22.data}/scripts/exec_parsl_function.py +0 -0
- {parsl-2024.4.8.data → parsl-2024.4.22.data}/scripts/parsl_coprocess.py +0 -0
- {parsl-2024.4.8.dist-info → parsl-2024.4.22.dist-info}/LICENSE +0 -0
- {parsl-2024.4.8.dist-info → parsl-2024.4.22.dist-info}/WHEEL +0 -0
- {parsl-2024.4.8.dist-info → parsl-2024.4.22.dist-info}/entry_points.txt +0 -0
- {parsl-2024.4.8.dist-info → parsl-2024.4.22.dist-info}/top_level.txt +0 -0
parsl/monitoring/router.py
CHANGED
@@ -15,6 +15,8 @@ from parsl.utils import setproctitle
|
|
15
15
|
|
16
16
|
from parsl.monitoring.message_type import MessageType
|
17
17
|
from parsl.monitoring.types import AddressedMonitoringMessage, TaggedMonitoringMessage
|
18
|
+
|
19
|
+
from multiprocessing.synchronize import Event
|
18
20
|
from typing import Optional, Tuple, Union
|
19
21
|
|
20
22
|
|
@@ -98,10 +100,10 @@ class MonitoringRouter:
|
|
98
100
|
priority_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
99
101
|
node_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
100
102
|
block_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
101
|
-
resource_msgs: "queue.Queue[AddressedMonitoringMessage]"
|
103
|
+
resource_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
104
|
+
exit_event: Event) -> None:
|
102
105
|
try:
|
103
|
-
|
104
|
-
while router_keep_going:
|
106
|
+
while not exit_event.is_set():
|
105
107
|
try:
|
106
108
|
data, addr = self.udp_sock.recvfrom(2048)
|
107
109
|
resource_msg = pickle.loads(data)
|
@@ -135,8 +137,6 @@ class MonitoringRouter:
|
|
135
137
|
priority_msgs.put(msg_0)
|
136
138
|
elif msg[0] == MessageType.WORKFLOW_INFO:
|
137
139
|
priority_msgs.put(msg_0)
|
138
|
-
if 'exit_now' in msg[1] and msg[1]['exit_now']:
|
139
|
-
router_keep_going = False
|
140
140
|
else:
|
141
141
|
# There is a type: ignore here because if msg[0]
|
142
142
|
# is of the correct type, this code is unreachable,
|
@@ -178,6 +178,7 @@ def router_starter(comm_q: "queue.Queue[Union[Tuple[int, int], str]]",
|
|
178
178
|
node_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
179
179
|
block_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
180
180
|
resource_msgs: "queue.Queue[AddressedMonitoringMessage]",
|
181
|
+
exit_event: Event,
|
181
182
|
|
182
183
|
hub_address: str,
|
183
184
|
udp_port: Optional[int],
|
@@ -202,7 +203,7 @@ def router_starter(comm_q: "queue.Queue[Union[Tuple[int, int], str]]",
|
|
202
203
|
|
203
204
|
router.logger.info("Starting MonitoringRouter in router_starter")
|
204
205
|
try:
|
205
|
-
router.start(priority_msgs, node_msgs, block_msgs, resource_msgs)
|
206
|
+
router.start(priority_msgs, node_msgs, block_msgs, resource_msgs, exit_event)
|
206
207
|
except Exception as e:
|
207
208
|
router.logger.exception("router.start exception")
|
208
209
|
exception_q.put(('Hub', str(e)))
|
parsl/providers/local/local.py
CHANGED
@@ -266,7 +266,7 @@ class LocalProvider(ExecutionProvider, RepresentationMixin):
|
|
266
266
|
for job in job_ids:
|
267
267
|
job_dict = self.resources[job]
|
268
268
|
job_dict['cancelled'] = True
|
269
|
-
logger.debug("Terminating job/
|
269
|
+
logger.debug("Terminating job/process ID: {0}".format(job))
|
270
270
|
cmd = "kill -- -$(ps -o pgid= {} | grep -o '[0-9]*')".format(job_dict['remote_pid'])
|
271
271
|
retcode, stdout, stderr = self.channel.execute_wait(cmd, self.cmd_timeout)
|
272
272
|
if retcode != 0:
|
@@ -31,6 +31,7 @@ from parsl.executors import HighThroughputExecutor
|
|
31
31
|
from parsl.data_provider.http import HTTPInTaskStaging
|
32
32
|
from parsl.data_provider.ftp import FTPInTaskStaging
|
33
33
|
from parsl.data_provider.file_noop import NoOpFileStaging
|
34
|
+
from parsl.data_provider.zip import ZipFileStaging
|
34
35
|
|
35
36
|
working_dir = os.getcwd() + "/" + "test_htex_alternate"
|
36
37
|
|
@@ -42,7 +43,7 @@ def fresh_config():
|
|
42
43
|
address="127.0.0.1",
|
43
44
|
label="htex_Local",
|
44
45
|
working_dir=working_dir,
|
45
|
-
storage_access=[FTPInTaskStaging(), HTTPInTaskStaging(), NoOpFileStaging()],
|
46
|
+
storage_access=[ZipFileStaging(), FTPInTaskStaging(), HTTPInTaskStaging(), NoOpFileStaging()],
|
46
47
|
worker_debug=True,
|
47
48
|
cores_per_worker=1,
|
48
49
|
heartbeat_period=2,
|
@@ -9,5 +9,4 @@ from parsl.data_provider.file_noop import NoOpFileStaging
|
|
9
9
|
|
10
10
|
def fresh_config():
|
11
11
|
return Config(executors=[TaskVineExecutor(manager_config=TaskVineManagerConfig(port=9000),
|
12
|
-
worker_launch_method='factory'
|
13
|
-
storage_access=[FTPInTaskStaging(), HTTPInTaskStaging(), NoOpFileStaging()])])
|
12
|
+
worker_launch_method='factory')])
|
parsl/tests/conftest.py
CHANGED
@@ -135,28 +135,27 @@ def pytest_configure(config):
|
|
135
135
|
)
|
136
136
|
config.addinivalue_line(
|
137
137
|
'markers',
|
138
|
-
'
|
138
|
+
'cleannet: Enable tests that require a clean network connection (such as for testing FTP)'
|
139
139
|
)
|
140
|
-
|
141
140
|
config.addinivalue_line(
|
142
141
|
'markers',
|
143
|
-
'
|
142
|
+
'staging_required: Marks tests that require a staging provider, when there is no sharedFS)'
|
144
143
|
)
|
145
144
|
config.addinivalue_line(
|
146
145
|
'markers',
|
147
|
-
'
|
146
|
+
'sshd_required: Marks tests that require a SSHD'
|
148
147
|
)
|
149
148
|
config.addinivalue_line(
|
150
149
|
'markers',
|
151
|
-
'
|
150
|
+
'multiple_cores_required: Marks tests that require multiple cores, such as htex affinity'
|
152
151
|
)
|
153
152
|
config.addinivalue_line(
|
154
153
|
'markers',
|
155
|
-
'
|
154
|
+
'issue3328: Marks tests broken by issue #3328'
|
156
155
|
)
|
157
156
|
config.addinivalue_line(
|
158
157
|
'markers',
|
159
|
-
'
|
158
|
+
'executor_supports_std_stream_tuples: Marks tests that require tuple support for stdout/stderr'
|
160
159
|
)
|
161
160
|
|
162
161
|
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import logging
|
1
2
|
import os
|
2
3
|
import random
|
3
4
|
import re
|
@@ -23,7 +24,6 @@ def foo(x, y, z=10, stdout=None, label=None):
|
|
23
24
|
return f"echo {x} {y} {z}"
|
24
25
|
|
25
26
|
|
26
|
-
@pytest.mark.issue363
|
27
27
|
def test_command_format_1(tmpd_cwd):
|
28
28
|
"""Testing command format for BashApps"""
|
29
29
|
|
@@ -38,8 +38,7 @@ def test_command_format_1(tmpd_cwd):
|
|
38
38
|
assert so_content == "1 4 10"
|
39
39
|
|
40
40
|
|
41
|
-
|
42
|
-
def test_auto_log_filename_format():
|
41
|
+
def test_auto_log_filename_format(caplog):
|
43
42
|
"""Testing auto log filename format for BashApps
|
44
43
|
"""
|
45
44
|
app_label = "label_test_auto_log_filename_format"
|
@@ -51,6 +50,8 @@ def test_auto_log_filename_format():
|
|
51
50
|
foo_future.result())
|
52
51
|
|
53
52
|
log_fpath = foo_future.stdout
|
53
|
+
assert isinstance(log_fpath, str)
|
54
|
+
|
54
55
|
log_pattern = fr".*/task_\d+_foo_{app_label}"
|
55
56
|
assert re.match(log_pattern, log_fpath), 'Output file "{0}" does not match pattern "{1}"'.format(
|
56
57
|
log_fpath, log_pattern)
|
@@ -61,8 +62,10 @@ def test_auto_log_filename_format():
|
|
61
62
|
assert contents == '1 {0} 10\n'.format(rand_int), \
|
62
63
|
'Output does not match expected string "1 {0} 10", Got: "{1}"'.format(rand_int, contents)
|
63
64
|
|
65
|
+
for record in caplog.records:
|
66
|
+
assert record.levelno < logging.ERROR
|
67
|
+
|
64
68
|
|
65
|
-
@pytest.mark.issue363
|
66
69
|
def test_parallel_for(tmpd_cwd, n=3):
|
67
70
|
"""Testing a simple parallel for loop"""
|
68
71
|
outdir = tmpd_cwd / "outputs/test_parallel"
|
@@ -76,7 +76,6 @@ def test_div_0(test_fn=div_0):
|
|
76
76
|
os.remove('std.out')
|
77
77
|
|
78
78
|
|
79
|
-
@pytest.mark.issue363
|
80
79
|
def test_bash_misuse(test_fn=bash_misuse):
|
81
80
|
err_code = test_matrix[test_fn]['exit_code']
|
82
81
|
f = test_fn()
|
@@ -91,7 +90,6 @@ def test_bash_misuse(test_fn=bash_misuse):
|
|
91
90
|
os.remove('std.out')
|
92
91
|
|
93
92
|
|
94
|
-
@pytest.mark.issue363
|
95
93
|
def test_command_not_found(test_fn=command_not_found):
|
96
94
|
err_code = test_matrix[test_fn]['exit_code']
|
97
95
|
f = test_fn()
|
@@ -108,7 +106,6 @@ def test_command_not_found(test_fn=command_not_found):
|
|
108
106
|
return True
|
109
107
|
|
110
108
|
|
111
|
-
@pytest.mark.issue363
|
112
109
|
def test_not_executable(test_fn=not_executable):
|
113
110
|
err_code = test_matrix[test_fn]['exit_code']
|
114
111
|
f = test_fn()
|
@@ -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
|
@@ -53,6 +53,7 @@ def test_interchange_binding_with_address(cert_dir: Optional[str]):
|
|
53
53
|
assert ix.interchange_address == address
|
54
54
|
|
55
55
|
|
56
|
+
@pytest.mark.skip("This behaviour is possibly unexpected. See issue #3037")
|
56
57
|
@pytest.mark.local
|
57
58
|
@pytest.mark.parametrize("encrypted", (True, False), indirect=True)
|
58
59
|
def test_interchange_binding_with_non_ipv4_address(cert_dir: Optional[str]):
|
@@ -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,137 @@
|
|
1
|
+
"""Tests monitoring records app name under various decoration patterns.
|
2
|
+
"""
|
3
|
+
|
4
|
+
import logging
|
5
|
+
import os
|
6
|
+
import parsl
|
7
|
+
import pytest
|
8
|
+
import re
|
9
|
+
import time
|
10
|
+
|
11
|
+
from typing import Union
|
12
|
+
|
13
|
+
from parsl.config import Config
|
14
|
+
from parsl.data_provider.files import File
|
15
|
+
from parsl.data_provider.data_manager import default_staging
|
16
|
+
from parsl.data_provider.staging import Staging
|
17
|
+
from parsl.executors import HighThroughputExecutor
|
18
|
+
from parsl.monitoring import MonitoringHub
|
19
|
+
from parsl.providers import LocalProvider
|
20
|
+
|
21
|
+
|
22
|
+
def fresh_config(run_dir):
|
23
|
+
return Config(
|
24
|
+
run_dir=str(run_dir),
|
25
|
+
executors=[
|
26
|
+
HighThroughputExecutor(
|
27
|
+
address="127.0.0.1",
|
28
|
+
label="htex_Local",
|
29
|
+
provider=LocalProvider(
|
30
|
+
init_blocks=1,
|
31
|
+
min_blocks=1,
|
32
|
+
max_blocks=1,
|
33
|
+
)
|
34
|
+
)
|
35
|
+
],
|
36
|
+
strategy='simple',
|
37
|
+
strategy_period=0.1,
|
38
|
+
monitoring=MonitoringHub(
|
39
|
+
hub_address="localhost",
|
40
|
+
hub_port=55055,
|
41
|
+
)
|
42
|
+
)
|
43
|
+
|
44
|
+
|
45
|
+
@parsl.python_app
|
46
|
+
def stdapp(stdout=None, stderr=None):
|
47
|
+
pass
|
48
|
+
|
49
|
+
|
50
|
+
class ArbitraryPathLike(os.PathLike):
|
51
|
+
def __init__(self, path: Union[str, bytes]) -> None:
|
52
|
+
self.path = path
|
53
|
+
|
54
|
+
def __fspath__(self) -> Union[str, bytes]:
|
55
|
+
return self.path
|
56
|
+
|
57
|
+
|
58
|
+
class ArbitraryStaging(Staging):
|
59
|
+
"""This staging provider will not actually do any staging, but will
|
60
|
+
accept arbitrary: scheme URLs. That's enough for this monitoring test
|
61
|
+
which doesn't need any actual stage out action to happen.
|
62
|
+
"""
|
63
|
+
def can_stage_out(self, file):
|
64
|
+
return file.scheme == "arbitrary"
|
65
|
+
|
66
|
+
|
67
|
+
@pytest.mark.local
|
68
|
+
@pytest.mark.parametrize('stdx,expected_stdx',
|
69
|
+
[('hello.txt', 'hello.txt'),
|
70
|
+
(None, ''),
|
71
|
+
(('tuple.txt', 'w'), 'tuple.txt'),
|
72
|
+
(ArbitraryPathLike('pl.txt'), 'pl.txt'),
|
73
|
+
(ArbitraryPathLike(b'pl2.txt'), 'pl2.txt'),
|
74
|
+
((ArbitraryPathLike('pl3.txt'), 'w'), 'pl3.txt'),
|
75
|
+
((ArbitraryPathLike(b'pl4.txt'), 'w'), 'pl4.txt'),
|
76
|
+
(parsl.AUTO_LOGNAME,
|
77
|
+
lambda p:
|
78
|
+
isinstance(p, str) and
|
79
|
+
os.path.isabs(p) and
|
80
|
+
re.match("^.*/task_0000_stdapp\\.std...$", p)),
|
81
|
+
(File("arbitrary:abc123"), "arbitrary:abc123"),
|
82
|
+
(File("file:///tmp/pl5"), "file:///tmp/pl5"),
|
83
|
+
])
|
84
|
+
@pytest.mark.parametrize('stream', ['stdout', 'stderr'])
|
85
|
+
def test_stdstream_to_monitoring(stdx, expected_stdx, stream, tmpd_cwd, caplog):
|
86
|
+
"""This tests that various forms of stdout/err specification are
|
87
|
+
represented in monitoring correctly. The stderr and stdout codepaths
|
88
|
+
are generally duplicated, rather than factorised, and so this test
|
89
|
+
runs the same tests on both stdout and stderr.
|
90
|
+
"""
|
91
|
+
|
92
|
+
# this is imported here rather than at module level because
|
93
|
+
# it isn't available in a plain parsl install, so this module
|
94
|
+
# would otherwise fail to import and break even a basic test
|
95
|
+
# run.
|
96
|
+
import sqlalchemy
|
97
|
+
|
98
|
+
c = fresh_config(tmpd_cwd)
|
99
|
+
c.monitoring.logging_endpoint = f"sqlite:///{tmpd_cwd}/monitoring.db"
|
100
|
+
c.executors[0].storage_access = default_staging + [ArbitraryStaging()]
|
101
|
+
|
102
|
+
with parsl.load(c):
|
103
|
+
kwargs = {stream: stdx}
|
104
|
+
stdapp(**kwargs).result()
|
105
|
+
|
106
|
+
parsl.clear()
|
107
|
+
|
108
|
+
engine = sqlalchemy.create_engine(c.monitoring.logging_endpoint)
|
109
|
+
with engine.begin() as connection:
|
110
|
+
|
111
|
+
def count_rows(table: str):
|
112
|
+
result = connection.execute(f"SELECT COUNT(*) FROM {table}")
|
113
|
+
(c, ) = result.first()
|
114
|
+
return c
|
115
|
+
|
116
|
+
# one workflow...
|
117
|
+
assert count_rows("workflow") == 1
|
118
|
+
|
119
|
+
# ... with one task ...
|
120
|
+
assert count_rows("task") == 1
|
121
|
+
|
122
|
+
# ... that was tried once ...
|
123
|
+
assert count_rows("try") == 1
|
124
|
+
|
125
|
+
# ... and has the expected name.
|
126
|
+
result = connection.execute(f"SELECT task_{stream} FROM task")
|
127
|
+
(c, ) = result.first()
|
128
|
+
|
129
|
+
if isinstance(expected_stdx, str):
|
130
|
+
assert c == expected_stdx
|
131
|
+
elif callable(expected_stdx):
|
132
|
+
assert expected_stdx(c)
|
133
|
+
else:
|
134
|
+
raise RuntimeError("Bad expected_stdx value")
|
135
|
+
|
136
|
+
for record in caplog.records:
|
137
|
+
assert record.levelno < logging.ERROR
|
@@ -24,15 +24,15 @@ def local_teardown():
|
|
24
24
|
|
25
25
|
|
26
26
|
@pytest.mark.local
|
27
|
-
def test_within_context_manger():
|
27
|
+
def test_within_context_manger(tmpd_cwd):
|
28
28
|
config = fresh_config()
|
29
29
|
with parsl.load(config=config) as dfk:
|
30
30
|
assert isinstance(dfk, DataFlowKernel)
|
31
31
|
|
32
|
-
bash_future = foo(1)
|
32
|
+
bash_future = foo(1, stdout=tmpd_cwd / 'foo.stdout')
|
33
33
|
assert bash_future.result() == 0
|
34
34
|
|
35
|
-
with open('foo.stdout', 'r') as f:
|
35
|
+
with open(tmpd_cwd / 'foo.stdout', 'r') as f:
|
36
36
|
assert f.read() == "2\n"
|
37
37
|
|
38
38
|
with pytest.raises(NoDataFlowKernelError) as excinfo:
|
@@ -9,6 +9,14 @@ from parsl.executors import HighThroughputExecutor
|
|
9
9
|
from parsl.launchers import SimpleLauncher
|
10
10
|
from parsl.providers import LocalProvider
|
11
11
|
|
12
|
+
# Timing notes:
|
13
|
+
# The configured strategy_period must be much smaller than the delay in
|
14
|
+
# app() so that multiple iterations of the strategy have had a chance
|
15
|
+
# to (mis)behave.
|
16
|
+
# The status polling interval in OneShotLocalProvider must be much bigger
|
17
|
+
# than the above times, so that the job status cached from the provider
|
18
|
+
# will not be updated while the single invocation of app() runs.
|
19
|
+
|
12
20
|
|
13
21
|
@parsl.python_app
|
14
22
|
def app():
|
@@ -55,20 +63,12 @@ def test_one_block(tmpd_cwd):
|
|
55
63
|
)
|
56
64
|
],
|
57
65
|
strategy='simple',
|
66
|
+
strategy_period=0.1
|
58
67
|
)
|
59
68
|
|
60
|
-
parsl.load(config)
|
61
|
-
|
62
|
-
|
63
|
-
def poller():
|
64
|
-
import time
|
65
|
-
while True:
|
66
|
-
dfk.job_status_poller.poll()
|
67
|
-
time.sleep(0.1)
|
69
|
+
with parsl.load(config):
|
70
|
+
app().result()
|
68
71
|
|
69
|
-
threading.Thread(target=poller, daemon=True).start()
|
70
|
-
app().result()
|
71
|
-
parsl.dfk().cleanup()
|
72
72
|
parsl.clear()
|
73
73
|
|
74
74
|
assert oneshot_provider.recorded_submits == 1
|