toil 8.0.0__py3-none-any.whl → 8.1.0b1__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 (41) hide show
  1. toil/__init__.py +4 -4
  2. toil/batchSystems/options.py +1 -0
  3. toil/batchSystems/slurm.py +227 -83
  4. toil/common.py +161 -45
  5. toil/cwl/cwltoil.py +31 -10
  6. toil/job.py +47 -38
  7. toil/jobStores/aws/jobStore.py +46 -10
  8. toil/lib/aws/session.py +14 -3
  9. toil/lib/aws/utils.py +92 -35
  10. toil/lib/dockstore.py +379 -0
  11. toil/lib/ec2nodes.py +3 -2
  12. toil/lib/history.py +1271 -0
  13. toil/lib/history_submission.py +681 -0
  14. toil/lib/io.py +22 -1
  15. toil/lib/misc.py +18 -0
  16. toil/lib/retry.py +10 -10
  17. toil/lib/{integration.py → trs.py} +95 -46
  18. toil/lib/web.py +38 -0
  19. toil/options/common.py +17 -2
  20. toil/options/cwl.py +10 -0
  21. toil/provisioners/gceProvisioner.py +4 -4
  22. toil/server/cli/wes_cwl_runner.py +3 -3
  23. toil/server/utils.py +2 -3
  24. toil/statsAndLogging.py +35 -1
  25. toil/test/batchSystems/test_slurm.py +172 -2
  26. toil/test/cwl/conftest.py +39 -0
  27. toil/test/cwl/cwlTest.py +105 -2
  28. toil/test/cwl/optional-file.cwl +18 -0
  29. toil/test/lib/test_history.py +212 -0
  30. toil/test/lib/test_trs.py +161 -0
  31. toil/test/wdl/wdltoil_test.py +1 -1
  32. toil/version.py +10 -10
  33. toil/wdl/wdltoil.py +23 -9
  34. toil/worker.py +113 -33
  35. {toil-8.0.0.dist-info → toil-8.1.0b1.dist-info}/METADATA +9 -4
  36. {toil-8.0.0.dist-info → toil-8.1.0b1.dist-info}/RECORD +40 -34
  37. {toil-8.0.0.dist-info → toil-8.1.0b1.dist-info}/WHEEL +1 -1
  38. toil/test/lib/test_integration.py +0 -104
  39. {toil-8.0.0.dist-info → toil-8.1.0b1.dist-info}/LICENSE +0 -0
  40. {toil-8.0.0.dist-info → toil-8.1.0b1.dist-info}/entry_points.txt +0 -0
  41. {toil-8.0.0.dist-info → toil-8.1.0b1.dist-info}/top_level.txt +0 -0
toil/common.py CHANGED
@@ -14,6 +14,7 @@
14
14
  import json
15
15
  import logging
16
16
  import os
17
+ import platform
17
18
  import pickle
18
19
  import re
19
20
  import signal
@@ -53,6 +54,7 @@ import requests
53
54
  from configargparse import ArgParser, YAMLConfigFileParser
54
55
  from ruamel.yaml import YAML
55
56
  from ruamel.yaml.comments import CommentedMap
57
+ from ruamel.yaml.scalarstring import DoubleQuotedScalarString
56
58
 
57
59
  from toil import logProcessContext, lookupEnvVar
58
60
  from toil.batchSystems.options import set_batchsystem_options
@@ -69,7 +71,10 @@ from toil.bus import (
69
71
  )
70
72
  from toil.fileStores import FileID
71
73
  from toil.lib.compatibility import deprecated
72
- from toil.lib.io import AtomicFileCreate, try_path
74
+ from toil.lib.history import HistoryManager
75
+ from toil.lib.history_submission import ask_user_about_publishing_metrics, create_history_submission, create_current_submission
76
+ from toil.lib.io import AtomicFileCreate, try_path, get_toil_home
77
+ from toil.lib.memoize import memoize
73
78
  from toil.lib.retry import retry
74
79
  from toil.lib.threading import ensure_filesystem_lockable
75
80
  from toil.options.common import JOBSTORE_HELP, add_base_toil_options
