parsl 2024.4.22__py3-none-any.whl → 2024.4.29__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/config.py +10 -1
- parsl/data_provider/zip.py +32 -0
- parsl/dataflow/dflow.py +33 -23
- parsl/executors/high_throughput/executor.py +7 -1
- parsl/providers/kubernetes/kube.py +20 -1
- parsl/tests/configs/local_threads_checkpoint_periodic.py +8 -10
- parsl/tests/conftest.py +12 -1
- parsl/tests/test_bash_apps/test_std_uri.py +128 -0
- parsl/tests/test_checkpointing/test_periodic.py +20 -33
- parsl/tests/test_htex/test_basic.py +2 -2
- parsl/tests/test_htex/test_missing_worker.py +0 -4
- parsl/tests/test_mpi_apps/test_resource_spec.py +2 -8
- parsl/tests/test_staging/test_zip_in.py +42 -0
- parsl/tests/test_staging/test_zip_to_zip.py +44 -0
- parsl/tests/unit/__init__.py +0 -0
- parsl/tests/unit/test_file.py +99 -0
- parsl/usage_tracking/api.py +66 -0
- parsl/usage_tracking/usage.py +39 -26
- parsl/version.py +1 -1
- {parsl-2024.4.22.dist-info → parsl-2024.4.29.dist-info}/METADATA +2 -2
- {parsl-2024.4.22.dist-info → parsl-2024.4.29.dist-info}/RECORD +28 -22
- {parsl-2024.4.22.data → parsl-2024.4.29.data}/scripts/exec_parsl_function.py +0 -0
- {parsl-2024.4.22.data → parsl-2024.4.29.data}/scripts/parsl_coprocess.py +0 -0
- {parsl-2024.4.22.data → parsl-2024.4.29.data}/scripts/process_worker_pool.py +0 -0
- {parsl-2024.4.22.dist-info → parsl-2024.4.29.dist-info}/LICENSE +0 -0
- {parsl-2024.4.22.dist-info → parsl-2024.4.29.dist-info}/WHEEL +0 -0
- {parsl-2024.4.22.dist-info → parsl-2024.4.29.dist-info}/entry_points.txt +0 -0
- {parsl-2024.4.22.dist-info → parsl-2024.4.29.dist-info}/top_level.txt +0 -0
parsl/config.py
CHANGED
@@ -10,11 +10,12 @@ from parsl.executors.threads import ThreadPoolExecutor
|
|
10
10
|
from parsl.errors import ConfigurationError
|
11
11
|
from parsl.dataflow.taskrecord import TaskRecord
|
12
12
|
from parsl.monitoring import MonitoringHub
|
13
|
+
from parsl.usage_tracking.api import UsageInformation
|
13
14
|
|
14
15
|
logger = logging.getLogger(__name__)
|
15
16
|
|
16
17
|
|
17
|
-
class Config(RepresentationMixin):
|
18
|
+
class Config(RepresentationMixin, UsageInformation):
|
18
19
|
"""
|
19
20
|
Specification of Parsl configuration options.
|
20
21
|
|
@@ -50,6 +51,9 @@ class Config(RepresentationMixin):
|
|
50
51
|
of 1.
|
51
52
|
run_dir : str, optional
|
52
53
|
Path to run directory. Default is 'runinfo'.
|
54
|
+
std_autopath : function, optional
|
55
|
+
Sets the function used to generate stdout/stderr specifications when parsl.AUTO_LOGPATH is used. If no function
|
56
|
+
is specified, generates paths that look like: ``rundir/NNN/task_logs/X/task_{id}_{name}{label}.{out/err}``
|
53
57
|
strategy : str, optional
|
54
58
|
Strategy to use for scaling blocks according to workflow needs. Can be 'simple', 'htex_auto_scale', 'none'
|
55
59
|
or `None`.
|
@@ -89,6 +93,7 @@ class Config(RepresentationMixin):
|
|
89
93
|
retries: int = 0,
|
90
94
|
retry_handler: Optional[Callable[[Exception, TaskRecord], float]] = None,
|
91
95
|
run_dir: str = 'runinfo',
|
96
|
+
std_autopath: Optional[Callable] = None,
|
92
97
|
strategy: Optional[str] = 'simple',
|
93
98
|
strategy_period: Union[float, int] = 5,
|
94
99
|
max_idletime: float = 120.0,
|
@@ -129,6 +134,7 @@ class Config(RepresentationMixin):
|
|
129
134
|
self.usage_tracking = usage_tracking
|
130
135
|
self.initialize_logging = initialize_logging
|
131
136
|
self.monitoring = monitoring
|
137
|
+
self.std_autopath: Optional[Callable] = std_autopath
|
132
138
|
|
133
139
|
@property
|
134
140
|
def executors(self) -> Sequence[ParslExecutor]:
|
@@ -144,3 +150,6 @@ class Config(RepresentationMixin):
|
|
144
150
|
if len(duplicates) > 0:
|
145
151
|
raise ConfigurationError('Executors must have unique labels ({})'.format(
|
146
152
|
', '.join(['label={}'.format(repr(d)) for d in duplicates])))
|
153
|
+
|
154
|
+
def get_usage_information(self):
|
155
|
+
return {"executors_len": len(self.executors)}
|
parsl/data_provider/zip.py
CHANGED
@@ -42,6 +42,12 @@ class ZipFileStaging(Staging):
|
|
42
42
|
"""
|
43
43
|
|
44
44
|
def can_stage_out(self, file: File) -> bool:
|
45
|
+
return self.is_zip_url(file)
|
46
|
+
|
47
|
+
def can_stage_in(self, file: File) -> bool:
|
48
|
+
return self.is_zip_url(file)
|
49
|
+
|
50
|
+
def is_zip_url(self, file: File) -> bool:
|
45
51
|
logger.debug("archive provider checking File {}".format(repr(file)))
|
46
52
|
|
47
53
|
# First check if this is the scheme we care about
|
@@ -76,6 +82,20 @@ class ZipFileStaging(Staging):
|
|
76
82
|
app_fut = stage_out_app(zip_path, inside_path, working_dir, inputs=[file], _parsl_staging_inhibit=True, parent_fut=parent_fut)
|
77
83
|
return app_fut
|
78
84
|
|
85
|
+
def stage_in(self, dm, executor, file, parent_fut):
|
86
|
+
assert file.scheme == 'zip'
|
87
|
+
|
88
|
+
zip_path, inside_path = zip_path_split(file.path)
|
89
|
+
|
90
|
+
working_dir = dm.dfk.executors[executor].working_dir
|
91
|
+
|
92
|
+
if working_dir:
|
93
|
+
file.local_path = os.path.join(working_dir, inside_path)
|
94
|
+
|
95
|
+
stage_in_app = _zip_stage_in_app(dm)
|
96
|
+
app_fut = stage_in_app(zip_path, inside_path, working_dir, outputs=[file], _parsl_staging_inhibit=True, parent_fut=parent_fut)
|
97
|
+
return app_fut._outputs[0]
|
98
|
+
|
79
99
|
|
80
100
|
def _zip_stage_out(zip_file, inside_path, working_dir, parent_fut=None, inputs=[], _parsl_staging_inhibit=True):
|
81
101
|
file = inputs[0]
|
@@ -93,6 +113,18 @@ def _zip_stage_out_app(dm):
|
|
93
113
|
return parsl.python_app(executors=['_parsl_internal'], data_flow_kernel=dm.dfk)(_zip_stage_out)
|
94
114
|
|
95
115
|
|
116
|
+
def _zip_stage_in(zip_file, inside_path, working_dir, *, parent_fut, outputs, _parsl_staging_inhibit=True):
|
117
|
+
with filelock.FileLock(zip_file + ".lock"):
|
118
|
+
with zipfile.ZipFile(zip_file, mode='r') as z:
|
119
|
+
content = z.read(inside_path)
|
120
|
+
with open(outputs[0], "wb") as of:
|
121
|
+
of.write(content)
|
122
|
+
|
123
|
+
|
124
|
+
def _zip_stage_in_app(dm):
|
125
|
+
return parsl.python_app(executors=['_parsl_internal'], data_flow_kernel=dm.dfk)(_zip_stage_in)
|
126
|
+
|
127
|
+
|
96
128
|
def zip_path_split(path: str) -> Tuple[str, str]:
|
97
129
|
"""Split zip: path into a zipfile name and a contained-file name.
|
98
130
|
"""
|
parsl/dataflow/dflow.py
CHANGED
@@ -798,7 +798,6 @@ class DataFlowKernel:
|
|
798
798
|
# be the original function wrapped with an in-task stageout wrapper), a
|
799
799
|
# rewritten File object to be passed to task to be executed
|
800
800
|
|
801
|
-
@typechecked
|
802
801
|
def stageout_one_file(file: File, rewritable_func: Callable):
|
803
802
|
if not self.check_staging_inhibited(kwargs):
|
804
803
|
# replace a File with a DataFuture - either completing when the stageout
|
@@ -996,32 +995,16 @@ class DataFlowKernel:
|
|
996
995
|
executor = random.choice(choices)
|
997
996
|
logger.debug("Task {} will be sent to executor {}".format(task_id, executor))
|
998
997
|
|
999
|
-
# The below uses func.__name__ before it has been wrapped by any staging code.
|
1000
|
-
|
1001
|
-
label = app_kwargs.get('label')
|
1002
|
-
for kw in ['stdout', 'stderr']:
|
1003
|
-
if kw in app_kwargs:
|
1004
|
-
if app_kwargs[kw] == parsl.AUTO_LOGNAME:
|
1005
|
-
if kw not in ignore_for_cache:
|
1006
|
-
ignore_for_cache += [kw]
|
1007
|
-
app_kwargs[kw] = os.path.join(
|
1008
|
-
self.run_dir,
|
1009
|
-
'task_logs',
|
1010
|
-
str(int(task_id / 10000)).zfill(4), # limit logs to 10k entries per directory
|
1011
|
-
'task_{}_{}{}.{}'.format(
|
1012
|
-
str(task_id).zfill(4),
|
1013
|
-
func.__name__,
|
1014
|
-
'' if label is None else '_{}'.format(label),
|
1015
|
-
kw)
|
1016
|
-
)
|
1017
|
-
|
1018
998
|
resource_specification = app_kwargs.get('parsl_resource_specification', {})
|
1019
999
|
|
1020
1000
|
task_record: TaskRecord
|
1021
|
-
task_record = {'
|
1001
|
+
task_record = {'args': app_args,
|
1002
|
+
'depends': [],
|
1022
1003
|
'dfk': self,
|
1023
1004
|
'executor': executor,
|
1005
|
+
'func': func,
|
1024
1006
|
'func_name': func.__name__,
|
1007
|
+
'kwargs': app_kwargs,
|
1025
1008
|
'memoize': cache,
|
1026
1009
|
'hashsum': None,
|
1027
1010
|
'exec_fu': None,
|
@@ -1043,18 +1026,30 @@ class DataFlowKernel:
|
|
1043
1026
|
|
1044
1027
|
self.update_task_state(task_record, States.unsched)
|
1045
1028
|
|
1029
|
+
for kw in ['stdout', 'stderr']:
|
1030
|
+
if kw in app_kwargs:
|
1031
|
+
if app_kwargs[kw] == parsl.AUTO_LOGNAME:
|
1032
|
+
if kw not in ignore_for_cache:
|
1033
|
+
ignore_for_cache += [kw]
|
1034
|
+
if self.config.std_autopath is None:
|
1035
|
+
app_kwargs[kw] = self.default_std_autopath(task_record, kw)
|
1036
|
+
else:
|
1037
|
+
app_kwargs[kw] = self.config.std_autopath(task_record, kw)
|
1038
|
+
|
1046
1039
|
app_fu = AppFuture(task_record)
|
1040
|
+
task_record['app_fu'] = app_fu
|
1047
1041
|
|
1048
1042
|
# Transform remote input files to data futures
|
1049
1043
|
app_args, app_kwargs, func = self._add_input_deps(executor, app_args, app_kwargs, func)
|
1050
1044
|
|
1051
1045
|
func = self._add_output_deps(executor, app_args, app_kwargs, app_fu, func)
|
1052
1046
|
|
1047
|
+
# Replace the function invocation in the TaskRecord with whatever file-staging
|
1048
|
+
# substitutions have been made.
|
1053
1049
|
task_record.update({
|
1054
1050
|
'args': app_args,
|
1055
1051
|
'func': func,
|
1056
|
-
'kwargs': app_kwargs
|
1057
|
-
'app_fu': app_fu})
|
1052
|
+
'kwargs': app_kwargs})
|
1058
1053
|
|
1059
1054
|
assert task_id not in self.tasks
|
1060
1055
|
|
@@ -1245,8 +1240,10 @@ class DataFlowKernel:
|
|
1245
1240
|
self._checkpoint_timer.close()
|
1246
1241
|
|
1247
1242
|
# Send final stats
|
1243
|
+
logger.info("Sending end message for usage tracking")
|
1248
1244
|
self.usage_tracker.send_end_message()
|
1249
1245
|
self.usage_tracker.close()
|
1246
|
+
logger.info("Closed usage tracking")
|
1250
1247
|
|
1251
1248
|
logger.info("Closing job status poller")
|
1252
1249
|
self.job_status_poller.close()
|
@@ -1440,6 +1437,19 @@ class DataFlowKernel:
|
|
1440
1437
|
log_std_stream("Standard out", task_record['app_fu'].stdout)
|
1441
1438
|
log_std_stream("Standard error", task_record['app_fu'].stderr)
|
1442
1439
|
|
1440
|
+
def default_std_autopath(self, taskrecord, kw):
|
1441
|
+
label = taskrecord['kwargs'].get('label')
|
1442
|
+
task_id = taskrecord['id']
|
1443
|
+
return os.path.join(
|
1444
|
+
self.run_dir,
|
1445
|
+
'task_logs',
|
1446
|
+
str(int(task_id / 10000)).zfill(4), # limit logs to 10k entries per directory
|
1447
|
+
'task_{}_{}{}.{}'.format(
|
1448
|
+
str(task_id).zfill(4),
|
1449
|
+
taskrecord['func_name'],
|
1450
|
+
'' if label is None else '_{}'.format(label),
|
1451
|
+
kw))
|
1452
|
+
|
1443
1453
|
|
1444
1454
|
class DataFlowKernelLoader:
|
1445
1455
|
"""Manage which DataFlowKernel is active.
|
@@ -14,6 +14,7 @@ import math
|
|
14
14
|
import warnings
|
15
15
|
|
16
16
|
import parsl.launchers
|
17
|
+
from parsl.usage_tracking.api import UsageInformation
|
17
18
|
from parsl.serialize import pack_res_spec_apply_message, deserialize
|
18
19
|
from parsl.serialize.errors import SerializationError, DeserializationError
|
19
20
|
from parsl.app.errors import RemoteExceptionWrapper
|
@@ -62,7 +63,7 @@ DEFAULT_LAUNCH_CMD = ("process_worker_pool.py {debug} {max_workers_per_node} "
|
|
62
63
|
"--available-accelerators {accelerators}")
|
63
64
|
|
64
65
|
|
65
|
-
class HighThroughputExecutor(BlockProviderExecutor, RepresentationMixin):
|
66
|
+
class HighThroughputExecutor(BlockProviderExecutor, RepresentationMixin, UsageInformation):
|
66
67
|
"""Executor designed for cluster-scale
|
67
68
|
|
68
69
|
The HighThroughputExecutor system has the following components:
|
@@ -818,4 +819,9 @@ class HighThroughputExecutor(BlockProviderExecutor, RepresentationMixin):
|
|
818
819
|
logger.info("Unable to terminate Interchange process; sending SIGKILL")
|
819
820
|
self.interchange_proc.kill()
|
820
821
|
|
822
|
+
self.interchange_proc.close()
|
823
|
+
|
821
824
|
logger.info("Finished HighThroughputExecutor shutdown attempt")
|
825
|
+
|
826
|
+
def get_usage_information(self):
|
827
|
+
return {"mpi": self.enable_mpi_mode}
|
@@ -105,7 +105,26 @@ class KubernetesProvider(ExecutionProvider, RepresentationMixin):
|
|
105
105
|
if not _kubernetes_enabled:
|
106
106
|
raise OptionalModuleMissing(['kubernetes'],
|
107
107
|
"Kubernetes provider requires kubernetes module and config.")
|
108
|
-
|
108
|
+
try:
|
109
|
+
config.load_kube_config()
|
110
|
+
except config.config_exception.ConfigException:
|
111
|
+
# `load_kube_config` assumes a local kube-config file, and fails if not
|
112
|
+
# present, raising:
|
113
|
+
#
|
114
|
+
# kubernetes.config.config_exception.ConfigException: Invalid
|
115
|
+
# kube-config file. No configuration found.
|
116
|
+
#
|
117
|
+
# Since running a parsl driver script on a kubernetes cluster is a common
|
118
|
+
# pattern to enable worker-interchange communication, this enables an
|
119
|
+
# in-cluster config to be loaded if a kube-config file isn't found.
|
120
|
+
#
|
121
|
+
# Based on: https://github.com/kubernetes-client/python/issues/1005
|
122
|
+
try:
|
123
|
+
config.load_incluster_config()
|
124
|
+
except config.config_exception.ConfigException:
|
125
|
+
raise config.config_exception.ConfigException(
|
126
|
+
"Failed to load both kube-config file and in-cluster configuration."
|
127
|
+
)
|
109
128
|
|
110
129
|
self.namespace = namespace
|
111
130
|
self.image = image
|
@@ -1,13 +1,11 @@
|
|
1
1
|
from parsl.config import Config
|
2
2
|
from parsl.executors.threads import ThreadPoolExecutor
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
checkpoint_period='00:00:05'
|
13
|
-
)
|
4
|
+
|
5
|
+
def fresh_config():
|
6
|
+
tpe = ThreadPoolExecutor(label='local_threads_checkpoint_periodic', max_threads=1)
|
7
|
+
return Config(
|
8
|
+
executors=[tpe],
|
9
|
+
checkpoint_mode='periodic',
|
10
|
+
checkpoint_period='00:00:02'
|
11
|
+
)
|
parsl/tests/conftest.py
CHANGED
@@ -3,8 +3,10 @@ import itertools
|
|
3
3
|
import logging
|
4
4
|
import os
|
5
5
|
import pathlib
|
6
|
+
import random
|
6
7
|
import re
|
7
8
|
import shutil
|
9
|
+
import string
|
8
10
|
import time
|
9
11
|
import types
|
10
12
|
import signal
|
@@ -139,7 +141,7 @@ def pytest_configure(config):
|
|
139
141
|
)
|
140
142
|
config.addinivalue_line(
|
141
143
|
'markers',
|
142
|
-
'staging_required: Marks tests that require a staging provider, when there is no sharedFS
|
144
|
+
'staging_required: Marks tests that require a staging provider, when there is no sharedFS'
|
143
145
|
)
|
144
146
|
config.addinivalue_line(
|
145
147
|
'markers',
|
@@ -245,6 +247,7 @@ def load_dfk_local_module(request, pytestconfig, tmpd_cwd_session):
|
|
245
247
|
|
246
248
|
if callable(local_teardown):
|
247
249
|
local_teardown()
|
250
|
+
assert DataFlowKernelLoader._dfk is None, "Expected teardown to clear DFK"
|
248
251
|
|
249
252
|
if local_config:
|
250
253
|
if parsl.dfk() != dfk:
|
@@ -421,3 +424,11 @@ def try_assert():
|
|
421
424
|
raise AssertionError("Bad assert call: no attempts or timeout period")
|
422
425
|
|
423
426
|
yield _impl
|
427
|
+
|
428
|
+
|
429
|
+
@pytest.fixture
|
430
|
+
def randomstring():
|
431
|
+
def func(length=5, alphabet=string.ascii_letters):
|
432
|
+
return "".join(random.choice(alphabet) for _ in range(length))
|
433
|
+
|
434
|
+
return func
|
@@ -0,0 +1,128 @@
|
|
1
|
+
import logging
|
2
|
+
import parsl
|
3
|
+
import pytest
|
4
|
+
import zipfile
|
5
|
+
|
6
|
+
from functools import partial
|
7
|
+
from parsl.app.futures import DataFuture
|
8
|
+
from parsl.data_provider.files import File
|
9
|
+
from parsl.executors import ThreadPoolExecutor
|
10
|
+
|
11
|
+
|
12
|
+
@parsl.bash_app
|
13
|
+
def app_stdout(stdout=parsl.AUTO_LOGNAME):
|
14
|
+
return "echo hello"
|
15
|
+
|
16
|
+
|
17
|
+
def const_str(cpath, task_record, err_or_out):
|
18
|
+
return cpath
|
19
|
+
|
20
|
+
|
21
|
+
def const_with_cpath(autopath_specifier, content_path, caplog):
|
22
|
+
with parsl.load(parsl.Config(std_autopath=partial(const_str, autopath_specifier))):
|
23
|
+
fut = app_stdout()
|
24
|
+
|
25
|
+
# we don't have to wait for a result to check this attributes
|
26
|
+
assert fut.stdout is autopath_specifier
|
27
|
+
|
28
|
+
# there is no DataFuture to wait for in the str case: the model is that
|
29
|
+
# the stdout will be immediately available on task completion.
|
30
|
+
fut.result()
|
31
|
+
|
32
|
+
with open(content_path, "r") as file:
|
33
|
+
assert file.readlines() == ["hello\n"]
|
34
|
+
|
35
|
+
for record in caplog.records:
|
36
|
+
assert record.levelno < logging.ERROR
|
37
|
+
|
38
|
+
parsl.clear()
|
39
|
+
|
40
|
+
|
41
|
+
@pytest.mark.local
|
42
|
+
def test_std_autopath_const_str(caplog, tmpd_cwd):
|
43
|
+
"""Tests str and tuple mode autopaths with constant autopath, which should
|
44
|
+
all be passed through unmodified.
|
45
|
+
"""
|
46
|
+
cpath = str(tmpd_cwd / "CONST")
|
47
|
+
const_with_cpath(cpath, cpath, caplog)
|
48
|
+
|
49
|
+
|
50
|
+
@pytest.mark.local
|
51
|
+
def test_std_autopath_const_pathlike(caplog, tmpd_cwd):
|
52
|
+
cpath = tmpd_cwd / "CONST"
|
53
|
+
const_with_cpath(cpath, cpath, caplog)
|
54
|
+
|
55
|
+
|
56
|
+
@pytest.mark.local
|
57
|
+
def test_std_autopath_const_tuples(caplog, tmpd_cwd):
|
58
|
+
file = tmpd_cwd / "CONST"
|
59
|
+
cpath = (file, "w")
|
60
|
+
const_with_cpath(cpath, file, caplog)
|
61
|
+
|
62
|
+
|
63
|
+
class URIFailError(Exception):
|
64
|
+
pass
|
65
|
+
|
66
|
+
|
67
|
+
def fail_uri(task_record, err_or_out):
|
68
|
+
raise URIFailError("Deliberate failure in std stream filename generation")
|
69
|
+
|
70
|
+
|
71
|
+
@pytest.mark.local
|
72
|
+
def test_std_autopath_fail(caplog):
|
73
|
+
with parsl.load(parsl.Config(std_autopath=fail_uri)):
|
74
|
+
with pytest.raises(URIFailError):
|
75
|
+
app_stdout()
|
76
|
+
|
77
|
+
parsl.clear()
|
78
|
+
|
79
|
+
|
80
|
+
@parsl.bash_app
|
81
|
+
def app_both(stdout=parsl.AUTO_LOGNAME, stderr=parsl.AUTO_LOGNAME):
|
82
|
+
return "echo hello; echo goodbye >&2"
|
83
|
+
|
84
|
+
|
85
|
+
def zip_uri(base, task_record, err_or_out):
|
86
|
+
"""Should generate Files in base.zip like app_both.0.out or app_both.123.err"""
|
87
|
+
zip_path = base / "base.zip"
|
88
|
+
file = f"{task_record['func_name']}.{task_record['id']}.{task_record['try_id']}.{err_or_out}"
|
89
|
+
return File(f"zip:{zip_path}/{file}")
|
90
|
+
|
91
|
+
|
92
|
+
@pytest.mark.local
|
93
|
+
def test_std_autopath_zip(caplog, tmpd_cwd):
|
94
|
+
with parsl.load(parsl.Config(run_dir=str(tmpd_cwd),
|
95
|
+
executors=[ThreadPoolExecutor(working_dir=str(tmpd_cwd))],
|
96
|
+
std_autopath=partial(zip_uri, tmpd_cwd))):
|
97
|
+
futs = []
|
98
|
+
|
99
|
+
for _ in range(10):
|
100
|
+
fut = app_both()
|
101
|
+
|
102
|
+
# assertions that should hold after submission
|
103
|
+
assert isinstance(fut.stdout, DataFuture)
|
104
|
+
assert fut.stdout.file_obj.url.startswith("zip")
|
105
|
+
|
106
|
+
futs.append(fut)
|
107
|
+
|
108
|
+
# Barrier for all the stageouts to complete so that we can
|
109
|
+
# poke at the zip file.
|
110
|
+
[(fut.stdout.result(), fut.stderr.result()) for fut in futs]
|
111
|
+
|
112
|
+
with zipfile.ZipFile(tmpd_cwd / "base.zip") as z:
|
113
|
+
for fut in futs:
|
114
|
+
|
115
|
+
assert fut.done(), "AppFuture should be done if stageout is done"
|
116
|
+
|
117
|
+
stdout_relative_path = f"app_both.{fut.tid}.0.stdout"
|
118
|
+
with z.open(stdout_relative_path) as f:
|
119
|
+
assert f.readlines() == [b'hello\n']
|
120
|
+
|
121
|
+
stderr_relative_path = f"app_both.{fut.tid}.0.stderr"
|
122
|
+
with z.open(stderr_relative_path) as f:
|
123
|
+
assert f.readlines()[-1] == b'goodbye\n'
|
124
|
+
|
125
|
+
for record in caplog.records:
|
126
|
+
assert record.levelno < logging.ERROR
|
127
|
+
|
128
|
+
parsl.clear()
|
@@ -1,16 +1,12 @@
|
|
1
|
-
import argparse
|
2
|
-
import time
|
3
|
-
|
4
1
|
import pytest
|
5
2
|
|
6
3
|
import parsl
|
7
4
|
from parsl.app.app import python_app
|
8
|
-
from parsl.tests.configs.local_threads_checkpoint_periodic import
|
5
|
+
from parsl.tests.configs.local_threads_checkpoint_periodic import fresh_config
|
9
6
|
|
10
7
|
|
11
8
|
def local_setup():
|
12
|
-
|
13
|
-
dfk = parsl.load(config)
|
9
|
+
parsl.load(fresh_config())
|
14
10
|
|
15
11
|
|
16
12
|
def local_teardown():
|
@@ -27,40 +23,31 @@ def slow_double(x, sleep_dur=1):
|
|
27
23
|
|
28
24
|
|
29
25
|
def tstamp_to_seconds(line):
|
30
|
-
print("Parsing line: ", line)
|
31
26
|
f = line.partition(" ")[0]
|
32
27
|
return float(f)
|
33
28
|
|
34
29
|
|
35
30
|
@pytest.mark.local
|
36
|
-
def test_periodic(
|
31
|
+
def test_periodic():
|
37
32
|
"""Test checkpointing with task_periodic behavior
|
38
33
|
"""
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
for i in range(0, n):
|
48
|
-
d[i].result()
|
49
|
-
print("Done sleeping")
|
50
|
-
|
51
|
-
time.sleep(16)
|
52
|
-
dfk.cleanup()
|
34
|
+
h, m, s = map(int, parsl.dfk().config.checkpoint_period.split(":"))
|
35
|
+
assert h == 0, "Verify test setup"
|
36
|
+
assert m == 0, "Verify test setup"
|
37
|
+
assert s > 0, "Verify test setup"
|
38
|
+
sleep_for = s + 1
|
39
|
+
with parsl.dfk():
|
40
|
+
futs = [slow_double(sleep_for) for _ in range(4)]
|
41
|
+
[f.result() for f in futs]
|
53
42
|
|
54
43
|
# Here we will check if the loglines came back with 5 seconds deltas
|
55
|
-
|
56
|
-
|
57
|
-
with open("{}/parsl.log".format(dfk.run_dir), 'r') as f:
|
44
|
+
with open("{}/parsl.log".format(parsl.dfk().run_dir)) as f:
|
58
45
|
log_lines = f.readlines()
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
46
|
+
expected_msg = " Done checkpointing"
|
47
|
+
expected_msg2 = " No tasks checkpointed in this pass"
|
48
|
+
|
49
|
+
lines = [line for line in log_lines if expected_msg in line or expected_msg2 in line]
|
50
|
+
assert len(lines) >= 3, "Insufficient checkpoint lines in logfile"
|
51
|
+
deltas = [tstamp_to_seconds(line) for line in lines]
|
52
|
+
assert deltas[1] - deltas[0] < 5.5, "Delta between checkpoints exceeded period"
|
53
|
+
assert deltas[2] - deltas[1] < 5.5, "Delta between checkpoints exceeded period"
|
@@ -37,7 +37,3 @@ def test_that_it_fails():
|
|
37
37
|
raise Exception("The app somehow ran without a valid worker")
|
38
38
|
|
39
39
|
assert parsl.dfk().config.executors[0]._executor_bad_state.is_set()
|
40
|
-
|
41
|
-
# htex needs shutting down explicitly because dfk.cleanup() will not
|
42
|
-
# do that, as it is in bad state
|
43
|
-
parsl.dfk().config.executors[0].shutdown()
|
@@ -25,17 +25,11 @@ from parsl.executors.high_throughput.mpi_prefix_composer import (
|
|
25
25
|
EXECUTOR_LABEL = "MPI_TEST"
|
26
26
|
|
27
27
|
|
28
|
-
def
|
28
|
+
def local_config():
|
29
29
|
config = fresh_config()
|
30
30
|
config.executors[0].label = EXECUTOR_LABEL
|
31
31
|
config.executors[0].max_workers_per_node = 1
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
def local_teardown():
|
36
|
-
logging.warning("Exiting")
|
37
|
-
parsl.dfk().cleanup()
|
38
|
-
parsl.clear()
|
32
|
+
return config
|
39
33
|
|
40
34
|
|
41
35
|
@python_app
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import parsl
|
2
|
+
import pytest
|
3
|
+
import random
|
4
|
+
import zipfile
|
5
|
+
|
6
|
+
from parsl.data_provider.files import File
|
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
|
+
@parsl.python_app
|
20
|
+
def count_lines(file):
|
21
|
+
with open(file, "r") as f:
|
22
|
+
return len(f.readlines())
|
23
|
+
|
24
|
+
|
25
|
+
@pytest.mark.local
|
26
|
+
def test_zip_in(tmpd_cwd):
|
27
|
+
# basic test of zip file stage-in
|
28
|
+
zip_path = tmpd_cwd / "container.zip"
|
29
|
+
file_base = "data.txt"
|
30
|
+
zip_file = File(f"zip:{zip_path / file_base}")
|
31
|
+
|
32
|
+
# create a zip file containing one file with some abitrary number of lines
|
33
|
+
n_lines = random.randint(0, 1000)
|
34
|
+
|
35
|
+
with zipfile.ZipFile(zip_path, mode='w') as z:
|
36
|
+
with z.open(file_base, mode='w') as f:
|
37
|
+
for _ in range(n_lines):
|
38
|
+
f.write(b'someline\n')
|
39
|
+
|
40
|
+
app_future = count_lines(zip_file)
|
41
|
+
|
42
|
+
assert app_future.result() == n_lines
|
@@ -0,0 +1,44 @@
|
|
1
|
+
import parsl
|
2
|
+
import pytest
|
3
|
+
import random
|
4
|
+
import zipfile
|
5
|
+
|
6
|
+
from parsl.data_provider.files import File
|
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
|
+
@parsl.python_app
|
20
|
+
def generate_lines(n: int, *, outputs):
|
21
|
+
with open(outputs[0], "w") as f:
|
22
|
+
for x in range(n):
|
23
|
+
# write numbered lines
|
24
|
+
f.write(str(x) + "\n")
|
25
|
+
|
26
|
+
|
27
|
+
@parsl.python_app
|
28
|
+
def count_lines(file):
|
29
|
+
with open(file, "r") as f:
|
30
|
+
return len(f.readlines())
|
31
|
+
|
32
|
+
|
33
|
+
@pytest.mark.local
|
34
|
+
def test_zip_pipeline(tmpd_cwd):
|
35
|
+
# basic test of zip file stage-in
|
36
|
+
zip_path = tmpd_cwd / "container.zip"
|
37
|
+
file_base = "data.txt"
|
38
|
+
zip_file = File(f"zip:{zip_path / file_base}")
|
39
|
+
|
40
|
+
n_lines = random.randint(0, 1000)
|
41
|
+
generate_fut = generate_lines(n_lines, outputs=[zip_file])
|
42
|
+
n_lines_out = count_lines(generate_fut.outputs[0]).result()
|
43
|
+
|
44
|
+
assert n_lines == n_lines_out
|
File without changes
|
@@ -0,0 +1,99 @@
|
|
1
|
+
import os
|
2
|
+
from unittest import mock
|
3
|
+
|
4
|
+
import pytest
|
5
|
+
|
6
|
+
from parsl import File
|
7
|
+
|
8
|
+
_MOCK_BASE = "parsl.data_provider.files."
|
9
|
+
|
10
|
+
|
11
|
+
@pytest.mark.local
|
12
|
+
@pytest.mark.parametrize("scheme", ("http", "https", "ftp", "ftps", "asdfasdf"))
|
13
|
+
def test_file_init_scheme(scheme):
|
14
|
+
basename = "some_base_name"
|
15
|
+
path = f"/some/path/1/2/3/{basename}"
|
16
|
+
fqdn = "some.fqdn.example.com"
|
17
|
+
exp_url = f"{scheme}://{fqdn}{path}"
|
18
|
+
f = File(exp_url)
|
19
|
+
assert f.url == exp_url, "Expected given url to be stored"
|
20
|
+
assert f.scheme == scheme
|
21
|
+
assert f.netloc == fqdn
|
22
|
+
assert f.path == path
|
23
|
+
assert f.filename == basename
|
24
|
+
assert f.local_path is None, "Expect only set by API consumer, not constructor"
|
25
|
+
|
26
|
+
|
27
|
+
@pytest.mark.local
|
28
|
+
@pytest.mark.parametrize("url", ("some weird :// url", "", "a"))
|
29
|
+
def test_file_init_file_url_fallback(url):
|
30
|
+
exp_url = "some weird :// url"
|
31
|
+
f = File(exp_url)
|
32
|
+
assert f.url == exp_url
|
33
|
+
assert not f.netloc, "invalid host, should be no netloc"
|
34
|
+
assert f.path == exp_url, "Should fail to fully parse, so path is whole url"
|
35
|
+
assert f.filename == exp_url.rsplit("/", 1)[-1]
|
36
|
+
|
37
|
+
assert f.scheme == "file"
|
38
|
+
|
39
|
+
|
40
|
+
@pytest.mark.local
|
41
|
+
def test_file_proxies_for_filepath(randomstring):
|
42
|
+
# verify (current) expected internal hookup
|
43
|
+
exp_filepath = randomstring()
|
44
|
+
with mock.patch(
|
45
|
+
f"{_MOCK_BASE}File.filepath", new_callable=mock.PropertyMock
|
46
|
+
) as mock_fpath:
|
47
|
+
mock_fpath.return_value = exp_filepath
|
48
|
+
f = File("")
|
49
|
+
assert str(f) == exp_filepath
|
50
|
+
assert os.fspath(f) == exp_filepath
|
51
|
+
|
52
|
+
|
53
|
+
@pytest.mark.local
|
54
|
+
@pytest.mark.parametrize("scheme", ("file://", ""))
|
55
|
+
def test_file_filepath_local_path_is_priority(scheme, randomstring):
|
56
|
+
exp_path = "/some/local/path"
|
57
|
+
url = f"{scheme}{exp_path}"
|
58
|
+
f = File(url)
|
59
|
+
|
60
|
+
f.local_path = randomstring()
|
61
|
+
assert f.filepath == f.local_path
|
62
|
+
|
63
|
+
f.local_path = None
|
64
|
+
assert f.filepath == exp_path
|
65
|
+
|
66
|
+
|
67
|
+
@pytest.mark.local
|
68
|
+
def test_file_filepath_requires_local_accessible_path():
|
69
|
+
with pytest.raises(ValueError) as pyt_exc:
|
70
|
+
_ = File("http://").filepath
|
71
|
+
|
72
|
+
assert "No local_path" in str(pyt_exc.value), "Expected reason in exception"
|
73
|
+
|
74
|
+
|
75
|
+
@pytest.mark.local
|
76
|
+
@pytest.mark.parametrize("scheme", ("https", "ftps", "", "file", "asdfasdf"))
|
77
|
+
def test_file_repr(scheme):
|
78
|
+
netloc = "some.netloc"
|
79
|
+
filename = "some_file_name"
|
80
|
+
path = f"/some/path/{filename}"
|
81
|
+
if scheme:
|
82
|
+
url = f"{scheme}://{netloc}{path}"
|
83
|
+
else:
|
84
|
+
scheme = "file"
|
85
|
+
url = path
|
86
|
+
|
87
|
+
f = File(url)
|
88
|
+
r = repr(f)
|
89
|
+
assert r.startswith("<")
|
90
|
+
assert r.endswith(">")
|
91
|
+
assert f"<{type(f).__name__} " in r
|
92
|
+
assert f" at 0x{id(f):x}" in r
|
93
|
+
assert f" url={url}" in r
|
94
|
+
assert f" scheme={scheme}" in r
|
95
|
+
assert f" path={path}" in r
|
96
|
+
assert f" filename={filename}" in r
|
97
|
+
|
98
|
+
if scheme != "file":
|
99
|
+
assert f" netloc={netloc}" in r
|
@@ -0,0 +1,66 @@
|
|
1
|
+
import inspect
|
2
|
+
|
3
|
+
from parsl.utils import RepresentationMixin
|
4
|
+
|
5
|
+
from abc import abstractmethod
|
6
|
+
from functools import singledispatch
|
7
|
+
from typing import Any, List, Sequence
|
8
|
+
|
9
|
+
|
10
|
+
# Traverse the configuration hierarchy, returning a JSON component
|
11
|
+
# for each one. Configuration components which implement
|
12
|
+
# RepresentationMixin will be in the right form for inspecting
|
13
|
+
# object attributes. Configuration components which are lists or tuples
|
14
|
+
# are traversed in sequence. Other types default to reporting no
|
15
|
+
# usage information.
|
16
|
+
|
17
|
+
@singledispatch
|
18
|
+
def get_parsl_usage(obj) -> List[Any]:
|
19
|
+
return []
|
20
|
+
|
21
|
+
|
22
|
+
@get_parsl_usage.register
|
23
|
+
def get_parsl_usage_representation_mixin(obj: RepresentationMixin) -> List[Any]:
|
24
|
+
t = type(obj)
|
25
|
+
qualified_name = t.__module__ + "." + t.__name__
|
26
|
+
|
27
|
+
# me can contain anything that can be rendered as JSON
|
28
|
+
me: List[Any] = []
|
29
|
+
|
30
|
+
if isinstance(obj, UsageInformation):
|
31
|
+
# report rich usage information for this component
|
32
|
+
attrs = {'c': qualified_name}
|
33
|
+
attrs.update(obj.get_usage_information())
|
34
|
+
me = [attrs]
|
35
|
+
else:
|
36
|
+
# report the class name of this component
|
37
|
+
me = [qualified_name]
|
38
|
+
|
39
|
+
# unwrap typeguard-style unwrapping
|
40
|
+
init: Any = type(obj).__init__
|
41
|
+
if hasattr(init, '__wrapped__'):
|
42
|
+
init = init.__wrapped__
|
43
|
+
|
44
|
+
argspec = inspect.getfullargspec(init)
|
45
|
+
|
46
|
+
for arg in argspec.args[1:]: # skip first arg, self
|
47
|
+
arg_value = getattr(obj, arg)
|
48
|
+
d = get_parsl_usage(arg_value)
|
49
|
+
me += d
|
50
|
+
|
51
|
+
return me
|
52
|
+
|
53
|
+
|
54
|
+
@get_parsl_usage.register(list)
|
55
|
+
@get_parsl_usage.register(tuple)
|
56
|
+
def get_parsl_usage_sequence(obj: Sequence) -> List[Any]:
|
57
|
+
result = []
|
58
|
+
for v in obj:
|
59
|
+
result += get_parsl_usage(v)
|
60
|
+
return result
|
61
|
+
|
62
|
+
|
63
|
+
class UsageInformation:
|
64
|
+
@abstractmethod
|
65
|
+
def get_usage_information(self) -> dict:
|
66
|
+
pass
|
parsl/usage_tracking/usage.py
CHANGED
@@ -7,6 +7,7 @@ import socket
|
|
7
7
|
import sys
|
8
8
|
import platform
|
9
9
|
|
10
|
+
from parsl.usage_tracking.api import get_parsl_usage
|
10
11
|
from parsl.utils import setproctitle
|
11
12
|
from parsl.multiprocessing import ForkProcess
|
12
13
|
from parsl.dataflow.states import States
|
@@ -17,6 +18,13 @@ logger = logging.getLogger(__name__)
|
|
17
18
|
from typing import Callable
|
18
19
|
from typing_extensions import ParamSpec
|
19
20
|
|
21
|
+
# protocol version byte: when (for example) compression parameters are changed
|
22
|
+
# that cannot be inferred from the compressed message itself, this version
|
23
|
+
# ID needs to imply those parameters.
|
24
|
+
|
25
|
+
# Earlier protocol versions: b'{' - the original pure-JSON protocol pre-March 2024
|
26
|
+
PROTOCOL_VERSION = b'1'
|
27
|
+
|
20
28
|
P = ParamSpec("P")
|
21
29
|
|
22
30
|
|
@@ -32,7 +40,7 @@ def async_process(fn: Callable[P, None]) -> Callable[P, None]:
|
|
32
40
|
|
33
41
|
|
34
42
|
@async_process
|
35
|
-
def udp_messenger(domain_name: str, UDP_PORT: int, sock_timeout: int, message:
|
43
|
+
def udp_messenger(domain_name: str, UDP_PORT: int, sock_timeout: int, message: bytes) -> None:
|
36
44
|
"""Send UDP messages to usage tracker asynchronously
|
37
45
|
|
38
46
|
This multiprocessing based messenger was written to overcome the limitations
|
@@ -46,16 +54,11 @@ def udp_messenger(domain_name: str, UDP_PORT: int, sock_timeout: int, message: s
|
|
46
54
|
setproctitle("parsl: Usage tracking")
|
47
55
|
|
48
56
|
try:
|
49
|
-
encoded_message = bytes(message, "utf-8")
|
50
|
-
|
51
57
|
UDP_IP = socket.gethostbyname(domain_name)
|
52
58
|
|
53
|
-
if UDP_PORT is None:
|
54
|
-
raise Exception("UDP_PORT is None")
|
55
|
-
|
56
59
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP
|
57
60
|
sock.settimeout(sock_timeout)
|
58
|
-
sock.sendto(
|
61
|
+
sock.sendto(message, (UDP_IP, UDP_PORT))
|
59
62
|
sock.close()
|
60
63
|
|
61
64
|
except socket.timeout:
|
@@ -102,7 +105,7 @@ class UsageTracker:
|
|
102
105
|
self.procs = []
|
103
106
|
self.dfk = dfk
|
104
107
|
self.config = self.dfk.config
|
105
|
-
self.
|
108
|
+
self.correlator_uuid = str(uuid.uuid4())
|
106
109
|
self.parsl_version = PARSL_VERSION
|
107
110
|
self.python_version = "{}.{}.{}".format(sys.version_info.major,
|
108
111
|
sys.version_info.minor,
|
@@ -130,22 +133,23 @@ class UsageTracker:
|
|
130
133
|
|
131
134
|
return track
|
132
135
|
|
133
|
-
def construct_start_message(self) ->
|
136
|
+
def construct_start_message(self) -> bytes:
|
134
137
|
"""Collect preliminary run info at the start of the DFK.
|
135
138
|
|
136
139
|
Returns :
|
137
140
|
- Message dict dumped as json string, ready for UDP
|
138
141
|
"""
|
139
|
-
message = {'
|
142
|
+
message = {'correlator': self.correlator_uuid,
|
140
143
|
'parsl_v': self.parsl_version,
|
141
144
|
'python_v': self.python_version,
|
142
|
-
'
|
143
|
-
'
|
144
|
-
'
|
145
|
+
'platform.system': platform.system(),
|
146
|
+
'start': int(time.time()),
|
147
|
+
'components': get_parsl_usage(self.dfk._config)}
|
148
|
+
logger.debug(f"Usage tracking start message: {message}")
|
145
149
|
|
146
|
-
return
|
150
|
+
return self.encode_message(message)
|
147
151
|
|
148
|
-
def construct_end_message(self) ->
|
152
|
+
def construct_end_message(self) -> bytes:
|
149
153
|
"""Collect the final run information at the time of DFK cleanup.
|
150
154
|
|
151
155
|
Returns:
|
@@ -153,20 +157,26 @@ class UsageTracker:
|
|
153
157
|
"""
|
154
158
|
app_count = self.dfk.task_count
|
155
159
|
|
156
|
-
site_count = len(self.dfk.config.executors)
|
157
|
-
|
158
160
|
app_fails = self.dfk.task_state_counts[States.failed] + self.dfk.task_state_counts[States.dep_fail]
|
159
161
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
162
|
+
# the DFK is tangled into this code as a god-object, so it is
|
163
|
+
# handled separately from the usual traversal code, but presenting
|
164
|
+
# the same protocol-level report.
|
165
|
+
dfk_component = {'c': type(self.dfk).__module__ + "." + type(self.dfk).__name__,
|
166
|
+
'app_count': app_count,
|
167
|
+
'app_fails': app_fails}
|
168
|
+
|
169
|
+
message = {'correlator': self.correlator_uuid,
|
170
|
+
'end': int(time.time()),
|
171
|
+
'components': [dfk_component] + get_parsl_usage(self.dfk._config)}
|
172
|
+
logger.debug(f"Usage tracking end message (unencoded): {message}")
|
166
173
|
|
167
|
-
return
|
174
|
+
return self.encode_message(message)
|
168
175
|
|
169
|
-
def
|
176
|
+
def encode_message(self, obj):
|
177
|
+
return PROTOCOL_VERSION + json.dumps(obj).encode()
|
178
|
+
|
179
|
+
def send_UDP_message(self, message: bytes) -> None:
|
170
180
|
"""Send UDP message."""
|
171
181
|
if self.tracking_enabled:
|
172
182
|
try:
|
@@ -191,7 +201,10 @@ class UsageTracker:
|
|
191
201
|
or won't respond to SIGTERM.
|
192
202
|
"""
|
193
203
|
for proc in self.procs:
|
204
|
+
logger.debug("Joining usage tracking process %s", proc)
|
194
205
|
proc.join(timeout=timeout)
|
195
206
|
if proc.is_alive():
|
196
|
-
logger.
|
207
|
+
logger.warning("Usage tracking process did not end itself; sending SIGKILL")
|
197
208
|
proc.kill()
|
209
|
+
|
210
|
+
proc.close()
|
parsl/version.py
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: parsl
|
3
|
-
Version: 2024.4.
|
3
|
+
Version: 2024.4.29
|
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.29.tar.gz
|
7
7
|
Author: The Parsl Team
|
8
8
|
Author-email: parsl@googlegroups.com
|
9
9
|
License: Apache 2.0
|
@@ -1,6 +1,6 @@
|
|
1
1
|
parsl/__init__.py,sha256=hq8rJmP59wzd9-yxaGcmq5gPpshOopH-Y1K0BkUBNY0,1843
|
2
2
|
parsl/addresses.py,sha256=0wPo-4HjW0l4ndqCKLmSdbbSWE_3WK7pxRvqBEp-3Lk,4821
|
3
|
-
parsl/config.py,sha256=
|
3
|
+
parsl/config.py,sha256=nT_XBE2ToRpp6jHCO1tFagGBxF4ShJEpxLx1BNvKHVw,7594
|
4
4
|
parsl/curvezmq.py,sha256=FtZEYP1IWDry39cH-tOKUm9TnaR1U7krOmvVYpATcOk,6939
|
5
5
|
parsl/errors.py,sha256=SzINzQFZDBDbj9l-DPQznD0TbGkNhHIRAPkcBCogf_A,1019
|
6
6
|
parsl/log_utils.py,sha256=Ckeb7YiIoK0FA8dA5CsWJDe28i9Sf4sxhFwp__VsD3o,3274
|
@@ -8,7 +8,7 @@ parsl/multiprocessing.py,sha256=hakfdg-sgxEjwloZeDrt6EhzwdzecvjJhkPHHxh8lII,1938
|
|
8
8
|
parsl/process_loggers.py,sha256=1G3Rfrh5wuZNo2X03grG4kTYPGOxz7hHCyG6L_A3b0A,1137
|
9
9
|
parsl/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
10
|
parsl/utils.py,sha256=DUPrl9ZdzwJzz2rmlRws77OMs43iQo_CT-Kr3uJs-fo,11202
|
11
|
-
parsl/version.py,sha256=
|
11
|
+
parsl/version.py,sha256=ZkQP1dxgTC3ws9wp7Qeu8XyvkriqFeJIGzAfXff0J3M,131
|
12
12
|
parsl/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
13
|
parsl/app/app.py,sha256=wAHchJetgnicT1pn0NJKDeDX0lV3vDFlG8cQd_Ciax4,8522
|
14
14
|
parsl/app/bash.py,sha256=VufxGROrlJB3dP03syNutU0x8rLzfI-gooWwBZ4FFQ8,5676
|
@@ -59,9 +59,9 @@ parsl/data_provider/globus.py,sha256=ss7R8XD64mR3p-y9lxNAb11rymiOlxI1hQzkPEW51ZI
|
|
59
59
|
parsl/data_provider/http.py,sha256=nDHTW7XmJqAukWJjPRQjyhUXt8r6GsQ36mX9mv_wOig,2986
|
60
60
|
parsl/data_provider/rsync.py,sha256=2-ZxqrT-hBj39x082NusJaBqsGW4Jd2qCW6JkVPpEl0,4254
|
61
61
|
parsl/data_provider/staging.py,sha256=l-mAXFburs3BWPjkSmiQKuAgJpsxCG62yATPDbrafYI,4523
|
62
|
-
parsl/data_provider/zip.py,sha256=
|
62
|
+
parsl/data_provider/zip.py,sha256=50xvXms3UoEFc4v_w6joLAcCrcdKuOjIPoPLgUz1Jio,4498
|
63
63
|
parsl/dataflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
64
|
-
parsl/dataflow/dflow.py,sha256=
|
64
|
+
parsl/dataflow/dflow.py,sha256=oKlzZlhftcHN2l1Pgh6EhZx1kWTmD8WnfPTGlk1decI,66037
|
65
65
|
parsl/dataflow/errors.py,sha256=w2vOt_ymzG2dOqJUO4IDcmTlrCIHlMZL8nBVyVq0O_8,2176
|
66
66
|
parsl/dataflow/futures.py,sha256=XGgaoaT3N2U3vvZ7DEoLkGBrZscq_VzffZ9goLB87ko,6081
|
67
67
|
parsl/dataflow/memoization.py,sha256=AsJO6c6cRp2ac6H8uGn2USlEi78_nX3QWvpxYt4XdYE,9583
|
@@ -79,7 +79,7 @@ parsl/executors/flux/executor.py,sha256=0omXRPvykdW5VZb8mwgBJjxVk4H6G8xoL5D_R9pu
|
|
79
79
|
parsl/executors/flux/flux_instance_manager.py,sha256=tTEOATClm9SwdgLeBRWPC6D55iNDuh0YxqJOw3c3eQ4,2036
|
80
80
|
parsl/executors/high_throughput/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
81
81
|
parsl/executors/high_throughput/errors.py,sha256=vl69wLuVOplbKxHI9WphEGBExHWkTn5n8T9QhBXuNH0,380
|
82
|
-
parsl/executors/high_throughput/executor.py,sha256=
|
82
|
+
parsl/executors/high_throughput/executor.py,sha256=Er0QUG-HNatDPVBsLYZbbEStyNJsu4oE9UQ7LtQYH7w,37318
|
83
83
|
parsl/executors/high_throughput/interchange.py,sha256=Rt6HyFvQYFuqUJ1ytXmUFTDIK9wOBm4l96IHoL6OFRc,31491
|
84
84
|
parsl/executors/high_throughput/manager_record.py,sha256=w5EwzVqPtsLOyOW8jP44U3uaogt8H--tkwp7FNyKN_o,385
|
85
85
|
parsl/executors/high_throughput/monitoring_info.py,sha256=3gQpwQjjNDEBz0cQqJZB6hRiwLiWwXs83zkQDmbOwxY,297
|
@@ -173,7 +173,7 @@ parsl/providers/grid_engine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
|
|
173
173
|
parsl/providers/grid_engine/grid_engine.py,sha256=xzh5clmo2xKQYm56ONkszfrx7aE_n2dfubEd5mLGexg,8565
|
174
174
|
parsl/providers/grid_engine/template.py,sha256=a7iViKr8LXcFTPmsf_qQeVK5o_RekOAIlUOF0X1q-2M,273
|
175
175
|
parsl/providers/kubernetes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
176
|
-
parsl/providers/kubernetes/kube.py,sha256=
|
176
|
+
parsl/providers/kubernetes/kube.py,sha256=nhaGoOFHxfkNSpKMFd9UuctjcroTQrPYNRbxUxd05rQ,13810
|
177
177
|
parsl/providers/kubernetes/template.py,sha256=VsRz6cmNaII-y4OdMT6sCwzQy95SJX6NMB0hmmFBhX4,50
|
178
178
|
parsl/providers/local/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
179
179
|
parsl/providers/local/local.py,sha256=7OYug8UaRr1QW2g3z-SYmdo1InSSZRP0tQOySvrkwbI,11372
|
@@ -197,7 +197,7 @@ parsl/serialize/facade.py,sha256=SpKGSpI8PQb3hhxuKRJUYoQoq284t5np9ouTpogKmtU,679
|
|
197
197
|
parsl/serialize/proxystore.py,sha256=Yo-38odKlSKSuQfXU4cB5YM9sYV_302uPn1z_en19SU,1623
|
198
198
|
parsl/tests/__init__.py,sha256=s_zoz7Ipgykh-QTQvctdpxENrMnmpXY8oe1bJbUmpqY,204
|
199
199
|
parsl/tests/callables_helper.py,sha256=ceP1YYsNtrZgKT6MAIvpgdccEjQ_CpFEOnZBGHKGOx0,30
|
200
|
-
parsl/tests/conftest.py,sha256=
|
200
|
+
parsl/tests/conftest.py,sha256=9AFrtThVnHasfyujoPw6iudxTvLlmqKQytbarzi3sWY,14579
|
201
201
|
parsl/tests/test_aalst_patterns.py,sha256=fi6JHKidV7vMJLv2nnu_-Q0ngGLc89mRm8rFrGIwiUM,9615
|
202
202
|
parsl/tests/test_callables.py,sha256=_QsdS8v2nGgOj4_X69NFHZOGUnqbOrOMCA9pCJColZw,1974
|
203
203
|
parsl/tests/test_curvezmq.py,sha256=yyhlS4vmaZdMitiySoy4l_ih9H1bsPiN-tMdwIh3H20,12431
|
@@ -227,7 +227,7 @@ parsl/tests/configs/local_radical_mpi.py,sha256=K6V2HbARujaow5DBAUYSIWt1RaYbt898
|
|
227
227
|
parsl/tests/configs/local_threads.py,sha256=oEnQSlom_JMLFX9_Ln49JAfOP3nSMbw8gTaDJo_NYfo,202
|
228
228
|
parsl/tests/configs/local_threads_checkpoint.py,sha256=Ex7CI1Eo6wVRsem9uXTtbVJrkKc_vOYlVvCNa2RLpIo,286
|
229
229
|
parsl/tests/configs/local_threads_checkpoint_dfk_exit.py,sha256=ECL1n0uBsXDuW3sLCmjiwe8s3Xd7EFIj5wt446w6bh4,254
|
230
|
-
parsl/tests/configs/local_threads_checkpoint_periodic.py,sha256=
|
230
|
+
parsl/tests/configs/local_threads_checkpoint_periodic.py,sha256=F2MVlwJZk-hkCgCrsAm_rKsv4mtLgsf5cyPsRoHm0ig,319
|
231
231
|
parsl/tests/configs/local_threads_checkpoint_task_exit.py,sha256=zHKN68T-xhAVQwQp3fSWPIEcWOx-F7NBGZTOhF07iL8,256
|
232
232
|
parsl/tests/configs/local_threads_ftp_in_task.py,sha256=c9odRbxgj1bM_ttpkWTh2Ch_MV7f5cmn-68BOjLeJ70,444
|
233
233
|
parsl/tests/configs/local_threads_globus.py,sha256=NhY27cD4vcqLh762Ye0BINZnt63EmTyHXg7FQMffOBw,1097
|
@@ -302,11 +302,12 @@ parsl/tests/test_bash_apps/test_memoize_ignore_args.py,sha256=zBB4zNMZe4VZExwKZH
|
|
302
302
|
parsl/tests/test_bash_apps/test_memoize_ignore_args_regr.py,sha256=zQDQktIA9bYdqciZ17bCCGZw7XCQf3XmGqDsZ4SU_e0,1362
|
303
303
|
parsl/tests/test_bash_apps/test_multiline.py,sha256=stpMEv2eopGG-ietxjUtD5gYMOVpwPdLauDizjUfTdA,1082
|
304
304
|
parsl/tests/test_bash_apps/test_pipeline.py,sha256=439Ml2bMn5NgiQiNcg4DPeWhsXK8WQaatvZy2PleQzQ,2444
|
305
|
+
parsl/tests/test_bash_apps/test_std_uri.py,sha256=cIQVc8tCN0jM32oXwEKzxkhZT20l259AriM_jmtHdI0,3808
|
305
306
|
parsl/tests/test_bash_apps/test_stdout.py,sha256=3jVDVjUfoQMDzp-f1Ifr7B6ivMq7U5ZzxXU3ZyUKogU,2787
|
306
307
|
parsl/tests/test_channels/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
307
308
|
parsl/tests/test_channels/test_large_output.py,sha256=PGeNSW_sN5mR7KF1hVL2CPfktydYxo4oNz1wVQ-ENN0,595
|
308
309
|
parsl/tests/test_checkpointing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
309
|
-
parsl/tests/test_checkpointing/test_periodic.py,sha256=
|
310
|
+
parsl/tests/test_checkpointing/test_periodic.py,sha256=8R9Plxdm-dUvaptJsw-pEKLOjO5q74lHnaB2kxbF5-M,1613
|
310
311
|
parsl/tests/test_checkpointing/test_python_checkpoint_1.py,sha256=7p_q5aFYYoRQpYmkFekuLOsPgTaILbj5-MMVCDP3Bsg,745
|
311
312
|
parsl/tests/test_checkpointing/test_python_checkpoint_2.py,sha256=f1s-qRzIzaCFJauEGU08fhFw6od3yGrMelk792WQuYI,1106
|
312
313
|
parsl/tests/test_checkpointing/test_python_checkpoint_3.py,sha256=8Np2OpDeQ8sE1Hmd5rYZo1qgt2xOuR4t-d-41JyLCHI,823
|
@@ -333,14 +334,14 @@ parsl/tests/test_error_handling/test_serialization_fail.py,sha256=jIJBKK-ItVe8B1
|
|
333
334
|
parsl/tests/test_error_handling/test_wrap_with_logs.py,sha256=AHQZj7wae759FOcAUeHZX47mx1MKOfDv2YBLnaDVW7Q,741
|
334
335
|
parsl/tests/test_flowcontrol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
335
336
|
parsl/tests/test_htex/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
336
|
-
parsl/tests/test_htex/test_basic.py,sha256=
|
337
|
+
parsl/tests/test_htex/test_basic.py,sha256=CTOXEkzrVW_kSp_scz3BGYwc1c6KYopTooXICiAsMso,469
|
337
338
|
parsl/tests/test_htex/test_connected_blocks.py,sha256=0628oJ_0_aVsda5xuFwG3_3q8ZiEAM-sfIOINkUHQrk,1639
|
338
339
|
parsl/tests/test_htex/test_cpu_affinity_explicit.py,sha256=tv12ojw4DdymlVBjVNnrFX7_mhwix2jnBLkRbKOQRao,1422
|
339
340
|
parsl/tests/test_htex/test_disconnected_blocks.py,sha256=iga7wmhGACwUN6gkEFPw1dLodj6SzNZpevgSHNYSyjI,1856
|
340
341
|
parsl/tests/test_htex/test_drain.py,sha256=BvPQIo0xx-z191eVR2rG51x22yzqD-6dLSH7bCAUhOg,2288
|
341
342
|
parsl/tests/test_htex/test_htex.py,sha256=4dXtcthZQvgEDtMc00g6Pw7FnqNWB_0j8fuJqHKO-IE,3896
|
342
343
|
parsl/tests/test_htex/test_manager_failure.py,sha256=gemQopZoDEoZLOvep5JZkY6tQlZoko8Z0Kmpj1-Gbws,1161
|
343
|
-
parsl/tests/test_htex/test_missing_worker.py,sha256=
|
344
|
+
parsl/tests/test_htex/test_missing_worker.py,sha256=R8FaUNnpmXY9E_ZCC797La4jWsJvp136xBhLY9yIgyU,818
|
344
345
|
parsl/tests/test_htex/test_multiple_disconnected_blocks.py,sha256=L4vw_Mo-upp2p9-TyPDfluNJJQ2BxHHNXgS3xhhuE28,1993
|
345
346
|
parsl/tests/test_htex/test_worker_failure.py,sha256=Uz-RHI-LK78FMjXUvrUFmo4iYfmpDVBUcBxxRb3UG9M,603
|
346
347
|
parsl/tests/test_htex/test_zmq_binding.py,sha256=2-y8HZPzNLrumVqyqG9yZl-lqefSIpez2jr5Ghrtd80,3013
|
@@ -360,7 +361,7 @@ parsl/tests/test_mpi_apps/test_mpi_mode_disabled.py,sha256=bMO66LHEvdVsV8sMFxESF
|
|
360
361
|
parsl/tests/test_mpi_apps/test_mpi_mode_enabled.py,sha256=V8dccWWVSCfKkWPJUaL1nHc7YeFL8KZhMB-xwIIKF4A,5034
|
361
362
|
parsl/tests/test_mpi_apps/test_mpi_prefix.py,sha256=OJ95-95t7DmDF86uYCtT2iRcY2gn9LFH45OWyjjL2h8,1950
|
362
363
|
parsl/tests/test_mpi_apps/test_mpi_scheduler.py,sha256=dZ8_mzLMx5Us7mra2nPfxaeWZnhZyqNJ8vPWkWHaEB8,6317
|
363
|
-
parsl/tests/test_mpi_apps/test_resource_spec.py,sha256=
|
364
|
+
parsl/tests/test_mpi_apps/test_resource_spec.py,sha256=8AKIXWFq9Tat8Mj0IRp-epNGVGFTqcxqZgF7jiLjE4Q,3607
|
364
365
|
parsl/tests/test_providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
365
366
|
parsl/tests/test_providers/test_cobalt_deprecation_warning.py,sha256=Fy5XXDkVs3KIX3tHyRjyReXEr35X1LWyEXcVXmWccDs,389
|
366
367
|
parsl/tests/test_providers/test_local_provider.py,sha256=G6Fuko22SvAtD7xhfQv8k_8HtJuFhZ8aHYcWQt073Pg,6968
|
@@ -440,20 +441,25 @@ parsl/tests/test_staging/test_staging_ftp_in_task.py,sha256=kR2XrGvbvVFDpHg53Nnj
|
|
440
441
|
parsl/tests/test_staging/test_staging_globus.py,sha256=ds8nDH5dNbI10FV_GxMHyVaY6GPnuPPzkX9IiqROLF0,2339
|
441
442
|
parsl/tests/test_staging/test_staging_https.py,sha256=ESNuvdc_P5JoPaMjBM3ofi1mNJM0U6vahi9JgbCsrPw,3307
|
442
443
|
parsl/tests/test_staging/test_staging_stdout.py,sha256=i4ksb9ehu-bKPgALxm6ADB12EQVTM_xusyFGmSwByLs,2025
|
444
|
+
parsl/tests/test_staging/test_zip_in.py,sha256=xLTXXAHgMyg5XAMTB_oq3oEhrNfq8JGMnuRu2vu5tdo,1143
|
443
445
|
parsl/tests/test_staging/test_zip_out.py,sha256=qR_iK8wqKOxisMBD5MqKr2RoyoTUmRejAj_O3jgJA2A,3512
|
446
|
+
parsl/tests/test_staging/test_zip_to_zip.py,sha256=Hz3fYvj-ePpfV4Hh6Fs1vM8iZ14iRxPkJhaI0spr8dE,1168
|
444
447
|
parsl/tests/test_threads/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
445
448
|
parsl/tests/test_threads/test_configs.py,sha256=QA9YjIMAtZ2jmkfOWqBzEfzQQcFVCDizH7Qwiy2HIMQ,909
|
446
449
|
parsl/tests/test_threads/test_lazy_errors.py,sha256=nGhYfCMHFZYSy6YJ4gnAmiLl9SfYs0WVnuvj8DXQ9bw,560
|
447
450
|
parsl/tests/test_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
448
451
|
parsl/tests/test_utils/test_representation_mixin.py,sha256=kUZeIDwA2rlbJ3-beGzLLwf3dOplTMCrWJN87etHcyY,1633
|
452
|
+
parsl/tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
453
|
+
parsl/tests/unit/test_file.py,sha256=vLycnYcv3bvSzL-FV8WdoibqTyb41BrH1LUYBavobsg,2850
|
449
454
|
parsl/usage_tracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
450
|
-
parsl/usage_tracking/
|
451
|
-
parsl
|
452
|
-
parsl-2024.4.
|
453
|
-
parsl-2024.4.
|
454
|
-
parsl-2024.4.
|
455
|
-
parsl-2024.4.
|
456
|
-
parsl-2024.4.
|
457
|
-
parsl-2024.4.
|
458
|
-
parsl-2024.4.
|
459
|
-
parsl-2024.4.
|
455
|
+
parsl/usage_tracking/api.py,sha256=V_IMgpxUloP--MtlZGY5Uf6SfHVQSPP8poa3PedOs_I,1822
|
456
|
+
parsl/usage_tracking/usage.py,sha256=IGNPVEo3YJ3SI48WGESrip7PiCqEAheQ0Kgu8dRgQag,7616
|
457
|
+
parsl-2024.4.29.data/scripts/exec_parsl_function.py,sha256=NtWNeBvRqksej38eRPw8zPBJ1CeW6vgaitve0tfz_qc,7801
|
458
|
+
parsl-2024.4.29.data/scripts/parsl_coprocess.py,sha256=Y7Tc-h9WGui-YDe3w_h91w2Sm1JNL1gJ9kAV4PE_gw8,5722
|
459
|
+
parsl-2024.4.29.data/scripts/process_worker_pool.py,sha256=31tyTtU7hrrsatGReuCbLM-3GWkaYK1bvlFE1MhKYQg,41253
|
460
|
+
parsl-2024.4.29.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
|
461
|
+
parsl-2024.4.29.dist-info/METADATA,sha256=77gJi-9dKlrSb6gATxE7Rhq8lVIdziv7RbmtMtHPnCA,4012
|
462
|
+
parsl-2024.4.29.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
463
|
+
parsl-2024.4.29.dist-info/entry_points.txt,sha256=XqnsWDYoEcLbsMcpnYGKLEnSBmaIe1YoM5YsBdJG2tI,176
|
464
|
+
parsl-2024.4.29.dist-info/top_level.txt,sha256=PIheYoUFQtF2icLsgOykgU-Cjuwr2Oi6On2jo5RYgRM,6
|
465
|
+
parsl-2024.4.29.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|