snakemake-executor-plugin-aws-batch-flex 0.1.0__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.
- snakemake_executor_plugin_aws_batch_flex/__init__.py +8 -0
- snakemake_executor_plugin_aws_batch_flex/bootstrap.py +376 -0
- snakemake_executor_plugin_aws_batch_flex/bundle.py +142 -0
- snakemake_executor_plugin_aws_batch_flex/check.py +129 -0
- snakemake_executor_plugin_aws_batch_flex/condacache.py +184 -0
- snakemake_executor_plugin_aws_batch_flex/executor.py +725 -0
- snakemake_executor_plugin_aws_batch_flex/headjob.py +244 -0
- snakemake_executor_plugin_aws_batch_flex/hints.py +75 -0
- snakemake_executor_plugin_aws_batch_flex/hostenv.py +221 -0
- snakemake_executor_plugin_aws_batch_flex/imageinfo.py +207 -0
- snakemake_executor_plugin_aws_batch_flex/jobdefs.py +221 -0
- snakemake_executor_plugin_aws_batch_flex/jobfixes.py +65 -0
- snakemake_executor_plugin_aws_batch_flex/jobmonitor.py +146 -0
- snakemake_executor_plugin_aws_batch_flex/naming.py +16 -0
- snakemake_executor_plugin_aws_batch_flex/orphans.py +51 -0
- snakemake_executor_plugin_aws_batch_flex/patches.py +196 -0
- snakemake_executor_plugin_aws_batch_flex/preflight.py +165 -0
- snakemake_executor_plugin_aws_batch_flex/profiles.py +98 -0
- snakemake_executor_plugin_aws_batch_flex/remote.py +409 -0
- snakemake_executor_plugin_aws_batch_flex/settings.py +482 -0
- snakemake_executor_plugin_aws_batch_flex/sizing.py +75 -0
- snakemake_executor_plugin_aws_batch_flex/sources.py +83 -0
- snakemake_executor_plugin_aws_batch_flex/tags.py +76 -0
- snakemake_executor_plugin_aws_batch_flex/transfer.py +193 -0
- snakemake_executor_plugin_aws_batch_flex/usage.py +208 -0
- snakemake_executor_plugin_aws_batch_flex-0.1.0.dist-info/METADATA +343 -0
- snakemake_executor_plugin_aws_batch_flex-0.1.0.dist-info/RECORD +30 -0
- snakemake_executor_plugin_aws_batch_flex-0.1.0.dist-info/WHEEL +4 -0
- snakemake_executor_plugin_aws_batch_flex-0.1.0.dist-info/entry_points.txt +3 -0
- snakemake_executor_plugin_aws_batch_flex-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Reuse conda environments between jobs instead of building them in every job.
|
|
2
|
+
|
|
3
|
+
Runs inside the job (in Snakemake's Python, from the job's launcher). Environments are built
|
|
4
|
+
under one fixed folder, so Snakemake gives the same environment the same name in every job
|
|
5
|
+
(its name is a hash of the definition and the folder). Before building one, a job looks:
|
|
6
|
+
|
|
7
|
+
1. in that folder: on EC2 it's the instance's /var/cache/smkflex-conda, shared by the jobs
|
|
8
|
+
on that machine (a lock makes the others wait while one builds);
|
|
9
|
+
2. in S3, if a cache URL is set: <url>/<platform>/<name>.tar.gz, uploaded by an earlier job.
|
|
10
|
+
|
|
11
|
+
The folder (<platform>) is the CPU type and the image's glibc version, e.g. x86_64-glibc-2.36,
|
|
12
|
+
so environments are only shared between machines and images that conda would have built the
|
|
13
|
+
same packages for: an x86 build doesn't run on Graviton (arm64).
|
|
14
|
+
|
|
15
|
+
Only the standard library and boto3 (which comes with the S3 storage plugin) are used."""
|
|
16
|
+
|
|
17
|
+
import fcntl
|
|
18
|
+
import os
|
|
19
|
+
import platform
|
|
20
|
+
import shutil
|
|
21
|
+
import sys
|
|
22
|
+
import tarfile
|
|
23
|
+
import tempfile
|
|
24
|
+
|
|
25
|
+
ROOT = os.environ.get("SMK_FLEX_CONDA_ROOT", "/tmp/smkflex-conda")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def platform_key():
|
|
29
|
+
try:
|
|
30
|
+
version = os.confstr("CS_GNU_LIBC_VERSION") or ""
|
|
31
|
+
except (ValueError, OSError, AttributeError):
|
|
32
|
+
version = ""
|
|
33
|
+
return f"{platform.machine() or 'unknown'}-{version.replace(' ', '-') or 'unknown'}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def env_prefix(root=None):
|
|
37
|
+
return os.path.join(root or ROOT, platform_key())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _say(msg):
|
|
41
|
+
print(f"[snakemake-batch] {msg}", file=sys.stderr, flush=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _s3(url):
|
|
45
|
+
import boto3
|
|
46
|
+
|
|
47
|
+
bucket, _, key = url[len("s3://"):].partition("/")
|
|
48
|
+
return boto3.client("s3"), bucket, key
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _fetch(url, dest):
|
|
52
|
+
"""Copy the cached archive at url (s3:// or a folder path) to dest; False if there's none."""
|
|
53
|
+
if url.startswith("s3://"):
|
|
54
|
+
s3, bucket, key = _s3(url)
|
|
55
|
+
from botocore.exceptions import ClientError
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
s3.download_file(bucket, key, dest)
|
|
59
|
+
except ClientError as e:
|
|
60
|
+
if e.response.get("Error", {}).get("Code") in ("404", "NoSuchKey"):
|
|
61
|
+
return False
|
|
62
|
+
raise
|
|
63
|
+
return True
|
|
64
|
+
if not os.path.isfile(url):
|
|
65
|
+
return False
|
|
66
|
+
shutil.copyfile(url, dest)
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _store(src, url):
|
|
71
|
+
if url.startswith("s3://"):
|
|
72
|
+
s3, bucket, key = _s3(url)
|
|
73
|
+
s3.upload_file(src, bucket, key)
|
|
74
|
+
else:
|
|
75
|
+
os.makedirs(os.path.dirname(url), exist_ok=True)
|
|
76
|
+
tmp = url + ".part"
|
|
77
|
+
shutil.copyfile(src, tmp)
|
|
78
|
+
os.replace(tmp, url)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def restore(url, prefix, name):
|
|
82
|
+
"""Unpack the cached environment `name` into prefix. True if it's there afterwards."""
|
|
83
|
+
fd, archive = tempfile.mkstemp(dir=prefix, suffix=".tar.gz")
|
|
84
|
+
os.close(fd)
|
|
85
|
+
try:
|
|
86
|
+
if not _fetch(url, archive):
|
|
87
|
+
return False
|
|
88
|
+
with tarfile.open(archive) as t:
|
|
89
|
+
members = t.getmembers()
|
|
90
|
+
if not members or any(not (m.name == name or m.name.startswith(name + "/")
|
|
91
|
+
or m.name == name + ".env_setup_done") for m in members):
|
|
92
|
+
raise ValueError(f"unexpected contents in {url}")
|
|
93
|
+
# the flag file comes last in the archive, so a half-unpacked env has none and
|
|
94
|
+
# Snakemake removes and rebuilds it
|
|
95
|
+
if hasattr(tarfile, "data_filter"):
|
|
96
|
+
# "tar" keeps the env's own permissions and links, unlike the "data" default
|
|
97
|
+
t.extractall(prefix, members=members, filter="tar")
|
|
98
|
+
else:
|
|
99
|
+
t.extractall(prefix, members=members)
|
|
100
|
+
return os.path.exists(os.path.join(prefix, name + ".env_setup_done"))
|
|
101
|
+
finally:
|
|
102
|
+
os.remove(archive)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def save(url, prefix, name):
|
|
106
|
+
fd, archive = tempfile.mkstemp(dir=prefix, suffix=".tar.gz")
|
|
107
|
+
os.close(fd)
|
|
108
|
+
try:
|
|
109
|
+
with tarfile.open(archive, "w:gz", compresslevel=1) as t:
|
|
110
|
+
t.add(os.path.join(prefix, name), arcname=name)
|
|
111
|
+
t.add(os.path.join(prefix, name + ".env_setup_done"), arcname=name + ".env_setup_done")
|
|
112
|
+
_store(archive, url)
|
|
113
|
+
finally:
|
|
114
|
+
os.remove(archive)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def wrap_create(orig, cache_url=None):
|
|
118
|
+
"""Wrap snakemake.deployment.conda.Env.create with the instance folder and the S3 cache."""
|
|
119
|
+
|
|
120
|
+
def create(self, dryrun=False):
|
|
121
|
+
if dryrun or getattr(self, "name", None) or getattr(self, "dir", None) or self.is_containerized:
|
|
122
|
+
return orig(self, dryrun)
|
|
123
|
+
path = self.address
|
|
124
|
+
prefix, name = os.path.dirname(path), os.path.basename(path)
|
|
125
|
+
done = path + ".env_setup_done"
|
|
126
|
+
os.makedirs(prefix, exist_ok=True)
|
|
127
|
+
with open(path + ".smkflex-lock", "w") as lock:
|
|
128
|
+
fcntl.flock(lock, fcntl.LOCK_EX) # another job on this machine may be building it
|
|
129
|
+
if os.path.isdir(path) and os.path.exists(done):
|
|
130
|
+
_say(f"conda env {name} was built by an earlier job on this machine")
|
|
131
|
+
return path
|
|
132
|
+
key = f"{cache_url.rstrip('/')}/{platform_key()}/{name}.tar.gz" if cache_url else None
|
|
133
|
+
if key:
|
|
134
|
+
try:
|
|
135
|
+
if restore(key, prefix, name):
|
|
136
|
+
_say(f"conda env {name} restored from {key}")
|
|
137
|
+
return path
|
|
138
|
+
except Exception as e:
|
|
139
|
+
_say(f"could not restore conda env {name} from the cache ({e!r}); building it")
|
|
140
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
141
|
+
result = orig(self, dryrun)
|
|
142
|
+
if key and os.path.exists(done):
|
|
143
|
+
try:
|
|
144
|
+
save(key, prefix, name)
|
|
145
|
+
_say(f"conda env {name} saved to {key}")
|
|
146
|
+
except Exception as e:
|
|
147
|
+
_say(f"could not save conda env {name} to the cache: {e!r}")
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
create._smkflex = True
|
|
151
|
+
return create
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def install(argv, setting):
|
|
155
|
+
"""Turn the cache on for this job: set Snakemake's conda prefix (unless the job has one)
|
|
156
|
+
and wrap Env.create. setting is 'instance' or an S3 URL; anything going wrong leaves
|
|
157
|
+
the job as it was."""
|
|
158
|
+
# --deploy-sources only unpacks the workflow before the job's real run: no envs there
|
|
159
|
+
if "--conda-prefix" in argv or "--deploy-sources" in argv:
|
|
160
|
+
return argv
|
|
161
|
+
try:
|
|
162
|
+
prefix = env_prefix()
|
|
163
|
+
os.makedirs(prefix, exist_ok=True)
|
|
164
|
+
# can this job write there? (a unique name: jobs in other containers on the same
|
|
165
|
+
# machine can have the same process ID)
|
|
166
|
+
fd, probe = tempfile.mkstemp(dir=prefix, prefix=".write-test-")
|
|
167
|
+
os.close(fd)
|
|
168
|
+
os.remove(probe)
|
|
169
|
+
import inspect
|
|
170
|
+
|
|
171
|
+
from snakemake.deployment import conda as smk_conda
|
|
172
|
+
|
|
173
|
+
env = smk_conda.Env
|
|
174
|
+
# what wrap_create was written for
|
|
175
|
+
if (list(inspect.signature(env.create).parameters)[:2] != ["self", "dryrun"]
|
|
176
|
+
or not all(hasattr(env, a) for a in ("address", "is_containerized"))):
|
|
177
|
+
raise RuntimeError("this Snakemake's conda Env changed")
|
|
178
|
+
if not getattr(smk_conda.Env.create, "_smkflex", False):
|
|
179
|
+
url = setting if setting.startswith("s3://") or setting.startswith("/") else None
|
|
180
|
+
smk_conda.Env.create = wrap_create(smk_conda.Env.create, url)
|
|
181
|
+
except Exception as e:
|
|
182
|
+
_say(f"conda env cache off for this job: {e!r}")
|
|
183
|
+
return argv
|
|
184
|
+
return argv + ["--conda-prefix", prefix]
|