@@ -79,7 +84,7 @@ from toil.options.wdl import add_wdl_options
79
84
  from toil.provisioners import add_provisioner_options, cluster_factory
80
85
  from toil.realtimeLogger import RealtimeLogger
81
86
  from toil.statsAndLogging import add_logging_options, set_logging_from_options
82
- from toil.version import dockerRegistry, dockerTag, version
87
+ from toil.version import dockerRegistry, dockerTag, version, baseVersion
83
88
 
84
89
  if TYPE_CHECKING:
85
90
  from toil.batchSystems.abstractBatchSystem import AbstractBatchSystem
@@ -92,11 +97,14 @@ if TYPE_CHECKING:
92
97
  UUID_LENGTH = 32
93
98
  logger = logging.getLogger(__name__)
94
99
 
95
- # TODO: should this use an XDG config directory or ~/.config to not clutter the
96
- # base home directory?
97
- TOIL_HOME_DIR: str = os.path.join(os.path.expanduser("~"), ".toil")
98
- DEFAULT_CONFIG_FILE: str = os.path.join(TOIL_HOME_DIR, "default.yaml")
100
+ @memoize
101
+ def get_default_config_path() -> str:
102
+ """
103
+ Get the default path where the Toil configuration file lives.
99
104
 
105
+ The file at the path will not necessarily exist.
106
+ """
107
+ return os.path.join(get_toil_home(), "default.yaml")
100
108
 
101
109
  class Config:
102
110
  """Class to represent configuration operations for a toil workflow run."""
@@ -214,6 +222,9 @@ class Config:
214
222
  write_messages: Optional[str]
215
223
  realTimeLogging: bool
216
224
 
225
+ # Data publishing
226
+ publish_workflow_metrics: Union[Literal["all"], Literal["current"], Literal["no"], None]
227
+
217
228
  # Misc
218
229
  environment: dict[str, str]
219
230
  disableChaining: bool
@@ -387,6 +398,9 @@ class Config:
387
398
  set_option("writeLogsGzip")
388
399
  set_option("writeLogsFromAllJobs")
389
400
  set_option("write_messages")
401
+
402
+ # Data Publishing Options
403
+ set_option("publish_workflow_metrics")
390
404
 
391
405
  if self.write_messages is None:
392
406
  # The user hasn't specified a place for the message bus so we
@@ -463,44 +477,20 @@ class Config:
463
477
  def __hash__(self) -> int:
464
478
  return self.__dict__.__hash__() # type: ignore
465
479
 
466
-
467
- def check_and_create_toil_home_dir() -> None:
468
- """
469
- Ensure that TOIL_HOME_DIR exists.
470
-
471
- Raises an error if it does not exist and cannot be created. Safe to run
472
- simultaneously in multiple processes.
473
- """
474
-
475
- dir_path = try_path(TOIL_HOME_DIR)
476
- if dir_path is None:
477
- raise RuntimeError(
478
- f"Cannot create or access Toil configuration directory {TOIL_HOME_DIR}"
479
- )
480
-
481
-
482
- def check_and_create_default_config_file() -> None:
480
+ def ensure_config(filepath: str) -> None:
483
481
  """
484
- If the default config file does not exist, create it in the Toil home directory. Create the Toil home directory
485
- if needed
482
+ If the config file at the filepath does not exist, create it.
483
+ The parent directory should be created prior to calling this.
486
484
 
487
- Raises an error if the default config file cannot be created.
485
+ Raises an error if the config file cannot be created.
488
486
  Safe to run simultaneously in multiple processes. If this process runs
489
- this function, it will always see the default config file existing with
487
+ this function, it will always see the config file existing with
490
488
  parseable contents, even if other processes are racing to create it.
491
489
 
492
- No process will see an empty or partially-written default config file.
493
- """
494
- check_and_create_toil_home_dir()
495
- # The default config file did not appear to exist when we checked.
496
- # It might exist now, though. Try creating it.
497
- check_and_create_config_file(DEFAULT_CONFIG_FILE)
498
-
490
+ No process will see a new empty or partially-written config file. The
491
+ caller should still check to make sure there isn't a preexisting empty file
492
+ here.
499
493
 
