parsl 2024.4.22__py3-none-any.whl → 2024.5.6__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 (34) hide show
  1. parsl/config.py +10 -1
  2. parsl/data_provider/zip.py +32 -0
  3. parsl/dataflow/dflow.py +34 -26
  4. parsl/executors/high_throughput/executor.py +7 -1
  5. parsl/executors/status_handling.py +0 -3
  6. parsl/executors/taskvine/executor.py +0 -31
  7. parsl/executors/workqueue/executor.py +0 -30
  8. parsl/jobs/job_status_poller.py +1 -3
  9. parsl/monitoring/monitoring.py +3 -0
  10. parsl/monitoring/radios.py +1 -1
  11. parsl/providers/kubernetes/kube.py +20 -1
  12. parsl/tests/configs/local_threads_checkpoint_periodic.py +8 -10
  13. parsl/tests/conftest.py +12 -1
  14. parsl/tests/test_bash_apps/test_std_uri.py +128 -0
  15. parsl/tests/test_checkpointing/test_periodic.py +20 -33
  16. parsl/tests/test_htex/test_basic.py +2 -2
  17. parsl/tests/test_htex/test_missing_worker.py +0 -4
  18. parsl/tests/test_mpi_apps/test_resource_spec.py +2 -8
  19. parsl/tests/test_staging/test_zip_in.py +42 -0
  20. parsl/tests/test_staging/test_zip_to_zip.py +44 -0
  21. parsl/tests/unit/__init__.py +0 -0
  22. parsl/tests/unit/test_file.py +99 -0
  23. parsl/usage_tracking/api.py +66 -0
  24. parsl/usage_tracking/usage.py +39 -26
  25. parsl/version.py +1 -1
  26. {parsl-2024.4.22.dist-info → parsl-2024.5.6.dist-info}/METADATA +2 -2
  27. {parsl-2024.4.22.dist-info → parsl-2024.5.6.dist-info}/RECORD +34 -28
  28. {parsl-2024.4.22.data → parsl-2024.5.6.data}/scripts/exec_parsl_function.py +0 -0
  29. {parsl-2024.4.22.data → parsl-2024.5.6.data}/scripts/parsl_coprocess.py +0 -0
  30. {parsl-2024.4.22.data → parsl-2024.5.6.data}/scripts/process_worker_pool.py +0 -0
  31. {parsl-2024.4.22.dist-info → parsl-2024.5.6.dist-info}/LICENSE +0 -0
  32. {parsl-2024.4.22.dist-info → parsl-2024.5.6.dist-info}/WHEEL +0 -0
  33. {parsl-2024.4.22.dist-info → parsl-2024.5.6.dist-info}/entry_points.txt +0 -0
  34. {parsl-2024.4.22.dist-info → parsl-2024.5.6.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)}
@@ -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
@@ -177,11 +177,9 @@ class DataFlowKernel:
177
177
 
178
178
  # this must be set before executors are added since add_executors calls
179
179
  # job_status_poller.add_executors.
180
- radio = self.monitoring.radio if self.monitoring else None
181
180
  self.job_status_poller = JobStatusPoller(strategy=self.config.strategy,
182
181
  strategy_period=self.config.strategy_period,
183
- max_idletime=self.config.max_idletime,
184
- monitoring=radio)
182
+ max_idletime=self.config.max_idletime)
185
183
 
186
184
  self.executors: Dict[str, ParslExecutor] = {}
187
185
 
@@ -798,7 +796,6 @@ class DataFlowKernel:
798
796
  # be the original function wrapped with an in-task stageout wrapper), a
799
797
  # rewritten File object to be passed to task to be executed
800
798
 
801
- @typechecked
802
799
  def stageout_one_file(file: File, rewritable_func: Callable):
803
800
  if not self.check_staging_inhibited(kwargs):
804
801
  # replace a File with a DataFuture - either completing when the stageout
@@ -996,32 +993,16 @@ class DataFlowKernel:
996
993
  executor = random.choice(choices)
997
994
  logger.debug("Task {} will be sent to executor {}".format(task_id, executor))
998
995
 
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
996
  resource_specification = app_kwargs.get('parsl_resource_specification', {})
1019
997
 
1020
998
  task_record: TaskRecord
