Skip to content

Commit 3072a31

Browse files
committed
Support parameterized job_id in dependencies
1 parent a6f3cff commit 3072a31

3 files changed

Lines changed: 137 additions & 3 deletions

File tree

python/apsis/cond/dependency.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from apsis.lib.json import check_schema
44
from apsis.lib.py import format_ctor, iterize
5-
from apsis.runs import Instance, get_bind_args
5+
from apsis.runs import Instance, get_bind_args, template_expand
66
from apsis.states import State, reachable
77
from .base import RunStoreCondition, _bind
88

@@ -90,11 +90,12 @@ def from_jso(cls, jso):
9090
)
9191

9292
def bind(self, run, jobs):
93-
job = jobs[self.job_id]
9493
bind_args = get_bind_args(run)
94+
job_id = template_expand(self.job_id, bind_args)
95+
job = jobs[job_id]
9596
args = _bind(job, self.args, run.inst.args, bind_args)
9697
return type(self)(
97-
self.job_id,
98+
job_id,
9899
args,
99100
states=self.states,
100101
exist=self.exist,

test/int/cond/test_cond.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,65 @@ def test_to_error(client):
148148
assert res["state"] == "running"
149149

150150

151+
def test_parametrized_dependency(client):
152+
"""
153+
Tests that a dependency with a templated job_id resolves correctly.
154+
155+
"region dep" depends on "region/{{ region }}" with args date={{ date }}.
156+
We schedule a "region dep" run with region=us, which should wait for
157+
"region/us" to succeed.
158+
"""
159+
# Schedule a run that depends on region/us.
160+
res = client.schedule("region dep", {"region": "us", "date": "2024-01-01"})
161+
run_id = res["run_id"]
162+
163+
# It should be waiting for its dependency.
164+
res = client.get_run(run_id)
165+
assert res["state"] == "waiting"
166+
167+
# Satisfy the dependency by scheduling and completing region/us.
168+
res = client.schedule("region/us", {"date": "2024-01-01"})
169+
dep_run_id = res["run_id"]
170+
171+
# The dependency run completes immediately (no-op, duration 0).
172+
time.sleep(0.5)
173+
174+
# The dependent run should now have succeeded.
175+
res = client.get_run(dep_run_id)
176+
assert res["state"] == "success"
177+
res = client.get_run(run_id)
178+
assert res["state"] == "success"
179+
180+
181+
def test_parametrized_dependency_different_regions(client):
182+
"""
183+
Tests that different parameter values resolve to different dependency jobs.
184+
"""
185+
# Schedule runs for two different regions.
186+
res_us = client.schedule("region dep", {"region": "us", "date": "2024-01-01"})
187+
run_us = res_us["run_id"]
188+
res_eu = client.schedule("region dep", {"region": "eu", "date": "2024-01-01"})
189+
run_eu = res_eu["run_id"]
190+
191+
# Both should be waiting.
192+
assert client.get_run(run_us)["state"] == "waiting"
193+
assert client.get_run(run_eu)["state"] == "waiting"
194+
195+
# Satisfy only the US dependency.
196+
client.schedule("region/us", {"date": "2024-01-01"})
197+
time.sleep(0.5)
198+
199+
# Only the US run should proceed; EU remains waiting.
200+
assert client.get_run(run_us)["state"] == "success"
201+
assert client.get_run(run_eu)["state"] == "waiting"
202+
203+
# Now satisfy the EU dependency.
204+
client.schedule("region/eu", {"date": "2024-01-01"})
205+
time.sleep(0.5)
206+
207+
assert client.get_run(run_eu)["state"] == "success"
208+
209+
151210
def test_thread_cond(inst):
152211
client = inst.client
153212

test/unit/test_cond.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import pytest
2+
13
from apsis.cond import Condition
24
from apsis.cond.dependency import Dependency
35
from apsis.cond.skip_duplicate import SkipDuplicate
@@ -10,6 +12,8 @@
1012
JOBS = {
1113
"testjob0": Job("testjob0", {"foo"}, (), None),
1214
"testjob1": Job("testjob1", {"foo", "bar"}, (), None),
15+
"dep/us": Job("dep/us", {"date"}, (), None),
16+
"dep/eu": Job("dep/eu", {"date"}, (), None),
1317
}
1418

1519

@@ -57,6 +61,76 @@ def test_bind3():
5761
assert bound.states == dep.states
5862

5963

64+
def test_bind_parametrized_job_id():
65+
# Dependency job_id is a template expanded from the run's args.
66+
dep = Dependency("dep/{{ region }}", {})
67+
run = Run(Instance("testjob0", {"foo": "x", "region": "us"}))
68+
# Use a jobs dict that includes the expanded job_id.
69+
jobs = {**JOBS, "dep/us": Job("dep/us", {"foo"}, (), None)}
70+
bound = dep.bind(run, jobs)
71+
72+
assert bound.job_id == "dep/us"
73+
assert bound.args == {"foo": "x"}
74+
75+
76+
def test_bind_parametrized_job_id_with_args():
77+
# Templated job_id combined with templated args.
78+
dep = Dependency("dep/{{ region }}", {"date": "{{ date }}"})
79+
run = Run(Instance("testjob0", {"foo": "x", "region": "eu", "date": "2024-01-01"}))
80+
bound = dep.bind(run, JOBS)
81+
82+
assert bound.job_id == "dep/eu"
83+
assert bound.args == {"date": "2024-01-01"}
84+
85+
86+
def test_bind_parametrized_job_id_inherited_args():
87+
# Templated job_id with args inherited from the run.
88+
dep = Dependency("dep/{{ region }}", {})
89+
run = Run(Instance("testjob0", {"foo": "x", "region": "us", "date": "2024-03-15"}))
90+
jobs = {**JOBS, "dep/us": Job("dep/us", {"date"}, (), None)}
91+
bound = dep.bind(run, jobs)
92+
93+
assert bound.job_id == "dep/us"
94+
assert bound.args == {"date": "2024-03-15"}
95+
96+
97+
def test_bind_parametrized_job_id_not_found():
98+
# Templated job_id that expands to a nonexistent job.
99+
dep = Dependency("dep/{{ region }}", {})
100+
run = Run(Instance("testjob0", {"foo": "x", "region": "xx"}))
101+
with pytest.raises(LookupError):
102+
dep.bind(run, JOBS)
103+
104+
105+
def test_bind_parametrized_job_id_missing_param():
106+
# Templated job_id referencing a param not in the run's args.
107+
dep = Dependency("dep/{{ region }}", {})
108+
run = Run(Instance("testjob0", {"foo": "x"}))
109+
with pytest.raises(NameError):
110+
dep.bind(run, JOBS)
111+
112+
113+
def test_bind_literal_job_id_unchanged():
114+
# A literal job_id (no template) still works as before.
115+
dep = Dependency("testjob0", {"foo": "banana"})
116+
run = Run(Instance("testjob1", {"foo": "apple", "bar": "celery"}))
117+
bound = dep.bind(run, JOBS)
118+
119+
assert bound.job_id == "testjob0"
120+
assert bound.args == {"foo": "banana"}
121+
122+
123+
def test_bind_parametrized_job_id_jso_roundtrip():
124+
# Verify that a dependency with a templated job_id survives JSO roundtrip
125+
# (the unbound form preserves the template).
126+
dep = Dependency("dep/{{ region }}", {"date": "2024-01-01"})
127+
jso = dep.to_jso()
128+
restored = Condition.from_jso(jso)
129+
130+
assert restored.job_id == "dep/{{ region }}"
131+
assert restored.args == {"date": "2024-01-01"}
132+
133+
60134
def test_skip_duplicate_jso():
61135
cond = SkipDuplicate()
62136
cond = Condition.from_jso(cond.to_jso())

0 commit comments

Comments
 (0)