500
- def check_and_create_config_file(filepath: str) -> None:
501
- """
502
- If the config file at the filepath does not exist, try creating it.
503
- The parent directory should be created prior to calling this
504
494
  :param filepath: path to config file
505
495
  :return: None
506
496
  """
@@ -648,9 +638,39 @@ def generate_config(filepath: str) -> None:
648
638
  yaml.dump(
649
639
  data,
650
640
  f,
641
+ # Comment everything out, Unix config file style, to show defaults
651
642
  transform=lambda s: re.sub(r"^(.)", r"#\1", s, flags=re.MULTILINE),
652
643
  )
653
644
 
645
+ def update_config(filepath: str, key: str, new_value: Union[str, bool, int, float]) -> None:
646
+ """
647
+ Set the given top-level key to the given value in the given YAML config
648
+ file.
649
+
650
+ Does not dramatically alter comments or formatting, and does not make a
651
+ partially-written file visible.
652
+
653
+ :param key: Setting to set. Must be the command-line option name, not the
654
+ destination variable name.
655
+ """
656
+
657
+ yaml = YAML(typ="rt")
658
+ data = yaml.load(open(filepath))
659
+
660
+ logger.info("Change config field %s from %s to %s", key, repr(data.get(key, None)), repr(new_value))
661
+
662
+ if isinstance(new_value, str):
663
+ # Strings with some values (no, yes) will be interpreted as booleans on
664
+ # load if not quoted. But ruamel is not determining that this is needed
665
+ # on serialization for newly-added values. So if we set something to a
666
+ # string we always quote it.
667
+ data[key] = DoubleQuotedScalarString(new_value)
668
+ else:
669
+ data[key] = new_value
670
+
671
+ with AtomicFileCreate(filepath) as temp_path:
672
+ with open(temp_path, "w") as f:
673
+ yaml.dump(data, f)
654
674
 