1021
- task_record = {'depends': [],
999
+ task_record = {'args': app_args,
1000
+ 'depends': [],
1022
1001
  'dfk': self,
1023
1002
  'executor': executor,
1003
+ 'func': func,
1024
1004
  'func_name': func.__name__,
1005
+ 'kwargs': app_kwargs,
1025
1006
  'memoize': cache,
1026
1007
  'hashsum': None,
1027
1008
  'exec_fu': None,
@@ -1043,18 +1024,30 @@ class DataFlowKernel:
1043
1024
 
1044
1025
  self.update_task_state(task_record, States.unsched)
1045
1026
 
1027
+ for kw in ['stdout', 'stderr']:
1028
+ if kw in app_kwargs:
1029
+ if app_kwargs[kw] == parsl.AUTO_LOGNAME:
1030
+ if kw not in ignore_for_cache:
1031
+ ignore_for_cache += [kw]
1032
+ if self.config.std_autopath is None:
1033
+ app_kwargs[kw] = self.default_std_autopath(task_record, kw)
1034
+ else:
1035
+ app_kwargs[kw] = self.config.std_autopath(task_record, kw)
1036
+
1046
1037
  app_fu = AppFuture(task_record)
1038
+ task_record['app_fu'] = app_fu
1047
1039
 
1048
1040
  # Transform remote input files to data futures
1049
1041
  app_args, app_kwargs, func = self._add_input_deps(executor, app_args, app_kwargs, func)
1050
1042
 
1051
1043
  func = self._add_output_deps(executor, app_args, app_kwargs, app_fu, func)
1052
1044
 
1045
+ # Replace the function invocation in the TaskRecord with whatever file-staging
1046
+ # substitutions have been made.
1053
1047
  task_record.update({
1054
1048
  'args': app_args,
1055
1049
  'func': func,
1056
- 'kwargs': app_kwargs,
1057
- 'app_fu': app_fu})
1050
+ 'kwargs': app_kwargs})
1058
1051
 
1059
1052
  assert task_id not in self.tasks
1060
1053
 
@@ -1245,8 +1238,10 @@ class DataFlowKernel:
1245
1238
  self._checkpoint_timer.close()
1246
1239
 
1247
1240
  # Send final stats
1241
+ logger.info("Sending end message for usage tracking")
1248
1242
  self.usage_tracker.send_end_message()
1249
1243
  self.usage_tracker.close()
1244
+ logger.info("Closed usage tracking")
1250
1245
 
1251
1246
  logger.info("Closing job status poller")
1252
1247
  self.job_status_poller.close()
@@ -1440,6 +1435,19 @@ class DataFlowKernel:
1440
1435
  log_std_stream("Standard out", task_record['app_fu'].stdout)
1441
1436
  log_std_stream("Standard error", task_record['app_fu'].stderr)
1442
1437
 
1438
+ def default_std_autopath(self, taskrecord, kw):
1439
+ label = taskrecord['kwargs'].get('label')
1440
+ task_id = taskrecord['id']
1441
+ return os.path.join(
1442
+ self.run_dir,
1443
+ 'task_logs',
1444
+ str(int(task_id / 10000)).zfill(4), # limit logs to 10k entries per directory
1445
+ 'task_{}_{}{}.{}'.format(
1446
+ str(task_id).zfill(4),
1447
+ taskrecord['func_name'],
1448
+ '' if label is None else '_{}'.format(label),
1449
+ kw))
1450
+
1443
1451
 
1444
1452
  class DataFlowKernelLoader:
1445
1453
  """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}
@@ -43,9 +43,6 @@ class BlockProviderExecutor(ParslExecutor):
43
43
  invoking scale_out, but it will not initialize the blocks requested by
44
44
  any init_blocks parameter. Subclasses must implement that behaviour
45
45
  themselves.
46
-
47
- BENC: TODO: block error handling: maybe I want this more user pluggable?
48
- I'm not sure of use cases for switchability at the moment beyond "yes or no"
49
46
  """
