compose-runner 0.6.3rc1__tar.gz → 0.6.4rc1__tar.gz
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.
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/Dockerfile +7 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/PKG-INFO +2 -2
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/README.md +1 -1
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/_version.py +2 -2
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/aws_lambda/run_handler.py +71 -1
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/ecs_task.py +3 -0
- compose_runner-0.6.4rc1/compose_runner/tests/cassettes/test_lambda_handlers/test_select_task_size_uses_large_for_montecarlo.yaml +60 -0
- compose_runner-0.6.4rc1/compose_runner/tests/cassettes/test_lambda_handlers/test_select_task_size_uses_standard_for_fdr.yaml +55 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/tests/test_lambda_handlers.py +51 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/infra/cdk/stacks/compose_runner_stack.py +96 -38
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/.gitignore +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/LICENSE +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/aws_lambda/.dockerignore +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/aws_lambda/Dockerfile +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/__init__.py +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/aws_lambda/__init__.py +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/aws_lambda/common.py +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/aws_lambda/log_poll_handler.py +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/aws_lambda/results_handler.py +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/aws_lambda/status_handler.py +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/cli.py +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/run.py +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/sentry.py +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/tests/cassettes/test_run/test_download_bundle.yaml +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/tests/cassettes/test_run/test_run_database_workflow.yaml +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/tests/cassettes/test_run/test_run_group_comparison_workflow.yaml +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/tests/cassettes/test_run/test_run_string_group_comparison_workflow.yaml +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/tests/cassettes/test_run/test_run_workflow.yaml +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/tests/conftest.py +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/tests/test_cli.py +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/tests/test_ecs_task.py +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/tests/test_run.py +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/infra/cdk/app.py +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/infra/cdk/cdk.json +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/infra/cdk/requirements.txt +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/infra/cdk/stacks/__init__.py +0 -0
- {compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/pyproject.toml +0 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
FROM python:3.13-slim
|
|
2
2
|
|
|
3
|
+
ARG COMPOSE_RUNNER_VERSION
|
|
4
|
+
ENV COMPOSE_RUNNER_VERSION=${COMPOSE_RUNNER_VERSION}
|
|
5
|
+
LABEL org.opencontainers.image.title="compose-runner ecs task"
|
|
6
|
+
LABEL org.opencontainers.image.version=${COMPOSE_RUNNER_VERSION}
|
|
7
|
+
|
|
8
|
+
RUN test -n "$COMPOSE_RUNNER_VERSION" || (echo "COMPOSE_RUNNER_VERSION build arg is required" && exit 1)
|
|
9
|
+
|
|
3
10
|
RUN apt-get update && apt-get install -y \
|
|
4
11
|
git \
|
|
5
12
|
&& rm -rf /var/lib/apt/lists/*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compose-runner
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4rc1
|
|
4
4
|
Summary: A package for running neurosynth-compose analyses
|
|
5
5
|
Project-URL: Repository, https://github.com/neurostuff/compose-runner
|
|
6
6
|
Author-email: James Kent <jamesdkent21@gmail.com>
|
|
@@ -67,7 +67,7 @@ The deployed architecture works like this:
|
|
|
67
67
|
Pass `-c resultsBucketName=<bucket>` to use an existing S3 bucket, or omit it
|
|
68
68
|
to let the stack create and retain a dedicated bucket. Additional knobs:
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
- `-c stateMachineTimeoutSeconds=32400` to control the max wall clock per run
|
|
71
71
|
- `-c submitTimeoutSeconds` / `-c statusTimeoutSeconds` / `-c pollTimeoutSeconds`
|
|
72
72
|
to tune Lambda timeouts
|
|
73
73
|
- `-c taskEphemeralStorageGiB` if the default 21 GiB scratch volume is insufficient
|
|
@@ -44,7 +44,7 @@ The deployed architecture works like this:
|
|
|
44
44
|
Pass `-c resultsBucketName=<bucket>` to use an existing S3 bucket, or omit it
|
|
45
45
|
to let the stack create and retain a dedicated bucket. Additional knobs:
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
- `-c stateMachineTimeoutSeconds=32400` to control the max wall clock per run
|
|
48
48
|
- `-c submitTimeoutSeconds` / `-c statusTimeoutSeconds` / `-c pollTimeoutSeconds`
|
|
49
49
|
to tune Lambda timeouts
|
|
50
50
|
- `-c taskEphemeralStorageGiB` if the default 21 GiB scratch volume is insufficient
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.6.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 6,
|
|
31
|
+
__version__ = version = '0.6.4rc1'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 6, 4, 'rc1')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
{compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/aws_lambda/run_handler.py
RENAMED
|
@@ -4,6 +4,8 @@ import json
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import uuid
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
7
9
|
from typing import Any, Dict, Optional
|
|
8
10
|
|
|
9
11
|
import boto3
|
|
@@ -22,6 +24,8 @@ RESULTS_PREFIX_ENV = "RESULTS_PREFIX"
|
|
|
22
24
|
NSC_KEY_ENV = "NSC_KEY"
|
|
23
25
|
NV_KEY_ENV = "NV_KEY"
|
|
24
26
|
|
|
27
|
+
DEFAULT_TASK_SIZE = "standard"
|
|
28
|
+
|
|
25
29
|
|
|
26
30
|
def _log(job_id: str, message: str, **details: Any) -> None:
|
|
27
31
|
payload = {"job_id": job_id, "message": message, **details}
|
|
@@ -29,6 +33,67 @@ def _log(job_id: str, message: str, **details: Any) -> None:
|
|
|
29
33
|
logger.info(json.dumps(payload))
|
|
30
34
|
|
|
31
35
|
|
|
36
|
+
def _compose_api_base_url(environment: str) -> str:
|
|
37
|
+
env = (environment or "production").lower()
|
|
38
|
+
if env == "staging":
|
|
39
|
+
return "https://synth.neurostore.xyz/api"
|
|
40
|
+
if env == "local":
|
|
41
|
+
return "http://localhost:81/api"
|
|
42
|
+
return "https://compose.neurosynth.org/api"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _fetch_meta_analysis(meta_analysis_id: str, environment: str) -> Optional[Dict[str, Any]]:
|
|
46
|
+
base_url = _compose_api_base_url(environment).rstrip("/")
|
|
47
|
+
url = f"{base_url}/meta-analyses/{meta_analysis_id}?nested=true"
|
|
48
|
+
request = urllib.request.Request(url, headers={"User-Agent": "compose-runner/submit"})
|
|
49
|
+
try:
|
|
50
|
+
with urllib.request.urlopen(request, timeout=10) as response:
|
|
51
|
+
return json.load(response)
|
|
52
|
+
except (urllib.error.URLError, urllib.error.HTTPError, json.JSONDecodeError) as exc:
|
|
53
|
+
logger.warning("Failed to fetch meta-analysis %s: %s", meta_analysis_id, exc)
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _requires_large_task(specification: Dict[str, Any]) -> bool:
|
|
58
|
+
if not isinstance(specification, dict):
|
|
59
|
+
return False
|
|
60
|
+
corrector = specification.get("corrector")
|
|
61
|
+
if not isinstance(corrector, dict):
|
|
62
|
+
return False
|
|
63
|
+
if corrector.get("type") != "FWECorrector":
|
|
64
|
+
return False
|
|
65
|
+
args = corrector.get("args")
|
|
66
|
+
if not isinstance(args, dict):
|
|
67
|
+
return False
|
|
68
|
+
method = args.get("method")
|
|
69
|
+
if method is None:
|
|
70
|
+
kwargs = args.get("**kwargs")
|
|
71
|
+
if isinstance(kwargs, dict):
|
|
72
|
+
method = kwargs.get("method")
|
|
73
|
+
if isinstance(method, str) and method.lower() == "montecarlo":
|
|
74
|
+
return True
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _select_task_size(meta_analysis_id: str, environment: str, artifact_prefix: str) -> str:
|
|
79
|
+
doc = _fetch_meta_analysis(meta_analysis_id, environment)
|
|
80
|
+
if not doc:
|
|
81
|
+
return DEFAULT_TASK_SIZE
|
|
82
|
+
specification = doc.get("specification")
|
|
83
|
+
try:
|
|
84
|
+
if _requires_large_task(specification):
|
|
85
|
+
_log(
|
|
86
|
+
artifact_prefix,
|
|
87
|
+
"workflow.task_size_selected",
|
|
88
|
+
task_size="large",
|
|
89
|
+
reason="montecarlo_fwe",
|
|
90
|
+
)
|
|
91
|
+
return "large"
|
|
92
|
+
except Exception as exc: # noqa: broad-except
|
|
93
|
+
logger.warning("Failed to evaluate specification for %s: %s", meta_analysis_id, exc)
|
|
94
|
+
return DEFAULT_TASK_SIZE
|
|
95
|
+
|
|
96
|
+
|
|
32
97
|
def _job_input(
|
|
33
98
|
payload: Dict[str, Any],
|
|
34
99
|
artifact_prefix: str,
|
|
@@ -36,6 +101,7 @@ def _job_input(
|
|
|
36
101
|
prefix: Optional[str],
|
|
37
102
|
nsc_key: Optional[str],
|
|
38
103
|
nv_key: Optional[str],
|
|
104
|
+
task_size: str,
|
|
39
105
|
) -> Dict[str, Any]:
|
|
40
106
|
no_upload_flag = bool(payload.get("no_upload", False))
|
|
41
107
|
doc: Dict[str, Any] = {
|
|
@@ -44,6 +110,7 @@ def _job_input(
|
|
|
44
110
|
"environment": payload.get("environment", "production"),
|
|
45
111
|
"no_upload": "true" if no_upload_flag else "false",
|
|
46
112
|
"results": {"bucket": bucket or "", "prefix": prefix or ""},
|
|
113
|
+
"task_size": task_size,
|
|
47
114
|
}
|
|
48
115
|
n_cores = payload.get("n_cores")
|
|
49
116
|
doc["n_cores"] = str(n_cores) if n_cores is not None else ""
|
|
@@ -76,7 +143,10 @@ def handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
|
|
|
76
143
|
nsc_key = payload.get("nsc_key") or os.environ.get(NSC_KEY_ENV)
|
|
77
144
|
nv_key = payload.get("nv_key") or os.environ.get(NV_KEY_ENV)
|
|
78
145
|
|
|
79
|
-
|
|
146
|
+
environment = payload.get("environment", "production")
|
|
147
|
+
task_size = _select_task_size(payload["meta_analysis_id"], environment, artifact_prefix)
|
|
148
|
+
|
|
149
|
+
job_input = _job_input(payload, artifact_prefix, bucket, prefix, nsc_key, nv_key, task_size)
|
|
80
150
|
params = {
|
|
81
151
|
"stateMachineArn": os.environ[STATE_MACHINE_ARN_ENV],
|
|
82
152
|
"name": artifact_prefix,
|
|
@@ -93,6 +93,7 @@ def main() -> None:
|
|
|
93
93
|
nv_key = os.environ.get(NV_KEY_ENV) or None
|
|
94
94
|
no_upload = _bool_from_env(os.environ.get(NO_UPLOAD_ENV))
|
|
95
95
|
n_cores = _resolve_n_cores(os.environ.get(N_CORES_ENV))
|
|
96
|
+
compose_runner_version = os.environ.get("COMPOSE_RUNNER_VERSION", "unknown")
|
|
96
97
|
|
|
97
98
|
bucket = os.environ.get(RESULTS_BUCKET_ENV)
|
|
98
99
|
prefix = os.environ.get(RESULTS_PREFIX_ENV)
|
|
@@ -106,6 +107,7 @@ def main() -> None:
|
|
|
106
107
|
meta_analysis_id=meta_analysis_id,
|
|
107
108
|
environment=environment,
|
|
108
109
|
no_upload=no_upload,
|
|
110
|
+
compose_runner_version=compose_runner_version,
|
|
109
111
|
)
|
|
110
112
|
try:
|
|
111
113
|
url, _ = run_compose(
|
|
@@ -125,6 +127,7 @@ def main() -> None:
|
|
|
125
127
|
"result_url": url,
|
|
126
128
|
"artifacts_bucket": bucket,
|
|
127
129
|
"artifacts_prefix": prefix,
|
|
130
|
+
"compose_runner_version": compose_runner_version,
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
if bucket:
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
interactions:
|
|
2
|
+
- request:
|
|
3
|
+
method: GET
|
|
4
|
+
uri: https://synth.neurostore.xyz/api/meta-analyses/ZPSvyvhZAopz?nested=true
|
|
5
|
+
body: null
|
|
6
|
+
headers:
|
|
7
|
+
Accept:
|
|
8
|
+
- '*/*'
|
|
9
|
+
Accept-Encoding:
|
|
10
|
+
- gzip, deflate
|
|
11
|
+
Connection:
|
|
12
|
+
- keep-alive
|
|
13
|
+
User-Agent:
|
|
14
|
+
- python-requests/2.32.4
|
|
15
|
+
response:
|
|
16
|
+
status:
|
|
17
|
+
code: 200
|
|
18
|
+
message: OK
|
|
19
|
+
headers:
|
|
20
|
+
Server:
|
|
21
|
+
- nginx/1.21.6
|
|
22
|
+
Date:
|
|
23
|
+
- Tue, 21 Oct 2025 14:08:45 GMT
|
|
24
|
+
Content-Type:
|
|
25
|
+
- application/json
|
|
26
|
+
Transfer-Encoding:
|
|
27
|
+
- chunked
|
|
28
|
+
Connection:
|
|
29
|
+
- keep-alive
|
|
30
|
+
Vary:
|
|
31
|
+
- Accept-Encoding
|
|
32
|
+
Content-Encoding:
|
|
33
|
+
- gzip
|
|
34
|
+
Strict-Transport-Security:
|
|
35
|
+
- max-age=31536000
|
|
36
|
+
body:
|
|
37
|
+
string: '{"id": "ZPSvyvhZAopz", "created_at": "2025-10-21T04:57:40.236536+00:00",
|
|
38
|
+
"updated_at": null, "user": "github|12564882", "username": "James Kent", "name":
|
|
39
|
+
"Untitled MKDADensity Meta Analysis: included", "description": "MKDADensity
|
|
40
|
+
meta analysis with FWECorrector", "provenance": null, "specification": {"id":
|
|
41
|
+
"zQdMa4uAaYYU", "created_at": "2025-10-21T04:57:39.888528+00:00", "updated_at":
|
|
42
|
+
null, "user": "github|12564882", "username": "James Kent", "type": "CBMA",
|
|
43
|
+
"estimator": {"type": "MKDADensity", "args": {"null_method": "approximate",
|
|
44
|
+
"n_iters": 5000, "**kwargs": {}, "kernel__r": 10, "kernel__value": 1}}, "database_studyset":
|
|
45
|
+
null, "filter": "included", "corrector": {"type": "FWECorrector", "args":
|
|
46
|
+
{"voxel_thresh": 0.001, "n_iters": 5000, "vfwe_only": false, "method": "montecarlo"}},
|
|
47
|
+
"conditions": [true], "weights": [1.0]}, "neurostore_analysis": {"id": "8S5xRedCGRkz",
|
|
48
|
+
"created_at": "2025-10-21T04:57:40.255480+00:00", "updated_at": null, "neurostore_id":
|
|
49
|
+
null, "exception": null, "traceback": null, "status": "PENDING"}, "studyset":
|
|
50
|
+
{"id": "9jPvdkuRufUP", "created_at": "2025-10-21T04:57:40.008456+00:00", "updated_at":
|
|
51
|
+
null, "user": "github|12564882", "username": "James Kent", "snapshot": null,
|
|
52
|
+
"neurostore_id": "3EmvH2LELwR2", "version": null, "url": "https://neurostore.org/api/studysets/3EmvH2LELwR2"},
|
|
53
|
+
"annotation": {"id": "YVLt6DRFKdd5", "created_at": "2025-10-21T04:57:40.121637+00:00",
|
|
54
|
+
"updated_at": null, "user": "github|12564882", "username": "James Kent", "snapshot":
|
|
55
|
+
null, "neurostore_id": "TebrRstj8ofh", "studyset": "3EmvH2LELwR2", "url":
|
|
56
|
+
"https://neurostore.org/api/annotations/TebrRstj8ofh"}, "project": "D2cTfoxNfpLy",
|
|
57
|
+
"cached_studyset": "9jPvdkuRufUP", "cached_annotation": "YVLt6DRFKdd5", "run_key":
|
|
58
|
+
"PDeDnh_8MXc88xoVJySz3w", "results": [], "neurostore_url": null}'
|
|
59
|
+
http_version: HTTP/1.1
|
|
60
|
+
version: 1
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
interactions:
|
|
2
|
+
- request:
|
|
3
|
+
body: null
|
|
4
|
+
headers:
|
|
5
|
+
Connection:
|
|
6
|
+
- close
|
|
7
|
+
Host:
|
|
8
|
+
- synth.neurostore.xyz
|
|
9
|
+
User-Agent:
|
|
10
|
+
- compose-runner/submit
|
|
11
|
+
method: GET
|
|
12
|
+
uri: https://synth.neurostore.xyz/api/meta-analyses/VtFZJFniCKvG?nested=true
|
|
13
|
+
response:
|
|
14
|
+
body:
|
|
15
|
+
string: '{"id": "VtFZJFniCKvG", "created_at": "2025-10-21T14:10:35.309383+00:00",
|
|
16
|
+
"updated_at": null, "user": "github|12564882", "username": "James Kent", "name":
|
|
17
|
+
"Untitled MKDADensity Meta Analysis: included (1)", "description": "MKDADensity
|
|
18
|
+
meta analysis with FDRCorrector", "provenance": null, "specification": {"id":
|
|
19
|
+
"DtVzKEKGaXLu", "created_at": "2025-10-21T14:10:34.564365+00:00", "updated_at":
|
|
20
|
+
null, "user": "github|12564882", "username": "James Kent", "type": "CBMA",
|
|
21
|
+
"estimator": {"type": "MKDADensity", "args": {"null_method": "approximate",
|
|
22
|
+
"n_iters": 5000, "**kwargs": {}, "kernel__r": 10, "kernel__value": 1}}, "database_studyset":
|
|
23
|
+
null, "filter": "included", "corrector": {"type": "FDRCorrector", "args":
|
|
24
|
+
{"method": "indep", "alpha": 0.05}}, "conditions": [true], "weights": [1.0]},
|
|
25
|
+
"neurostore_analysis": {"id": "564c8kRnJVT4", "created_at": "2025-10-21T14:10:35.325173+00:00",
|
|
26
|
+
"updated_at": null, "neurostore_id": null, "exception": null, "traceback":
|
|
27
|
+
null, "status": "PENDING"}, "studyset": {"id": "FA3BDBdGRZ5d", "created_at":
|
|
28
|
+
"2025-10-21T14:10:34.821625+00:00", "updated_at": null, "user": "github|12564882",
|
|
29
|
+
"username": "James Kent", "snapshot": null, "neurostore_id": "3EmvH2LELwR2",
|
|
30
|
+
"version": null, "url": "https://neurostore.org/api/studysets/3EmvH2LELwR2"},
|
|
31
|
+
"annotation": {"id": "XELVYV7ftp7e", "created_at": "2025-10-21T14:10:35.183354+00:00",
|
|
32
|
+
"updated_at": null, "user": "github|12564882", "username": "James Kent", "snapshot":
|
|
33
|
+
null, "neurostore_id": "TebrRstj8ofh", "studyset": "3EmvH2LELwR2", "url":
|
|
34
|
+
"https://neurostore.org/api/annotations/TebrRstj8ofh"}, "project": "D2cTfoxNfpLy",
|
|
35
|
+
"cached_studyset": "FA3BDBdGRZ5d", "cached_annotation": "XELVYV7ftp7e", "run_key":
|
|
36
|
+
"V_jTcP2zfNlWD4KhwKKcJw", "results": [], "neurostore_url": null}'
|
|
37
|
+
headers:
|
|
38
|
+
Connection:
|
|
39
|
+
- close
|
|
40
|
+
Content-Length:
|
|
41
|
+
- '1750'
|
|
42
|
+
Content-Type:
|
|
43
|
+
- application/json
|
|
44
|
+
Date:
|
|
45
|
+
- Tue, 21 Oct 2025 14:14:50 GMT
|
|
46
|
+
Server:
|
|
47
|
+
- nginx/1.21.6
|
|
48
|
+
Strict-Transport-Security:
|
|
49
|
+
- max-age=31536000
|
|
50
|
+
Vary:
|
|
51
|
+
- Accept-Encoding
|
|
52
|
+
status:
|
|
53
|
+
code: 200
|
|
54
|
+
message: OK
|
|
55
|
+
version: 1
|
{compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/tests/test_lambda_handlers.py
RENAMED
|
@@ -4,6 +4,8 @@ import json
|
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
5
|
from typing import Any, Dict
|
|
6
6
|
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
7
9
|
from compose_runner.aws_lambda import log_poll_handler, results_handler, run_handler, status_handler
|
|
8
10
|
|
|
9
11
|
|
|
@@ -23,6 +25,28 @@ def _make_http_event(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
|
|
28
|
+
def test_requires_large_task_detection():
|
|
29
|
+
spec = {"corrector": {"type": "FWECorrector", "args": {"method": "montecarlo"}}}
|
|
30
|
+
assert run_handler._requires_large_task(spec)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_requires_large_task_false_when_method_differs():
|
|
34
|
+
spec = {"corrector": {"type": "FWECorrector", "args": {"method": "bonferroni"}}}
|
|
35
|
+
assert run_handler._requires_large_task(spec) is False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.vcr(record_mode="once")
|
|
39
|
+
def test_select_task_size_uses_large_for_montecarlo():
|
|
40
|
+
task_size = run_handler._select_task_size("ZPSvyvhZAopz", "staging", "artifact-test")
|
|
41
|
+
assert task_size == "large"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.mark.vcr(record_mode="once")
|
|
45
|
+
def test_select_task_size_uses_standard_for_fdr():
|
|
46
|
+
task_size = run_handler._select_task_size("VtFZJFniCKvG", "staging", "artifact-test")
|
|
47
|
+
assert task_size == "standard"
|
|
48
|
+
|
|
49
|
+
|
|
26
50
|
def test_run_handler_http_success(monkeypatch, tmp_path):
|
|
27
51
|
captured = {}
|
|
28
52
|
|
|
@@ -36,6 +60,7 @@ def test_run_handler_http_success(monkeypatch, tmp_path):
|
|
|
36
60
|
...
|
|
37
61
|
|
|
38
62
|
monkeypatch.setattr(run_handler, "_SFN_CLIENT", FakeSFN())
|
|
63
|
+
monkeypatch.setattr(run_handler, "_select_task_size", lambda *args: "standard")
|
|
39
64
|
monkeypatch.setenv("STATE_MACHINE_ARN", "arn:aws:states:state-machine")
|
|
40
65
|
monkeypatch.setenv("RESULTS_BUCKET", "bucket")
|
|
41
66
|
monkeypatch.setenv("RESULTS_PREFIX", "prefix")
|
|
@@ -63,6 +88,32 @@ def test_run_handler_http_success(monkeypatch, tmp_path):
|
|
|
63
88
|
assert input_doc["results"]["prefix"] == "prefix"
|
|
64
89
|
assert input_doc["nsc_key"] == "nsc"
|
|
65
90
|
assert input_doc["nv_key"] == "nv"
|
|
91
|
+
assert input_doc["task_size"] == "standard"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_run_handler_http_uses_large_task(monkeypatch):
|
|
95
|
+
captured = {}
|
|
96
|
+
|
|
97
|
+
class FakeSFN:
|
|
98
|
+
def start_execution(self, **kwargs):
|
|
99
|
+
captured.update(kwargs)
|
|
100
|
+
return {"executionArn": "arn:aws:states:us-east-1:123:execution:state-machine:run-456"}
|
|
101
|
+
|
|
102
|
+
class exceptions:
|
|
103
|
+
class ExecutionAlreadyExists(Exception):
|
|
104
|
+
...
|
|
105
|
+
|
|
106
|
+
monkeypatch.setattr(run_handler, "_SFN_CLIENT", FakeSFN())
|
|
107
|
+
monkeypatch.setattr(run_handler, "_select_task_size", lambda *args: "large")
|
|
108
|
+
monkeypatch.setenv("STATE_MACHINE_ARN", "arn:aws:states:state-machine")
|
|
109
|
+
monkeypatch.setenv("RESULTS_BUCKET", "bucket")
|
|
110
|
+
monkeypatch.setenv("RESULTS_PREFIX", "prefix")
|
|
111
|
+
|
|
112
|
+
event = _make_http_event({"meta_analysis_id": "abc123"})
|
|
113
|
+
response = run_handler.handler(event, DummyContext())
|
|
114
|
+
assert response["statusCode"] == 202
|
|
115
|
+
input_doc = json.loads(captured["input"])
|
|
116
|
+
assert input_doc["task_size"] == "large"
|
|
66
117
|
|
|
67
118
|
|
|
68
119
|
def test_run_handler_missing_meta_analysis(monkeypatch):
|
{compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/infra/cdk/stacks/compose_runner_stack.py
RENAMED
|
@@ -40,7 +40,12 @@ class ComposeRunnerStack(Stack):
|
|
|
40
40
|
task_cpu = int(self.node.try_get_context("taskCpu") or 4096)
|
|
41
41
|
task_memory_mib = int(self.node.try_get_context("taskMemoryMiB") or 30720)
|
|
42
42
|
task_ephemeral_storage_gib = int(self.node.try_get_context("taskEphemeralStorageGiB") or 21)
|
|
43
|
-
|
|
43
|
+
task_cpu_large = int(self.node.try_get_context("taskCpuLarge") or 16384)
|
|
44
|
+
task_memory_large_mib = int(self.node.try_get_context("taskMemoryLargeMiB") or 65536)
|
|
45
|
+
state_machine_timeout_seconds = int(self.node.try_get_context("stateMachineTimeoutSeconds") or 32400)
|
|
46
|
+
|
|
47
|
+
if task_cpu_large >= 16384 and task_memory_large_mib < 32768:
|
|
48
|
+
raise ValueError("taskMemoryLargeMiB must be at least 32768 MiB for 16 vCPU tasks.")
|
|
44
49
|
|
|
45
50
|
project_root = Path(__file__).resolve().parents[3]
|
|
46
51
|
project_version = self.node.try_get_context("composeRunnerVersion")
|
|
@@ -121,6 +126,20 @@ class ComposeRunnerStack(Stack):
|
|
|
121
126
|
ephemeral_storage_gib=task_ephemeral_storage_gib,
|
|
122
127
|
)
|
|
123
128
|
|
|
129
|
+
task_definition_large = ecs.FargateTaskDefinition(
|
|
130
|
+
self,
|
|
131
|
+
"ComposeRunnerLargeTaskDefinition",
|
|
132
|
+
cpu=task_cpu_large,
|
|
133
|
+
memory_limit_mib=task_memory_large_mib,
|
|
134
|
+
ephemeral_storage_gib=task_ephemeral_storage_gib,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
container_environment = {
|
|
138
|
+
"RESULTS_BUCKET": results_bucket.bucket_name,
|
|
139
|
+
"RESULTS_PREFIX": results_prefix,
|
|
140
|
+
"DELETE_TMP": "true",
|
|
141
|
+
}
|
|
142
|
+
|
|
124
143
|
container = task_definition.add_container(
|
|
125
144
|
"ComposeRunnerContainer",
|
|
126
145
|
image=ecs.ContainerImage.from_docker_image_asset(fargate_asset),
|
|
@@ -129,16 +148,46 @@ class ComposeRunnerStack(Stack):
|
|
|
129
148
|
log_group=task_log_group,
|
|
130
149
|
stream_prefix="compose-runner",
|
|
131
150
|
),
|
|
132
|
-
environment=
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
151
|
+
environment=container_environment,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
container_large = task_definition_large.add_container(
|
|
155
|
+
"ComposeRunnerLargeContainer",
|
|
156
|
+
image=ecs.ContainerImage.from_docker_image_asset(fargate_asset),
|
|
157
|
+
entry_point=["python", "-m", "compose_runner.ecs_task"],
|
|
158
|
+
logging=ecs.LogDriver.aws_logs(
|
|
159
|
+
log_group=task_log_group,
|
|
160
|
+
stream_prefix="compose-runner",
|
|
161
|
+
),
|
|
162
|
+
environment=container_environment,
|
|
137
163
|
)
|
|
138
164
|
|
|
139
165
|
results_bucket.grant_read_write(task_definition.task_role)
|
|
166
|
+
results_bucket.grant_read_write(task_definition_large.task_role)
|
|
167
|
+
|
|
168
|
+
container_env_overrides = [
|
|
169
|
+
tasks.TaskEnvironmentVariable(
|
|
170
|
+
name="ARTIFACT_PREFIX", value=sfn.JsonPath.string_at("$.artifact_prefix")
|
|
171
|
+
),
|
|
172
|
+
tasks.TaskEnvironmentVariable(
|
|
173
|
+
name="META_ANALYSIS_ID", value=sfn.JsonPath.string_at("$.meta_analysis_id")
|
|
174
|
+
),
|
|
175
|
+
tasks.TaskEnvironmentVariable(
|
|
176
|
+
name="ENVIRONMENT", value=sfn.JsonPath.string_at("$.environment")
|
|
177
|
+
),
|
|
178
|
+
tasks.TaskEnvironmentVariable(name="NSC_KEY", value=sfn.JsonPath.string_at("$.nsc_key")),
|
|
179
|
+
tasks.TaskEnvironmentVariable(name="NV_KEY", value=sfn.JsonPath.string_at("$.nv_key")),
|
|
180
|
+
tasks.TaskEnvironmentVariable(name="NO_UPLOAD", value=sfn.JsonPath.string_at("$.no_upload")),
|
|
181
|
+
tasks.TaskEnvironmentVariable(name="N_CORES", value=sfn.JsonPath.string_at("$.n_cores")),
|
|
182
|
+
tasks.TaskEnvironmentVariable(
|
|
183
|
+
name="RESULTS_BUCKET", value=sfn.JsonPath.string_at("$.results.bucket")
|
|
184
|
+
),
|
|
185
|
+
tasks.TaskEnvironmentVariable(
|
|
186
|
+
name="RESULTS_PREFIX", value=sfn.JsonPath.string_at("$.results.prefix")
|
|
187
|
+
),
|
|
188
|
+
]
|
|
140
189
|
|
|
141
|
-
|
|
190
|
+
run_task_standard = tasks.EcsRunTask(
|
|
142
191
|
self,
|
|
143
192
|
"RunFargateJob",
|
|
144
193
|
integration_pattern=sfn.IntegrationPattern.RUN_JOB,
|
|
@@ -153,41 +202,41 @@ class ComposeRunnerStack(Stack):
|
|
|
153
202
|
container_overrides=[
|
|
154
203
|
tasks.ContainerOverride(
|
|
155
204
|
container_definition=container,
|
|
156
|
-
environment=
|
|
157
|
-
tasks.TaskEnvironmentVariable(
|
|
158
|
-
name="ARTIFACT_PREFIX", value=sfn.JsonPath.string_at("$.artifact_prefix")
|
|
159
|
-
),
|
|
160
|
-
tasks.TaskEnvironmentVariable(
|
|
161
|
-
name="META_ANALYSIS_ID", value=sfn.JsonPath.string_at("$.meta_analysis_id")
|
|
162
|
-
),
|
|
163
|
-
tasks.TaskEnvironmentVariable(
|
|
164
|
-
name="ENVIRONMENT", value=sfn.JsonPath.string_at("$.environment")
|
|
165
|
-
),
|
|
166
|
-
tasks.TaskEnvironmentVariable(
|
|
167
|
-
name="NSC_KEY", value=sfn.JsonPath.string_at("$.nsc_key")
|
|
168
|
-
),
|
|
169
|
-
tasks.TaskEnvironmentVariable(
|
|
170
|
-
name="NV_KEY", value=sfn.JsonPath.string_at("$.nv_key")
|
|
171
|
-
),
|
|
172
|
-
tasks.TaskEnvironmentVariable(
|
|
173
|
-
name="NO_UPLOAD", value=sfn.JsonPath.string_at("$.no_upload")
|
|
174
|
-
),
|
|
175
|
-
tasks.TaskEnvironmentVariable(
|
|
176
|
-
name="N_CORES", value=sfn.JsonPath.string_at("$.n_cores")
|
|
177
|
-
),
|
|
178
|
-
tasks.TaskEnvironmentVariable(
|
|
179
|
-
name="RESULTS_BUCKET", value=sfn.JsonPath.string_at("$.results.bucket")
|
|
180
|
-
),
|
|
181
|
-
tasks.TaskEnvironmentVariable(
|
|
182
|
-
name="RESULTS_PREFIX", value=sfn.JsonPath.string_at("$.results.prefix")
|
|
183
|
-
),
|
|
184
|
-
],
|
|
205
|
+
environment=container_env_overrides,
|
|
185
206
|
)
|
|
186
207
|
],
|
|
187
208
|
result_path="$.ecs",
|
|
188
209
|
)
|
|
189
210
|
|
|
190
|
-
|
|
211
|
+
run_task_large = tasks.EcsRunTask(
|
|
212
|
+
self,
|
|
213
|
+
"RunFargateJobLarge",
|
|
214
|
+
integration_pattern=sfn.IntegrationPattern.RUN_JOB,
|
|
215
|
+
cluster=cluster,
|
|
216
|
+
task_definition=task_definition_large,
|
|
217
|
+
launch_target=tasks.EcsFargateLaunchTarget(
|
|
218
|
+
platform_version=ecs.FargatePlatformVersion.LATEST
|
|
219
|
+
),
|
|
220
|
+
assign_public_ip=True,
|
|
221
|
+
security_groups=[task_security_group],
|
|
222
|
+
subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC),
|
|
223
|
+
container_overrides=[
|
|
224
|
+
tasks.ContainerOverride(
|
|
225
|
+
container_definition=container_large,
|
|
226
|
+
environment=container_env_overrides,
|
|
227
|
+
)
|
|
228
|
+
],
|
|
229
|
+
result_path="$.ecs",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
run_task_standard.add_retry(
|
|
233
|
+
errors=["States.ALL"],
|
|
234
|
+
interval=Duration.seconds(30),
|
|
235
|
+
backoff_rate=2.0,
|
|
236
|
+
max_attempts=2,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
run_task_large.add_retry(
|
|
191
240
|
errors=["States.ALL"],
|
|
192
241
|
interval=Duration.seconds(30),
|
|
193
242
|
backoff_rate=2.0,
|
|
@@ -202,11 +251,20 @@ class ComposeRunnerStack(Stack):
|
|
|
202
251
|
"meta_analysis_id.$": "$.meta_analysis_id",
|
|
203
252
|
"environment.$": "$.environment",
|
|
204
253
|
"results.$": "$.results",
|
|
254
|
+
"task_size.$": "$.task_size",
|
|
205
255
|
"ecs.$": "$.ecs",
|
|
206
256
|
},
|
|
207
257
|
)
|
|
208
258
|
|
|
209
|
-
definition_chain =
|
|
259
|
+
definition_chain = sfn.Choice(
|
|
260
|
+
self,
|
|
261
|
+
"SelectFargateTask",
|
|
262
|
+
).when(
|
|
263
|
+
sfn.Condition.string_equals("$.task_size", "large"),
|
|
264
|
+
run_task_large.next(run_output),
|
|
265
|
+
).otherwise(
|
|
266
|
+
run_task_standard.next(run_output)
|
|
267
|
+
)
|
|
210
268
|
|
|
211
269
|
state_machine = sfn.StateMachine(
|
|
212
270
|
self,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/aws_lambda/log_poll_handler.py
RENAMED
|
File without changes
|
{compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/aws_lambda/results_handler.py
RENAMED
|
File without changes
|
{compose_runner-0.6.3rc1 → compose_runner-0.6.4rc1}/compose_runner/aws_lambda/status_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|