655
675
  def parser_with_common_options(
656
676
  provisioner_options: bool = False,
@@ -708,11 +728,13 @@ def addOptions(
708
728
  f"Unanticipated class: {parser.__class__}. Must be: argparse.ArgumentParser or ArgumentGroup."
709
729
  )
710
730
 
731
+ config_path = get_default_config_path()
732
+
711
733
  if isinstance(parser, ArgParser):
712
734
  # in case the user passes in their own configargparse instance instead of calling getDefaultArgumentParser()
713
735
  # this forces configargparser to process the config file in YAML rather than in it's own format
714
736
  parser._config_file_parser = YAMLConfigFileParser() # type: ignore[misc]
715
- parser._default_config_files = [DEFAULT_CONFIG_FILE] # type: ignore[misc]
737
+ parser._default_config_files = [config_path] # type: ignore[misc]
716
738
  else:
717
739
  # configargparse advertises itself as a drag and drop replacement, and running the normal argparse ArgumentParser
718
740
  # through this code still seems to work (with the exception of --config and environmental variables)
@@ -723,24 +745,24 @@ def addOptions(
723
745
  DeprecationWarning,
724
746
  )
725
747
 
726
- check_and_create_default_config_file()
748
+ ensure_config(config_path)
727
749
  # Check on the config file to make sure it is sensible
728
- config_status = os.stat(DEFAULT_CONFIG_FILE)
750
+ config_status = os.stat(config_path)
729
751
  if config_status.st_size == 0:
730
752
  # If we have an empty config file, someone has to manually delete
731
753
  # it before we will work again.
732
754
  raise RuntimeError(
733
- f"Config file {DEFAULT_CONFIG_FILE} exists but is empty. Delete it! Stat says: {config_status}"
755
+ f"Config file {config_path} exists but is empty. Delete it! Stat says: {config_status}"
734
756
  )
735
757
  try:
736
- with open(DEFAULT_CONFIG_FILE) as f:
758
+ with open(config_path) as f:
737
759
  yaml = YAML(typ="safe")
738
760
  s = yaml.load(f)
739
761
  logger.debug("Initialized default configuration: %s", json.dumps(s))
740
762
  except:
741
763
  # Something went wrong reading the default config, so dump its
742
764
  # contents to the log.
743
- logger.info("Configuration file contents: %s", open(DEFAULT_CONFIG_FILE).read())
765
+ logger.info("Configuration file contents: %s", open(config_path).read())
744
766
  raise
745
767
 
746
768
  # Add base toil options
@@ -902,8 +924,9 @@ class Toil(ContextManager["Toil"]):
902
924
  _jobStore: "AbstractJobStore"
903
925
  _batchSystem: "AbstractBatchSystem"
904
926
  _provisioner: Optional["AbstractProvisioner"]
927
+ _start_time: float
905
928
 
906
- def __init__(self, options: Namespace) -> None:
929
+ def __init__(self, options: Namespace, workflow_name: Optional[str] = None, trs_spec: Optional[str] = None) -> None:
907
930
  """
908
931
  Initialize a Toil object from the given options.
909
932
 
@@ -911,6 +934,12 @@ class Toil(ContextManager["Toil"]):
911
934
  done when the context is entered.
912
935
 
913
936
  :param options: command line options specified by the user
937
+ :param workflow_name: A human-readable name (probably a filename, URL,
938
+ or TRS specifier) for the workflow being run. Used for Toil history
939
+ storage.
940
+ :param trs_spec: A TRS id:version string for the workflow being run, if
941
+ any. Used for Toil history storage and publishing workflow
942
+ execution metrics to Dockstore.
914
943
  """
915
944
  super().__init__()
916
945
  self.options = options
@@ -918,6 +947,17 @@ class Toil(ContextManager["Toil"]):
918
947
  self._inContextManager: bool = False
919
948
  self._inRestart: bool = False
920
949
 
950
+ if workflow_name is None:
951
+ # Try to use the entrypoint file.
952
+ import __main__
953
+ if hasattr(__main__, '__file__'):
954
+ workflow_name = __main__.__file__
955
+ if workflow_name is None:
956
+ # If there's no file, say this is an interactive usage of Toil.
957
+ workflow_name = "<interactive>"
958
+ self._workflow_name: str = workflow_name
959
+ self._trs_spec = trs_spec
960
+
921
961
  def __enter__(self) -> "Toil":
922
962
  """
923
963
  Derive configuration from the command line options.
@@ -937,9 +977,16 @@ class Toil(ContextManager["Toil"]):
937
977
  # Set the caching option because it wasn't set originally, resuming jobstore rebuilds config from CLI options
938
978
  self.options.caching = config.caching
939
979
 
980
+ if self._trs_spec and config.publish_workflow_metrics is None:
981
+ # We could potentially publish this workflow run. Get a call from the user.
982
+ config.publish_workflow_metrics = ask_user_about_publishing_metrics()
983
+
940
984
  if not config.restart:
941
985
  config.prepare_start()
942
986
  jobStore.initialize(config)
987
+ assert config.workflowID is not None
988
+ # Record that there is a workflow beign run
989
+ HistoryManager.record_workflow_creation(config.workflowID, self.canonical_locator(config.jobStore))
943
990
  else:
944
991
  jobStore.resume()
945
992
  # Merge configuration from job store with command line options
@@ -949,6 +996,7 @@ class Toil(ContextManager["Toil"]):
949
996
  jobStore.write_config()
950
997
  self.config = config
951
998
  self._jobStore = jobStore
999
+ self._start_time = time.time()
952
1000
  self._inContextManager = True
953
1001
 
954
1002
  # This will make sure `self.__exit__()` is called when we get a SIGTERM signal.
@@ -968,6 +1016,50 @@ class Toil(ContextManager["Toil"]):
968
1016
  Depending on the configuration, delete the job store.
969
1017
  """
970
1018
  try:
1019
+ if self.config.workflowID is not None:
1020
+ # Record that this attempt to run the workflow succeeded or failed.
1021
+ # TODO: Get ahold of the timing from statsAndLogging instead of redoing it here!
1022
+ # To record the batch system, we need to avoid capturing typos/random text the user types instead of a real batch system.
1023
+ batch_system_type="<Not Initialized>"
1024
+ if hasattr(self, "_batchSystem"):
1025
+ batch_system_type = type(self._batchSystem).__module__ + "." + type(self._batchSystem).__qualname__
1026
+ HistoryManager.record_workflow_attempt(
1027
+ self.config.workflowID,
1028
+ self.config.workflowAttemptNumber,
1029
+ exc_type is None,
1030
+ self._start_time,
1031
+ time.time() - self._start_time,
1032
+ batch_system=batch_system_type,
1033
+ caching=self.config.caching,
1034
+ # Use the git-hash-free Toil version which should not be unique
1035
+ toil_version=baseVersion,
1036
+ # This should always be major.minor.patch.
1037
+ python_version=platform.python_version(),
1038
+ platform_system=platform.system(),
1039
+ platform_machine=platform.machine()
1040
+ )
1041
+
1042
+ if self.config.publish_workflow_metrics == "all":
1043
+ # Publish metrics for all workflows, including previous ones.
1044
+ submission = create_history_submission()
1045
+ while not submission.empty():
1046
+ if not submission.submit():
1047
+ # Submitting this batch failed. An item might be broken
1048
+ # and we don't want to get stuck making no progress on
1049
+ # a batch of stuff that can't really be submitted.
1050
+ break
1051
+ # Keep making submissions until we've uploaded the whole
1052
+ # history or something goes wrong.
1053
+ submission = create_history_submission()
1054
+
1055
+ elif self.config.publish_workflow_metrics == "current" and self.config.workflowID is not None:
1056
+ # Publish metrics for this run only. Might be empty if we had no TRS ID.
1057
+ create_current_submission(self.config.workflowID, self.config.workflowAttemptNumber).submit()
1058
+
1059
+ # Make sure the history doesn't stay too big
1060
+ HistoryManager.enforce_byte_size_limit()
1061
+
1062
+
971
1063
  if (
972
1064
  exc_type is not None
973
1065
  and self.config.clean == "onError"
@@ -1012,6 +1104,9 @@ class Toil(ContextManager["Toil"]):
1012
1104
  """
1013
1105
  self._assertContextManagerUsed()
1014
1106
 
1107
+ assert self.config.workflowID is not None
1108
+ HistoryManager.record_workflow_metadata(self.config.workflowID, self._workflow_name, self._trs_spec)
1109
+
1015
1110
  from toil.job import Job
1016
1111
 
1017
1112
  # Check that the rootJob is an instance of the Job class
@@ -1110,6 +1205,8 @@ class Toil(ContextManager["Toil"]):
1110
1205
  )
1111
1206
  self._provisioner.setAutoscaledNodeTypes(self.config.nodeTypes)
1112
1207
 
1208
+ JOB_STORE_TYPES = ["file", "aws", "google"]
1209
+
1113
1210
  @classmethod
1114
1211
  def getJobStore(cls, locator: str) -> "AbstractJobStore":
1115
1212
  """
@@ -1137,6 +1234,14 @@ class Toil(ContextManager["Toil"]):
1137
1234
 
1138
1235
  @staticmethod
1139
1236
  def parseLocator(locator: str) -> tuple[str, str]:
1237
+ """
1238
+ Parse a job store locator to a type string and the data needed for that
1239
+ implementation to connect to it.
1240
+
1241
+ Does not validate the set of possible job store types.
1242
+
1243
+ :raises RuntimeError: if the locator is not in the approproate syntax.
1244
+ """
1140
1245
  if locator[0] in "/." or ":" not in locator:
1141
1246
  return "file", locator
1142
1247
  else:
@@ -1153,6 +1258,17 @@ class Toil(ContextManager["Toil"]):
1153
1258
  raise ValueError(f"Can't have a ':' in the name: '{name}'.")
1154
1259
  return f"{name}:{rest}"
1155
1260
 
1261
+ @classmethod
1262
+ def canonical_locator(cls, locator: str) -> str:
1263
+ """
1264
+ Turn a job store locator into one that will work from any directory and
1265
+ always includes the explicit type of job store.
1266
+ """
1267
+ job_store_type, rest = cls.parseLocator(locator)
1268
+ if job_store_type == "file":
1269
+ rest = os.path.abspath(rest)
1270
+ return cls.buildLocator(job_store_type, rest)
1271
+
1156
1272
  @classmethod
1157
1273
  def resumeJobStore(cls, locator: str) -> "AbstractJobStore":
1158
1274
  jobStore = cls.getJobStore(locator)
toil/cwl/cwltoil.py CHANGED
@@ -111,7 +111,7 @@ from toil.batchSystems.abstractBatchSystem import InsufficientSystemResources
111
111
  from toil.batchSystems.registry import DEFAULT_BATCH_SYSTEM
112
112
  from toil.common import Config, Toil, addOptions
113
113
  from toil.cwl import check_cwltool_version
114
- from toil.lib.integration import resolve_workflow
114
+ from toil.lib.trs import resolve_workflow
115
115
  from toil.lib.misc import call_command
116
116
  from toil.provisioners.clusterScaler import JobTooBigError
117
117
 
@@ -1214,7 +1214,7 @@ def toil_make_tool(
1214
1214
  return cwltool.workflow.default_make_tool(toolpath_object, loadingContext)
1215
1215
 
1216
1216
 
1217
- # When a file we want to have is missing, we can give it this sentinal location
1217
+ # When a file we want to have is missing, we can give it this sentinel location
1218
1218
  # URI instead of raising an error right away, in case it is optional.
1219
1219
  MISSING_FILE = "missing://"
1220
1220
 
@@ -1812,6 +1812,9 @@ def convert_file_uri_to_toil_uri(
1812
1812
  # with unsupportedRequirement when retrieving later with getFile
1813
1813
  elif file_uri.startswith("_:"):
1814
1814
  return file_uri
1815
+ elif file_uri.startswith(MISSING_FILE):
1816
+ # We cannot import a missing file
1817
+ raise FileNotFoundError(f"Could not find {file_uri[len(MISSING_FILE):]}")
1815
1818
  else:
1816
1819
  file_uri = existing.get(file_uri, file_uri)
1817
1820
  if file_uri not in index:
@@ -1876,7 +1879,7 @@ def extract_file_uri_once(
1876
1879
  ):
1877
1880
  if mark_broken:
1878
1881
  logger.debug("File %s is missing", file_metadata)
1879
- file_metadata["location"] = location = MISSING_FILE
1882
+ file_metadata["location"] = location = MISSING_FILE + location
1880
1883
  else:
1881
1884
  raise cwl_utils.errors.WorkflowException(
1882
1885
  "File is missing: %s" % file_metadata
@@ -3599,6 +3602,7 @@ class CWLInstallImportsJob(Job):
3599
3602
  """
3600
3603
  Given a mapping of filenames to Toil file IDs, replace the filename with the file IDs throughout the CWL object.
3601
3604
  """
3605
+
3602
3606
  def fill_in_file(filename: str) -> FileID:
3603
3607
  """
3604
3608
  Return the file name's associated Toil file ID
@@ -3954,10 +3958,10 @@ def filtered_secondary_files(
3954
3958
  sf,
3955
3959
  )
3956
3960
  # remove secondary files that are not present in the filestore or pointing
3957
- # to existant things on disk
3961
+ # to existent things on disk
3958
3962
  for sf in intermediate_secondary_files:
3959
3963
  sf_loc = cast(str, sf.get("location", ""))
3960
- if sf_loc != MISSING_FILE or sf.get("class", "") == "Directory":
3964
+ if not sf_loc.startswith(MISSING_FILE) or sf.get("class", "") == "Directory":
3961
3965
  # Pass imported files, and all Directories
3962
3966
  final_secondary_files.append(sf)
3963
3967
  else:
@@ -4166,15 +4170,15 @@ def get_options(args: list[str]) -> Namespace:
4166
4170
  description=textwrap.dedent(
4167
4171
  """
4168
4172
  positional arguments:
4169
-
4173
+
4170
4174
  WORKFLOW CWL file to run.
4171
4175
 
4172
4176
  INFILE YAML or JSON file of workflow inputs.
4173
-
4177
+
4174
4178
  WF_OPTIONS Additional inputs to the workflow as command-line
4175
4179
  flags. If CWL workflow takes an input, the name of the
4176
4180
  input can be used as an option. For example:
4177
-
4181
+
4178
4182
  %(prog)s workflow.cwl --file1 file
4179
4183
 
4180
4184
  If an input has the same name as a Toil option, pass
@@ -4261,8 +4265,18 @@ def main(args: Optional[list[str]] = None, stdout: TextIO = sys.stdout) -> int:
4261
4265
  runtime_context.move_outputs = "leave"
4262
4266
  runtime_context.rm_tmpdir = False
4263
4267
  runtime_context.streaming_allowed = not options.disable_streaming
4268
+ if options.cachedir is not None:
4269
+ runtime_context.cachedir = os.path.abspath(options.cachedir)
4270
+ # Automatically bypass the file store to be compatible with cwltool caching
4271
+ # Otherwise, the CWL caching code makes links to temporary local copies
4272
+ # of filestore files and caches those.
4273
+ logger.debug("CWL task caching is turned on. Bypassing file store.")
4274
+ options.bypass_file_store = True
4264
4275
  if options.mpi_config_file is not None:
4265
4276
  runtime_context.mpi_config = MpiConfig.load(options.mpi_config_file)
4277
+ if cwltool.main.check_working_directories(runtime_context) is not None:
4278
+ logger.error("Failed to create directory. If using tmpdir_prefix, tmpdir_outdir_prefix, or cachedir, consider changing directory locations.")
4279
+ return 1
4266
4280
  setattr(runtime_context, "bypass_file_store", options.bypass_file_store)
4267
4281
  if options.bypass_file_store and options.destBucket:
4268
4282
  # We use the file store to write to buckets, so we can't do this (yet?)
@@ -4293,6 +4307,10 @@ def main(args: Optional[list[str]] = None, stdout: TextIO = sys.stdout) -> int:
4293
4307
 
4294
4308
  try:
4295
4309
 
4310
+ # We might have workflow metadata to pass to Toil
4311
+ workflow_name=None
4312
+ trs_spec = None
4313
+
4296
4314
  if not options.restart:
4297
4315
  # Make a version of the config based on the initial options, for
4298
4316
  # setting up CWL option stuff
@@ -4302,7 +4320,9 @@ def main(args: Optional[list[str]] = None, stdout: TextIO = sys.stdout) -> int:
4302
4320
  # Before showing the options to any cwltool stuff that wants to
4303
4321
  # load the workflow, transform options.cwltool, where our
4304
4322
  # argument for what to run is, to handle Dockstore workflows.
4305
- options.cwltool = resolve_workflow(options.cwltool)
4323
+ options.cwltool, trs_spec = resolve_workflow(options.cwltool)
4324
+ # Figure out what to call the workflow
4325
+ workflow_name = trs_spec or options.cwltool
4306
4326
 
4307
4327
  # TODO: why are we doing this? Does this get applied to all
4308
4328
  # tools as a default or something?
@@ -4474,7 +4494,7 @@ def main(args: Optional[list[str]] = None, stdout: TextIO = sys.stdout) -> int:
4474
4494
  logger.debug("Root tool: %s", tool)
4475
4495
  tool = remove_pickle_problems(tool)
4476
4496
 
4477
- with Toil(options) as toil:
4497
+ with Toil(options, workflow_name=workflow_name, trs_spec=trs_spec) as toil:
4478
4498
  if options.restart:
4479
4499
  outobj = toil.restart()
4480
4500
  else:
@@ -4575,6 +4595,7 @@ def main(args: Optional[list[str]] = None, stdout: TextIO = sys.stdout) -> int:
4575
4595
  InvalidImportExportUrlException,
4576
4596
  UnimplementedURLException,
4577
4597
  JobTooBigError,
4598
+ FileNotFoundError
4578
4599
  ) as err:
4579
4600
  logging.error(err)
4580
4601
  return 1
toil/job.py CHANGED
@@ -3140,46 +3140,55 @@ class Job:
3140
3140
  startTime = time.time()
3141
3141
  startClock = ResourceMonitor.get_total_cpu_time()
3142
3142
  baseDir = os.getcwd()
3143
+
3144
+ succeeded = False
3145
+ try:
3146
+ yield
3147
+
3148
+ if "download_only" in self._debug_flags:
3149
+ # We should stop right away
3150
+ logger.debug("Job did not stop itself after downloading files; stopping.")
3151
+ raise DebugStoppingPointReached()
3152
+
3153
+ # If the job is not a checkpoint job, add the promise files to delete
3154
+ # to the list of jobStoreFileIDs to delete
3155
+ # TODO: why is Promise holding a global list here???
3156
+ if not self.checkpoint:
3157
+ for jobStoreFileID in Promise.filesToDelete:
3158
+ # Make sure to wrap the job store ID in a FileID object so the file store will accept it
3159
+ # TODO: talk directly to the job store here instead.
3160
+ fileStore.deleteGlobalFile(FileID(jobStoreFileID, 0))
3161
+ else:
3162
+ # Else copy them to the job description to delete later
3163
+ self.description.checkpointFilesToDelete = list(Promise.filesToDelete)
3164
+ Promise.filesToDelete.clear()
3165
+ # Now indicate the asynchronous update of the job can happen
3166
+ fileStore.startCommit(jobState=True)
3143
3167
 
3144
- yield
3145
-
3146
- if "download_only" in self._debug_flags:
3147
- # We should stop right away
3148
- logger.debug("Job did not stop itself after downloading files; stopping.")
3149
- raise DebugStoppingPointReached()
3150
-
3151
- # If the job is not a checkpoint job, add the promise files to delete
3152
- # to the list of jobStoreFileIDs to delete
3153
- # TODO: why is Promise holding a global list here???
3154
- if not self.checkpoint:
3155
- for jobStoreFileID in Promise.filesToDelete:
3156
- # Make sure to wrap the job store ID in a FileID object so the file store will accept it
3157
- # TODO: talk directly to the job store here instead.
3158
- fileStore.deleteGlobalFile(FileID(jobStoreFileID, 0))
3159
- else:
3160
- # Else copy them to the job description to delete later
3161
- self.description.checkpointFilesToDelete = list(Promise.filesToDelete)
3162
- Promise.filesToDelete.clear()
3163
- # Now indicate the asynchronous update of the job can happen
3164
- fileStore.startCommit(jobState=True)
3165
- # Change dir back to cwd dir, if changed by job (this is a safety issue)
3166
- if os.getcwd() != baseDir:
3167
- os.chdir(baseDir)
3168
- # Finish up the stats
3169
- if stats is not None:
3170
- totalCpuTime, totalMemoryUsage = (
3171
- ResourceMonitor.get_total_cpu_time_and_memory_usage()
3172
- )
3173
- stats.jobs.append(
3174
- Expando(
3175
- time=str(time.time() - startTime),
3176
- clock=str(totalCpuTime - startClock),
3177
- class_name=self._jobName(),
3178
- memory=str(totalMemoryUsage),
3179
- requested_cores=str(self.cores),
3180
- disk=str(fileStore.get_disk_usage()),
3168
+ succeeded = True
3169
+ finally:
3170
+ # Change dir back to cwd dir, if changed by job (this is a safety issue)
3171
+ if os.getcwd() != baseDir:
3172
+ os.chdir(baseDir)
3173
+ # Finish up the stats
3174
+ if stats is not None:
3175
+ totalCpuTime, total_memory_kib = (
3176
+ ResourceMonitor.get_total_cpu_time_and_memory_usage()
3177
+ )
3178
+ stats.jobs.append(
3179
+ # TODO: We represent everything as strings in the stats
3180
+ # even though the JSON transport can take bools and floats.
3181
+ Expando(
3182
+ start=str(startTime),
3183
+ time=str(time.time() - startTime),
3184
+ clock=str(totalCpuTime - startClock),
3185
+ class_name=self._jobName(),
3186
+ memory=str(total_memory_kib),
3187
+ requested_cores=str(self.cores), # TODO: Isn't this really consumed cores?
3188
+ disk=str(fileStore.get_disk_usage()),
3189
+ succeeded=str(succeeded),
3190
+ )
3181
3191
  )
3182
- )
3183
3192
 
3184
3193
  def _runner(
3185
3194
  self,