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.
Files changed (44) hide show
  1. parsl/data_provider/data_manager.py +2 -1
  2. parsl/data_provider/zip.py +104 -0
  3. parsl/dataflow/dflow.py +57 -48
  4. parsl/dataflow/futures.py +0 -7
  5. parsl/executors/base.py +12 -9
  6. parsl/executors/high_throughput/executor.py +14 -19
  7. parsl/executors/high_throughput/process_worker_pool.py +3 -1
  8. parsl/executors/status_handling.py +82 -9
  9. parsl/executors/taskvine/executor.py +7 -2
  10. parsl/executors/workqueue/executor.py +8 -3
  11. parsl/jobs/job_status_poller.py +27 -107
  12. parsl/jobs/strategy.py +31 -32
  13. parsl/monitoring/monitoring.py +14 -23
  14. parsl/monitoring/radios.py +15 -0
  15. parsl/monitoring/remote.py +2 -1
  16. parsl/monitoring/router.py +7 -6
  17. parsl/providers/local/local.py +1 -1
  18. parsl/tests/configs/htex_local_alternate.py +2 -1
  19. parsl/tests/configs/taskvine_ex.py +1 -2
  20. parsl/tests/configs/workqueue_ex.py +1 -2
  21. parsl/tests/conftest.py +6 -7
  22. parsl/tests/test_bash_apps/test_basic.py +5 -4
  23. parsl/tests/test_bash_apps/test_error_codes.py +0 -3
  24. parsl/tests/test_bash_apps/test_kwarg_storage.py +0 -1
  25. parsl/tests/test_bash_apps/test_memoize.py +0 -2
  26. parsl/tests/test_bash_apps/test_memoize_ignore_args.py +0 -1
  27. parsl/tests/test_bash_apps/test_memoize_ignore_args_regr.py +0 -1
  28. parsl/tests/test_bash_apps/test_multiline.py +0 -1
  29. parsl/tests/test_bash_apps/test_stdout.py +11 -6
  30. parsl/tests/test_monitoring/test_basic.py +46 -21
  31. parsl/tests/test_monitoring/test_fuzz_zmq.py +10 -1
  32. parsl/tests/test_python_apps/test_outputs.py +0 -1
  33. parsl/tests/test_scaling/test_scale_down_htex_unregistered.py +74 -0
  34. parsl/tests/test_staging/test_zip_out.py +113 -0
  35. parsl/version.py +1 -1
  36. {parsl-2024.4.1.data → parsl-2024.4.15.data}/scripts/process_worker_pool.py +3 -1
  37. {parsl-2024.4.1.dist-info → parsl-2024.4.15.dist-info}/METADATA +3 -2
  38. {parsl-2024.4.1.dist-info → parsl-2024.4.15.dist-info}/RECORD +44 -41
  39. {parsl-2024.4.1.data → parsl-2024.4.15.data}/scripts/exec_parsl_function.py +0 -0
  40. {parsl-2024.4.1.data → parsl-2024.4.15.data}/scripts/parsl_coprocess.py +0 -0
  41. {parsl-2024.4.1.dist-info → parsl-2024.4.15.dist-info}/LICENSE +0 -0
  42. {parsl-2024.4.1.dist-info → parsl-2024.4.15.dist-info}/WHEEL +0 -0
  43. {parsl-2024.4.1.dist-info → parsl-2024.4.15.dist-info}/entry_points.txt +0 -0
  44. {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
  """
@@ -22,7 +22,6 @@ def no_checkpoint_stdout_app_ignore_args(stdout=None):
22
22
  return "echo X"
23
23
 
24
24
 
25
- @pytest.mark.issue363
26
25
  def test_memo_stdout():
27
26
 
28
27
  # this should run and create a file named after path_x
@@ -30,7 +30,6 @@ def no_checkpoint_stdout_app(stdout=None):
30
30
  return "echo X"
31
31
 
32
32
 
33
- @pytest.mark.issue363
34
33
  def test_memo_stdout():
35
34
 
36
35
  assert const_list_x == const_list_x_arg
@@ -14,7 +14,6 @@ def multiline(inputs=(), outputs=(), stderr=None, stdout=None):
14
14
  """.format(inputs=inputs, outputs=outputs)
15
15
 
16
16
 
17
- @pytest.mark.issue363
18
17
  def test_multiline(tmpd_cwd):
19
18
  so, se = tmpd_cwd / "std.out", tmpd_cwd / "std.err"
20
19
  f = multiline(
@@ -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.issue363
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.issue363
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
- @pytest.mark.issue363
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
- logger = logging.getLogger(__name__)
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
- def test_row_counts():
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
- if os.path.exists("runinfo/monitoring.db"):
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
- logger.info("loading parsl")
36
- parsl.load(fresh_config())
62
+ config = fresh_config()
63
+ config.run_dir = tmpd_cwd
64
+ config.monitoring.logging_endpoint = db_url
37
65
 
38
- logger.info("invoking and waiting for result")
39
- assert this_app().result() == 5
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
- logger.info("checking database content")
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
- # Two entries: one showing manager active, one inactive
71
- result = connection.execute(text("SELECT COUNT(*) FROM node"))
72
- (c, ) = result.first()
73
- assert c == 2
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
- parsl.dfk().monitoring._dfk_channel.send(b'FuzzyByte\rSTREAM')
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.
@@ -16,7 +16,6 @@ def double(x, outputs=[]):
16
16
  whitelist = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'configs', '*threads*')
17
17
 
18
18
 
19
- @pytest.mark.issue363
20
19
  def test_launch_apps(tmpd_cwd, n=2):
21
20
  outdir = tmpd_cwd / "outputs"
22
21
  outdir.mkdir()
@@ -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
@@ -3,4 +3,4 @@
3
3
  Year.Month.Day[alpha/beta/..]
4
4
  Alphas will be numbered like this -> 2024.12.10a0
5
5
  """
6
- VERSION = '2024.04.01'
6
+ VERSION = '2024.04.15'
@@ -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([t['task_id'] for t in tasks], task_recv_counter))
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.1
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.01.tar.gz
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'