50
47
  def __init__(self, *,
51
48
  provider: Optional[ExecutionProvider],
@@ -4,7 +4,6 @@ high-throughput system for delegating Parsl tasks to thousands of remote machine
4
4
  """
5
5
 
6
6
  # Import Python built-in libraries
7
- import atexit
8
7
  import threading
9
8
  import multiprocessing
10
9
  import logging
@@ -180,24 +179,6 @@ class TaskVineExecutor(BlockProviderExecutor, putils.RepresentationMixin):
180
179
  else:
181
180
  self._poncho_available = True
182
181
 
183
- # Register atexit handler to cleanup when Python shuts down
184
- atexit.register(self.atexit_cleanup)
185
-
186
- # Attribute indicating whether this executor was started to shut it down properly.
187
- # This safeguards cases where an object of this executor is created but
188
- # the executor never starts, so it shouldn't be shutdowned.
189
- self._is_started = False
190
-
191
- # Attribute indicating whether this executor was shutdown before.
192
- # This safeguards cases where this object is automatically shut down (e.g.,
193
- # via atexit) and the user also explicitly calls shut down. While this is
194
- # permitted, the effect of an executor shutdown should happen only once.
195
- self._is_shutdown = False
196
-
197
- def atexit_cleanup(self):
198
- # Calls this executor's shutdown method upon Python exiting the process.
199
- self.shutdown()
200
-
201
182
  def _get_launch_command(self, block_id):
202
183
  # Implements BlockProviderExecutor's abstract method.
203
184
  # This executor uses different terminology for worker/launch
@@ -257,9 +238,6 @@ class TaskVineExecutor(BlockProviderExecutor, putils.RepresentationMixin):
257
238
  retrieve Parsl tasks within the TaskVine system.
258
239
  """
259
240
 
260
- # Mark this executor object as started
261
- self._is_started = True
262
-
263
241
  # Synchronize connection and communication settings between the manager and factory
264
242
  self.__synchronize_manager_factory_comm_settings()
265
243
 
@@ -618,14 +596,6 @@ class TaskVineExecutor(BlockProviderExecutor, putils.RepresentationMixin):
618
596
  """Shutdown the executor. Sets flag to cancel the submit process and
619
597
  collector thread, which shuts down the TaskVine system submission.
620
598
  """
621
- if not self._is_started:
622
- # Don't shutdown if the executor never starts.
623
- return
624
-
625
- if self._is_shutdown:
626
- # Don't shutdown this executor again.
627
- return
628
-
629
599
  logger.debug("TaskVine shutdown started")
630
600
  self._should_stop.set()
631
601
 
@@ -650,7 +620,6 @@ class TaskVineExecutor(BlockProviderExecutor, putils.RepresentationMixin):
650
620
  self._finished_task_queue.close()
651
621
  self._finished_task_queue.join_thread()
652
622
 
653
- self._is_shutdown = True
654
623
  logger.debug("TaskVine shutdown completed")
655
624
 
656
625
  @wrap_with_logs
@@ -3,7 +3,6 @@ Cooperative Computing Lab (CCL) at Notre Dame to provide a fault-tolerant,
3
3
  high-throughput system for delegating Parsl tasks to thousands of remote machines
4
4
  """
5
5
 
6
- import atexit
7
6
  import threading
8
7
  import multiprocessing
9
8
  import logging
@@ -298,24 +297,6 @@ class WorkQueueExecutor(BlockProviderExecutor, putils.RepresentationMixin):
298
297
  if self.init_command != "":
299
298
  self.launch_cmd = self.init_command + "; " + self.launch_cmd
300
299
 
301
- # register atexit handler to cleanup when Python shuts down
302
- atexit.register(self.atexit_cleanup)
303
-
304
- # Attribute indicating whether this executor was started to shut it down properly.
305
- # This safeguards cases where an object of this executor is created but
306
- # the executor never starts, so it shouldn't be shutdowned.
307
- self.is_started = False
308
-
309
- # Attribute indicating whether this executor was shutdown before.
310
- # This safeguards cases where this object is automatically shut down (e.g.,
311
- # via atexit) and the user also explicitly calls shut down. While this is
312
- # permitted, the effect of an executor shutdown should happen only once.
313
- self.is_shutdown = False
314
-
315
- def atexit_cleanup(self):
316
- # Calls this executor's shutdown method upon Python exiting the process.
317
- self.shutdown()
318
-
319
300
  def _get_launch_command(self, block_id):
320
301
  # this executor uses different terminology for worker/launch
321
302
  # commands than in htex
@@ -325,8 +306,6 @@ class WorkQueueExecutor(BlockProviderExecutor, putils.RepresentationMixin):
325
306
  """Create submit process and collector thread to create, send, and
326
307
  retrieve Parsl tasks within the Work Queue system.
327
308
  """
328
- # Mark this executor object as started
329
- self.is_started = True
330
309
  self.tasks_lock = threading.Lock()
331
310
 
332
311
  # Create directories for data and results
@@ -713,14 +692,6 @@ class WorkQueueExecutor(BlockProviderExecutor, putils.RepresentationMixin):
713
692
  """Shutdown the executor. Sets flag to cancel the submit process and
714
693
  collector thread, which shuts down the Work Queue system submission.
715
694
  """
716
- if not self.is_started:
717
- # Don't shutdown if the executor never starts.
718
- return
719
-
720
- if self.is_shutdown:
721
- # Don't shutdown this executor again.
722
- return
723
-
724
695
  logger.debug("Work Queue shutdown started")
725
696
  self.should_stop.value = True
726
697
 
@@ -741,7 +712,6 @@ class WorkQueueExecutor(BlockProviderExecutor, putils.RepresentationMixin):
741
712
  self.collector_queue.close()
742
713
  self.collector_queue.join_thread()
743
714
 
744
- self.is_shutdown = True
745
715
  logger.debug("Work Queue shutdown completed")
746
716
 
747
717
  @wrap_with_logs
@@ -1,5 +1,4 @@
1
1
  import logging
2
- import parsl
3
2
  from typing import List, Sequence, Optional, Union
4
3
 
5
4
  from parsl.jobs.strategy import Strategy
@@ -14,8 +13,7 @@ logger = logging.getLogger(__name__)
14
13
 
15
14
  class JobStatusPoller(Timer):
16
15
  def __init__(self, *, strategy: Optional[str], max_idletime: float,
17
- strategy_period: Union[float, int],
18
- monitoring: Optional["parsl.monitoring.radios.MonitoringRadio"] = None) -> None:
16
+ strategy_period: Union[float, int]) -> None:
19
17
  self._executors = [] # type: List[BlockProviderExecutor]
20
18
  self._strategy = Strategy(strategy=strategy,
21
19
  max_idletime=max_idletime)
@@ -244,6 +244,7 @@ class MonitoringHub(RepresentationMixin):
244
244
  self.router_exit_event.set()
245
245
  logger.info("Waiting for router to terminate")
246
246
  self.router_proc.join()
247
+ self.router_proc.close()
247
248
  logger.debug("Finished waiting for router termination")
248
249
  if len(exception_msgs) == 0:
249
250
  logger.debug("Sending STOP to DBM")
@@ -252,6 +253,7 @@ class MonitoringHub(RepresentationMixin):
252
253
  logger.debug("Not sending STOP to DBM, because there were DBM exceptions")
253
254
  logger.debug("Waiting for DB termination")
254
255
  self.dbm_proc.join()
256
+ self.dbm_proc.close()
255
257
  logger.debug("Finished waiting for DBM termination")
256
258
 
257
259
  # should this be message based? it probably doesn't need to be if
@@ -259,6 +261,7 @@ class MonitoringHub(RepresentationMixin):
259
261
  logger.info("Terminating filesystem radio receiver process")
260
262
  self.filesystem_proc.terminate()
261
263
  self.filesystem_proc.join()
264
+ self.filesystem_proc.close()
262
265
 
263
266
  logger.info("Closing monitoring multiprocessing queues")
264
267
  self.exception_q.close()
@@ -177,7 +177,7 @@ class UDPRadio(MonitoringRadio):
177
177
 
178
178
 
179
179
  class MultiprocessingQueueRadio(MonitoringRadio):
180
- """A monitoring radio intended which connects over a multiprocessing Queue.
180
+ """A monitoring radio which connects over a multiprocessing Queue.
181
181
  This radio is intended to be used on the submit side, where components
182
182
  in the submit process, or processes launched by multiprocessing, will have
183
183
  access to a Queue shared with the monitoring database code (bypassing the
@@ -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
- config.load_kube_config()
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
- config = Config(
5
- executors=[
6
- ThreadPoolExecutor(
7
- label='local_threads_checkpoint_periodic',
8
- max_threads=1
9
- )
10
- ],
11
- checkpoint_mode='periodic',
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()