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.
Files changed (53) hide show
  1. parsl/addresses.py +2 -2
  2. parsl/app/bash.py +10 -2
  3. parsl/app/errors.py +3 -5
  4. parsl/data_provider/data_manager.py +2 -1
  5. parsl/data_provider/zip.py +104 -0
  6. parsl/dataflow/dflow.py +92 -43
  7. parsl/dataflow/futures.py +26 -12
  8. parsl/executors/base.py +28 -9
  9. parsl/executors/high_throughput/executor.py +14 -19
  10. parsl/executors/high_throughput/process_worker_pool.py +3 -1
  11. parsl/executors/status_handling.py +81 -1
  12. parsl/executors/taskvine/executor.py +13 -2
  13. parsl/executors/workqueue/executor.py +14 -3
  14. parsl/jobs/job_status_poller.py +19 -113
  15. parsl/jobs/strategy.py +22 -27
  16. parsl/monitoring/monitoring.py +29 -23
  17. parsl/monitoring/radios.py +15 -0
  18. parsl/monitoring/router.py +7 -6
  19. parsl/providers/local/local.py +1 -1
  20. parsl/tests/configs/htex_local_alternate.py +2 -1
  21. parsl/tests/configs/taskvine_ex.py +1 -2
  22. parsl/tests/configs/workqueue_ex.py +1 -2
  23. parsl/tests/conftest.py +6 -7
  24. parsl/tests/test_bash_apps/test_basic.py +7 -4
  25. parsl/tests/test_bash_apps/test_error_codes.py +0 -3
  26. parsl/tests/test_bash_apps/test_kwarg_storage.py +0 -1
  27. parsl/tests/test_bash_apps/test_memoize.py +0 -2
  28. parsl/tests/test_bash_apps/test_memoize_ignore_args.py +0 -1
  29. parsl/tests/test_bash_apps/test_memoize_ignore_args_regr.py +0 -1
  30. parsl/tests/test_bash_apps/test_multiline.py +0 -1
  31. parsl/tests/test_bash_apps/test_stdout.py +11 -6
  32. parsl/tests/test_checkpointing/test_task_exit.py +1 -1
  33. parsl/tests/test_htex/test_zmq_binding.py +1 -0
  34. parsl/tests/test_monitoring/test_basic.py +46 -21
  35. parsl/tests/test_monitoring/test_fuzz_zmq.py +10 -1
  36. parsl/tests/test_monitoring/test_stdouterr.py +137 -0
  37. parsl/tests/test_python_apps/test_context_manager.py +3 -3
  38. parsl/tests/test_python_apps/test_outputs.py +0 -1
  39. parsl/tests/test_scaling/test_regression_1621.py +11 -11
  40. parsl/tests/test_scaling/test_scale_down_htex_unregistered.py +74 -0
  41. parsl/tests/test_staging/test_staging_stdout.py +61 -0
  42. parsl/tests/test_staging/test_zip_out.py +113 -0
  43. parsl/utils.py +11 -2
  44. parsl/version.py +1 -1
  45. {parsl-2024.4.8.data → parsl-2024.4.22.data}/scripts/process_worker_pool.py +3 -1
  46. {parsl-2024.4.8.dist-info → parsl-2024.4.22.dist-info}/METADATA +5 -4
  47. {parsl-2024.4.8.dist-info → parsl-2024.4.22.dist-info}/RECORD +53 -48
  48. {parsl-2024.4.8.data → parsl-2024.4.22.data}/scripts/exec_parsl_function.py +0 -0
  49. {parsl-2024.4.8.data → parsl-2024.4.22.data}/scripts/parsl_coprocess.py +0 -0
  50. {parsl-2024.4.8.dist-info → parsl-2024.4.22.dist-info}/LICENSE +0 -0
  51. {parsl-2024.4.8.dist-info → parsl-2024.4.22.dist-info}/WHEEL +0 -0
  52. {parsl-2024.4.8.dist-info → parsl-2024.4.22.dist-info}/entry_points.txt +0 -0
  53. {parsl-2024.4.8.dist-info → parsl-2024.4.22.dist-info}/top_level.txt +0 -0
@@ -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]") -> None:
103
+ resource_msgs: "queue.Queue[AddressedMonitoringMessage]",
104
+ exit_event: Event) -> None:
102
105
  try:
103
- router_keep_going = True
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)))
@@ -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/proc_id: {0}".format(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')])
@@ -8,5 +8,4 @@ from parsl.data_provider.file_noop import NoOpFileStaging
8
8
 
9
9
  def fresh_config():
10
10
  return Config(executors=[WorkQueueExecutor(port=9000,
11
- coprocess=True,
12
- storage_access=[FTPInTaskStaging(), HTTPInTaskStaging(), NoOpFileStaging()])])
11
+ coprocess=True)])
parsl/tests/conftest.py CHANGED
@@ -135,28 +135,27 @@ def pytest_configure(config):
135
135
  )
136
136
  config.addinivalue_line(
137
137
  'markers',
138
- 'noci: mark test to be unsuitable for running during automated tests'
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
- 'cleannet: Enable tests that require a clean network connection (such as for testing FTP)'
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
- 'issue363: Marks tests that require a shared filesystem for stdout/stderr - see issue #363'
146
+ 'sshd_required: Marks tests that require a SSHD'
148
147
  )
149
148
  config.addinivalue_line(
150
149
  'markers',
151
- 'staging_required: Marks tests that require a staging provider, when there is no sharedFS)'
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
- 'sshd_required: Marks tests that require a SSHD'
154
+ 'issue3328: Marks tests broken by issue #3328'
156
155
  )
157
156
  config.addinivalue_line(
158
157
  'markers',
159
- 'multiple_cores_required: Marks tests that require multiple cores, such as htex affinity'
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
- @pytest.mark.issue363
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()
@@ -8,7 +8,6 @@ def foo(z=2, stdout=None):
8
8
  return f"echo {z}"
9
9
 
10
10
 
11
- @pytest.mark.issue363
12
11
  def test_command_format_1(tmpd_cwd):
13
12
  """Testing command format for BashApps
14
13
  """
@@ -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
@@ -15,7 +15,7 @@ def local_setup():
15
15
 
16
16
 
17
17
  def local_teardown():
18
- parsl.dfk().cleanup
18
+ parsl.dfk().cleanup()
19
19
  parsl.clear()
20
20
 
21
21
 
@@ -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
- 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.
@@ -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:
@@ -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()
@@ -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
- dfk = parsl.dfk()
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