Skip to content

Commit c682c05

Browse files
radakamIsaac
authored andcommitted
Add job_runs lifecycle.triggers.on_value_change
Adds `lifecycle.triggers.on_value_change` for `resources.job_runs` (direct engine): watch an interpolated expression (a variable, workspace field, or a ${resources.*} reference) and re-fire the run when its resolved value changes. - Snapshot the expression before interpolation and use the trimmed form as the stable state key (SnapshotJobRunValueTriggers), so a value change is a modification of one entry rather than a change to the set of watches. - Resolution reuses existing mechanisms: normal variable interpolation resolves ${var.*}/${workspace.*} into triggers_state, and the planner resolves ${resources.*} via the references PrepareInputConfig registers, so the deploy graph depends on them. The resolved value is stored in state directly. - Removing or adding a watch is classified in OverrideChangeDesc alongside on_file_change: adding/changing recreates the run, removing keeps it. Built on the on_file_change work (#6309): no PersistState, path.String()-based change classification, prevent_destroy left to the generic checkForPreventDestroy. Co-authored-by: Isaac <no-reply@databricks.com>
1 parent af5bc3c commit c682c05

32 files changed

Lines changed: 858 additions & 19 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Add `lifecycle.triggers.on_value_change` for `job_runs`: re-fire the run when a watched expression's resolved value changes. ([#6341](https://github.com/databricks/cli/pull/6341))

‎acceptance/bundle/refschema/out.fields.txt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,10 +946,13 @@ resources.job_runs.*.lifecycle.triggers []resources.JobRunTrigger INPUT
946946
resources.job_runs.*.lifecycle.triggers[*] resources.JobRunTrigger INPUT
947947
resources.job_runs.*.lifecycle.triggers[*].on_bundle_deploy *bool INPUT
948948
resources.job_runs.*.lifecycle.triggers[*].on_file_change *string INPUT
949+
resources.job_runs.*.lifecycle.triggers[*].on_value_change *string INPUT
949950
resources.job_runs.*.lifecycle.triggers_state *resources.JobRunTriggersState INPUT STATE
950951
resources.job_runs.*.lifecycle.triggers_state.on_bundle_deploy string INPUT STATE
951952
resources.job_runs.*.lifecycle.triggers_state.on_file_change map[string]string INPUT STATE
952953
resources.job_runs.*.lifecycle.triggers_state.on_file_change.* string INPUT STATE
954+
resources.job_runs.*.lifecycle.triggers_state.on_value_change map[string]string INPUT STATE
955+
resources.job_runs.*.lifecycle.triggers_state.on_value_change.* string INPUT STATE
953956
resources.job_runs.*.modified_status string INPUT
954957
resources.job_runs.*.notebook_params map[string]string ALL
955958
resources.job_runs.*.notebook_params.* string ALL
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
bundle:
2+
name: job-runs-on-value-change
3+
4+
variables:
5+
stamp:
6+
default: v1
7+
other:
8+
default: a1
9+
number:
10+
default: 42
11+
long:
12+
default: a-long-non-hex-watched-value-used-to-exercise-a-lengthy-string-zzz
13+
14+
resources:
15+
jobs:
16+
my_job:
17+
name: my-job
18+
tasks:
19+
- task_key: main
20+
notebook_task:
21+
notebook_path: /Workspace/test
22+
23+
job_runs:
24+
my_run:
25+
job_id: ${resources.jobs.my_job.id}
26+
lifecycle:
27+
triggers:
28+
- on_file_change: watched.txt
29+
- on_value_change: " ${var.stamp} "
30+
- on_value_change: ${var.other}
31+
- on_value_change: ${var.number}
32+
- on_value_change: ${workspace.root_path}
33+
- on_value_change: ${var.long}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Cloud = false
2+
EnvMatrix.DMS = ["", "true"]
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
2+
=== first deploy triggers a run
3+
>>> [CLI] bundle deploy
4+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/job-runs-on-value-change/default/files...
5+
Created jobs.my_job
6+
Output from job_runs.my_run: id=[MY_RUN_ID]: Run URL: [DATABRICKS_URL]/jobs/[MY_JOB_ID]/runs/[MY_RUN_ID]?o=[NUMID]
7+
Output from job_runs.my_run: id=[MY_RUN_ID]: SUCCESS
8+
Created job_runs.my_run
9+
Files: 2 uploaded, 0 deleted
10+
Resources: 2 created, 0 changed, 0 deleted, 0 unchanged
11+
12+
>>> read_id.py my_job
13+
[MY_JOB_ID]
14+
15+
>>> print_requests.py //jobs/run-now
16+
{
17+
"method": "POST",
18+
"path": "/api/2.2/jobs/run-now",
19+
"body": {
20+
"idempotency_token": "[UUID]",
21+
"job_id": [MY_JOB_ID]
22+
}
23+
}
24+
25+
=== resolved values are stored as hashes
26+
>>> print_state.py
27+
{
28+
"on_file_change": {
29+
"watched.txt": "[FILE_HASH][0]"
30+
},
31+
"on_value_change": {
32+
"${var.long}": "a-long-non-hex-watched-value-used-to-exercise-a-lengthy-string-zzz",
33+
"${var.number}": "42",
34+
"${var.other}": "a1",
35+
"${var.stamp}": "v1",
36+
"${workspace.root_path}": "/Workspace/Users/[USERNAME]/.bundle/job-runs-on-value-change/default"
37+
}
38+
}
39+
40+
=== redeploy with unchanged values plans nothing
41+
>>> [CLI] bundle plan
42+
Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged
43+
44+
>>> [CLI] bundle deploy
45+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/job-runs-on-value-change/default/files...
46+
Files: 0 uploaded, 0 deleted
47+
Resources: 0 created, 0 changed, 0 deleted, 2 unchanged
48+
49+
>>> print_requests.py //jobs/run-now
50+
51+
=== changing one value re-fires
52+
>>> [CLI] bundle plan
53+
recreate job_runs.my_run
54+
55+
Plan: 1 to add, 0 to change, 1 to delete, 1 unchanged
56+
57+
>>> changes
58+
{
59+
"lifecycle.triggers_state.on_value_change['${var.stamp}']": {
60+
"action": "recreate",
61+
"reason": "immutable",
62+
"old": "v1",
63+
"new": "v2"
64+
},
65+
"result_state": {
66+
"action": "skip",
67+
"reason": "remote_already_set",
68+
"new": "SUCCESS",
69+
"remote": "SUCCESS"
70+
}
71+
}
72+
73+
>>> [CLI] bundle deploy
74+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/job-runs-on-value-change/default/files...
75+
Output from job_runs.my_run: id=[MY_RUN_ID_2]: Run URL: [DATABRICKS_URL]/jobs/[MY_JOB_ID]/runs/[MY_RUN_ID_2]?o=[NUMID]
76+
Output from job_runs.my_run: id=[MY_RUN_ID_2]: SUCCESS
77+
Recreated job_runs.my_run
78+
Files: 1 uploaded, 0 deleted
79+
Resources: 1 created, 0 changed, 1 deleted, 1 unchanged
80+
81+
>>> print_requests.py --keep //jobs/runs/delete
82+
83+
>>> print_requests.py //jobs/run-now
84+
{
85+
"method": "POST",
86+
"path": "/api/2.2/jobs/run-now",
87+
"body": {
88+
"idempotency_token": "[UUID]",
89+
"job_id": [MY_JOB_ID]
90+
}
91+
}
92+
93+
=== removing one on_value_change through a saved plan does not run
94+
>>> [CLI] bundle plan
95+
Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged
96+
97+
>>> [CLI] bundle deploy --plan tmp.plan.json
98+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/job-runs-on-value-change/default/files...
99+
Files: 2 uploaded, 0 deleted
100+
Resources: 0 created, 0 changed, 0 deleted, 2 unchanged
101+
102+
>>> print_requests.py //jobs/run-now
103+
104+
=== re-adding the same on_value_change after the saved plan re-fires
105+
>>> [CLI] bundle plan
106+
Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged
107+
108+
>>> changes
109+
{
110+
"result_state": {
111+
"action": "skip",
112+
"reason": "remote_already_set",
113+
"new": "SUCCESS",
114+
"remote": "SUCCESS"
115+
}
116+
}
117+
118+
>>> [CLI] bundle deploy
119+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/job-runs-on-value-change/default/files...
120+
Files: 1 uploaded, 0 deleted
121+
Resources: 0 created, 0 changed, 0 deleted, 2 unchanged
122+
123+
>>> print_requests.py --keep //jobs/runs/delete
124+
125+
>>> print_requests.py //jobs/run-now
126+
127+
=== removing the same on_value_change again does not run
128+
>>> [CLI] bundle plan
129+
Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged
130+
131+
>>> [CLI] bundle deploy
132+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/job-runs-on-value-change/default/files...
133+
Files: 1 uploaded, 0 deleted
134+
Resources: 0 created, 0 changed, 0 deleted, 2 unchanged
135+
136+
>>> print_requests.py //jobs/run-now
137+
138+
=== changing a remaining watch to a removed value re-fires
139+
>>> [CLI] bundle plan
140+
recreate job_runs.my_run
141+
142+
Plan: 1 to add, 0 to change, 1 to delete, 1 unchanged
143+
144+
>>> changes
145+
{
146+
"lifecycle.triggers_state.on_value_change['${var.stamp}']": {
147+
"action": "recreate",
148+
"reason": "immutable",
149+
"old": "v2",
150+
"new": "a1"
151+
},
152+
"result_state": {
153+
"action": "skip",
154+
"reason": "remote_already_set",
155+
"new": "SUCCESS",
156+
"remote": "SUCCESS"
157+
}
158+
}
159+
160+
>>> [CLI] bundle deploy
161+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/job-runs-on-value-change/default/files...
162+
Output from job_runs.my_run: id=[MY_RUN_ID_4]: Run URL: [DATABRICKS_URL]/jobs/[MY_JOB_ID]/runs/[MY_RUN_ID_4]?o=[NUMID]
163+
Output from job_runs.my_run: id=[MY_RUN_ID_4]: SUCCESS
164+
Recreated job_runs.my_run
165+
Files: 1 uploaded, 0 deleted
166+
Resources: 1 created, 0 changed, 1 deleted, 1 unchanged
167+
168+
>>> print_requests.py --keep //jobs/runs/delete
169+
170+
>>> print_requests.py //jobs/run-now
171+
{
172+
"method": "POST",
173+
"path": "/api/2.2/jobs/run-now",
174+
"body": {
175+
"idempotency_token": "[UUID]",
176+
"job_id": [MY_JOB_ID]
177+
}
178+
}
179+
180+
=== removing the remaining on_value_change entries is unchanged and does not run
181+
>>> [CLI] bundle plan
182+
Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged
183+
184+
>>> [CLI] bundle deploy
185+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/job-runs-on-value-change/default/files...
186+
Files: 1 uploaded, 0 deleted
187+
Resources: 0 created, 0 changed, 0 deleted, 2 unchanged
188+
189+
>>> print_requests.py //jobs/run-now
190+
191+
=== file changes still re-fire after value triggers are removed
192+
>>> [CLI] bundle plan
193+
recreate job_runs.my_run
194+
195+
Plan: 1 to add, 0 to change, 1 to delete, 1 unchanged
196+
197+
>>> changes
198+
{
199+
"lifecycle.triggers_state.on_file_change['watched.txt']": {
200+
"action": "recreate",
201+
"reason": "immutable",
202+
"old": "[FILE_HASH][0]",
203+
"new": "[FILE_HASH][1]"
204+
},
205+
"result_state": {
206+
"action": "skip",
207+
"reason": "remote_already_set",
208+
"new": "SUCCESS",
209+
"remote": "SUCCESS"
210+
}
211+
}
212+
213+
>>> [CLI] bundle deploy
214+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/job-runs-on-value-change/default/files...
215+
Output from job_runs.my_run: id=[MY_RUN_ID_5]: Run URL: [DATABRICKS_URL]/jobs/[MY_JOB_ID]/runs/[MY_RUN_ID_5]?o=[NUMID]
216+
Output from job_runs.my_run: id=[MY_RUN_ID_5]: SUCCESS
217+
Recreated job_runs.my_run
218+
Files: 1 uploaded, 0 deleted
219+
Resources: 1 created, 0 changed, 1 deleted, 1 unchanged
220+
221+
>>> print_requests.py --keep //jobs/runs/delete
222+
223+
>>> print_requests.py //jobs/run-now
224+
{
225+
"method": "POST",
226+
"path": "/api/2.2/jobs/run-now",
227+
"body": {
228+
"idempotency_token": "[UUID]",
229+
"job_id": [MY_JOB_ID]
230+
}
231+
}
232+
233+
>>> [CLI] bundle destroy --auto-approve
234+
The following resources will be deleted:
235+
delete resources.jobs.my_job
236+
237+
All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/job-runs-on-value-change/default
238+
239+
Destroy: 1 deleted
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
cleanup() {
2+
trace $CLI bundle destroy --auto-approve
3+
rm -f out.requests.txt
4+
}
5+
trap cleanup EXIT
6+
7+
changes() {
8+
$CLI bundle plan -o json | jq '.plan["resources.job_runs.my_run"].changes'
9+
}
10+
11+
title "first deploy triggers a run"
12+
trace $CLI bundle deploy
13+
trace read_id.py my_job
14+
read_id.py my_run > /dev/null
15+
trace print_requests.py //jobs/run-now
16+
17+
title "resolved values are stored as hashes"
18+
trace print_state.py | jq '.state["resources.job_runs.my_run"].state.lifecycle.triggers_state'
19+
20+
title "redeploy with unchanged values plans nothing"
21+
trace $CLI bundle plan
22+
trace $CLI bundle deploy
23+
trace print_requests.py //jobs/run-now
24+
25+
title "changing one value re-fires"
26+
update_file.py databricks.yml "default: v1" "default: v2"
27+
trace $CLI bundle plan
28+
trace changes
29+
trace $CLI bundle deploy
30+
read_id.py my_run > /dev/null
31+
trace print_requests.py --keep //jobs/runs/delete
32+
trace print_requests.py //jobs/run-now
33+
34+
title "removing one on_value_change through a saved plan does not run"
35+
update_file.py databricks.yml ' - on_value_change: ${var.other}
36+
' ''
37+
trace $CLI bundle plan
38+
$CLI bundle plan -o json > tmp.plan.json
39+
trace $CLI bundle deploy --plan tmp.plan.json
40+
trace print_requests.py //jobs/run-now
41+
42+
title "re-adding the same on_value_change after the saved plan re-fires"
43+
update_file.py databricks.yml ' - on_value_change: " ${var.stamp} "
44+
' ' - on_value_change: " ${var.stamp} "
45+
- on_value_change: ${var.other}
46+
'
47+
trace $CLI bundle plan
48+
trace changes
49+
trace $CLI bundle deploy
50+
read_id.py my_run > /dev/null
51+
trace print_requests.py --keep //jobs/runs/delete
52+
trace print_requests.py //jobs/run-now
53+
54+
title "removing the same on_value_change again does not run"
55+
update_file.py databricks.yml ' - on_value_change: ${var.other}
56+
' ''
57+
trace $CLI bundle plan
58+
trace $CLI bundle deploy
59+
trace print_requests.py //jobs/run-now
60+
61+
title "changing a remaining watch to a removed value re-fires"
62+
update_file.py databricks.yml "default: v2" "default: a1"
63+
trace $CLI bundle plan
64+
trace changes
65+
trace $CLI bundle deploy
66+
read_id.py my_run > /dev/null
67+
trace print_requests.py --keep //jobs/runs/delete
68+
trace print_requests.py //jobs/run-now
69+
70+
title "removing the remaining on_value_change entries is unchanged and does not run"
71+
update_file.py databricks.yml ' - on_value_change: " ${var.stamp} "
72+
- on_value_change: ${var.number}
73+
- on_value_change: ${workspace.root_path}
74+
- on_value_change: ${var.long}
75+
' ''
76+
trace $CLI bundle plan
77+
trace $CLI bundle deploy
78+
trace print_requests.py //jobs/run-now
79+
80+
title "file changes still re-fire after value triggers are removed"
81+
update_file.py watched.txt "v1" "v2"
82+
trace $CLI bundle plan
83+
trace changes
84+
trace $CLI bundle deploy
85+
read_id.py my_run > /dev/null
86+
trace print_requests.py --keep //jobs/runs/delete
87+
trace print_requests.py //jobs/run-now
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# The on_file_change content hash contains digit runs that the parent `\d{8,}`
2+
# rule would mangle. Order=1 runs before it; Distinct keeps different hashes distinct.
3+
Ignore = ["tmp.plan.json"]
4+
5+
[[Repls]]
6+
Old = '[0-9a-f]{64}'
7+
New = '[FILE_HASH]'
8+
Order = 1
9+
Distinct = true
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v1

0 commit comments

Comments
 (0)