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.
- toil/__init__.py +4 -4
- toil/batchSystems/options.py +1 -0
- toil/batchSystems/slurm.py +227 -83
- toil/common.py +161 -45
- toil/cwl/cwltoil.py +31 -10
- toil/job.py +47 -38
- toil/jobStores/aws/jobStore.py +46 -10
- toil/lib/aws/session.py +14 -3
- toil/lib/aws/utils.py +92 -35
- toil/lib/dockstore.py +379 -0
- toil/lib/ec2nodes.py +3 -2
- toil/lib/history.py +1271 -0
- toil/lib/history_submission.py +681 -0
- toil/lib/io.py +22 -1
- toil/lib/misc.py +18 -0
- toil/lib/retry.py +10 -10
- toil/lib/{integration.py → trs.py} +95 -46
- toil/lib/web.py +38 -0
- toil/options/common.py +17 -2
- toil/options/cwl.py +10 -0
- toil/provisioners/gceProvisioner.py +4 -4
- toil/server/cli/wes_cwl_runner.py +3 -3
- toil/server/utils.py +2 -3
- toil/statsAndLogging.py +35 -1
- toil/test/batchSystems/test_slurm.py +172 -2
- toil/test/cwl/conftest.py +39 -0
- toil/test/cwl/cwlTest.py +105 -2
- toil/test/cwl/optional-file.cwl +18 -0
- toil/test/lib/test_history.py +212 -0
- toil/test/lib/test_trs.py +161 -0
- toil/test/wdl/wdltoil_test.py +1 -1
- toil/version.py +10 -10
- toil/wdl/wdltoil.py +23 -9
- toil/worker.py +113 -33
- {toil-8.0.0.dist-info → toil-8.1.0b1.dist-info}/METADATA +9 -4
- {toil-8.0.0.dist-info → toil-8.1.0b1.dist-info}/RECORD +40 -34
- {toil-8.0.0.dist-info → toil-8.1.0b1.dist-info}/WHEEL +1 -1
- toil/test/lib/test_integration.py +0 -104
- {toil-8.0.0.dist-info → toil-8.1.0b1.dist-info}/LICENSE +0 -0
- {toil-8.0.0.dist-info → toil-8.1.0b1.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
485
|
-
|
|
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
|
|
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
|
|
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
|
|
493
|
-
|
|
494
|
-
|
|
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 = [
|
|
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
|
-
|
|
748
|
+
ensure_config(config_path)
|
|
727
749
|
# Check on the config file to make sure it is sensible
|
|
728
|
-
config_status = os.stat(
|
|
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 {
|
|
755
|
+
f"Config file {config_path} exists but is empty. Delete it! Stat says: {config_status}"
|
|
734
756
|
)
|
|
735
757
|
try:
|
|
736
|
-
with open(
|
|
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(
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
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,
|