-
Notifications
You must be signed in to change notification settings - Fork 207
Job runs wait for completion #6091
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d9b213a
19f4999
de9b3c7
2e821c2
fabfcbe
67d9dd7
5fae863
4bb2518
01616bb
52a212e
a60fbb5
7d2e425
c16e514
8b26816
bd184a7
57b8378
6a563e0
c0bbf1e
4287123
0490bc4
f9c6b62
7618426
94dc061
df8657d
c66be74
884095a
5645955
7fa1984
72fde3c
afadfa2
08f1300
dd314d9
9326d19
7a6acc5
d9d4783
22249ed
27fe634
78a7a6f
bcfaf78
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| direct: the experimental `job_runs` resource now waits for the triggered run to finish, so other resources can reference its outcome (e.g. `${resources.job_runs.nightly.state.result_state}`). A run that does not succeed fails the deploy, naming the failed task, and is run again on the next deploy. If a deploy is interrupted while waiting, the next one resumes waiting on the same run. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| print("hello from a job_run") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,8 @@ Resources: | |
| >>> [CLI] bundle deploy | ||
| Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/job-runs-basic/default/files... | ||
| Deploying resources... | ||
| job run [MY_RUN_ID]: Run URL: [DATABRICKS_URL]/jobs/[MY_JOB_ID]/runs/[MY_RUN_ID]?o=[NUMID] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we include key name in the prefix? "Output from jobs_runs.foo: id=[MY_RUN_ID]: ..." |
||
| job run [MY_RUN_ID]: SUCCESS | ||
| Updating deployment state... | ||
| Deployment complete! | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| bundle: | ||
| name: job-runs-failed-run | ||
|
|
||
| workspace: | ||
| root_path: ~/.bundle/$UNIQUE_NAME | ||
|
|
||
| resources: | ||
| jobs: | ||
| my_job: | ||
| name: test-job-$UNIQUE_NAME | ||
| tasks: | ||
| # Serverless keeps the run to a few minutes: the Jobs API retries the | ||
| # failed task once before giving up on it. | ||
| - task_key: main | ||
| spark_python_task: | ||
| python_file: ./fail.py | ||
| environment_key: default | ||
|
|
||
| environments: | ||
| - environment_key: default | ||
| spec: | ||
| environment_version: "2" | ||
|
|
||
| # Reads the run's outcome, so the failing run aborts the deploy before this | ||
| # job is created. Separate from my_job, which my_run already depends on, to | ||
| # avoid a cycle. | ||
| downstream_job: | ||
| name: test-downstream-job-$UNIQUE_NAME | ||
| tags: | ||
| run_result: ${resources.job_runs.my_run.state.result_state} | ||
| tasks: | ||
| - task_key: main | ||
| condition_task: | ||
| op: EQUAL_TO | ||
| left: "1" | ||
| right: "1" | ||
|
|
||
| job_runs: | ||
| my_run: | ||
| job_id: ${resources.jobs.my_job.id} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| raise RuntimeError("intentional failure") |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
|
|
||
| === a run that finishes FAILED fails the deploy | ||
| >>> [CLI] bundle deploy | ||
| Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/[UNIQUE_NAME]/files... | ||
| Deploying resources... | ||
| job run [MY_RUN_ID]: Run URL: [RUN_URL] | ||
| Error: cannot create resources.job_runs.my_run: waiting after creating id=[MY_RUN_ID]: run did not succeed: FAILED: Task main failed with message: Workload failed, see run output for details. | ||
| task "main": RuntimeError: intentional failure | ||
| run page: [RUN_URL] | ||
|
|
||
| Error: cannot create resources.jobs.downstream_job: dependency failed: resources.job_runs.my_run | ||
|
|
||
| Updating deployment state... | ||
|
|
||
| === the failed run is recorded, and not having succeeded is drift | ||
| >>> read_id.py my_run | ||
| [MY_RUN_ID] | ||
|
|
||
| >>> [CLI] bundle plan | ||
| recreate job_runs.my_run | ||
| create jobs.downstream_job | ||
|
|
||
| Plan: 2 to add, 0 to change, 1 to delete, 1 unchanged | ||
|
|
||
| === so a redeploy runs the job again, and fails again | ||
| >>> [CLI] bundle deploy | ||
| Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/[UNIQUE_NAME]/files... | ||
| Deploying resources... | ||
| job run [MY_RUN_ID_2]: Run URL: [RUN_URL] | ||
| Error: cannot recreate resources.job_runs.my_run: waiting after creating id=[MY_RUN_ID_2]: run did not succeed: FAILED: Task main failed with message: Workload failed, see run output for details. | ||
| task "main": RuntimeError: intentional failure | ||
| run page: [RUN_URL] | ||
|
|
||
| Error: cannot create resources.jobs.downstream_job: dependency failed: resources.job_runs.my_run | ||
|
|
||
| Updating deployment state... | ||
|
|
||
| === run-now was issued once per deploy, and the recreate deleted the failed run | ||
| >>> print_requests.py --keep //jobs/run-now | ||
| { | ||
| "method": "POST", | ||
| "path": "/api/2.2/jobs/run-now", | ||
| "body": { | ||
| "job_id": [MY_JOB_ID] | ||
| } | ||
| } | ||
| { | ||
| "method": "POST", | ||
| "path": "/api/2.2/jobs/run-now", | ||
| "body": { | ||
| "job_id": [MY_JOB_ID] | ||
| } | ||
| } | ||
|
|
||
| >>> print_requests.py //jobs/runs/delete | ||
| { | ||
| "method": "POST", | ||
| "path": "/api/2.2/jobs/runs/delete", | ||
| "body": { | ||
| "run_id": [MY_RUN_ID] | ||
| } | ||
| } | ||
|
|
||
| >>> [CLI] bundle destroy --auto-approve | ||
| The following resources will be deleted: | ||
| delete resources.job_runs.my_run | ||
| delete resources.jobs.my_job | ||
|
|
||
| All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/[UNIQUE_NAME] | ||
|
|
||
| Deleting files... | ||
| Destroy complete! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| envsubst < databricks.yml.tmpl > databricks.yml | ||
|
|
||
| cleanup() { | ||
| trace $CLI bundle destroy --auto-approve | ||
| rm -f out.requests.txt | ||
| } | ||
| trap cleanup EXIT | ||
|
|
||
| # The error names the failed task and the message the workspace reported for it, | ||
| # and downstream_job is reported as a failed dependency because it reads the | ||
| # run's result_state. | ||
| title "a run that finishes FAILED fails the deploy" | ||
| musterr trace $CLI bundle deploy | ||
|
|
||
| # The framework saves the run id before calling WaitAfterCreate, so the failed run | ||
| # stays recorded, and FAILED against the required SUCCESS is drift. | ||
| title "the failed run is recorded, and not having succeeded is drift" | ||
| trace read_id.py my_run | ||
| trace $CLI bundle plan | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we record json plan here? narrow it to run job_runs specifically. |
||
|
|
||
| # Name the job too, so the run-now bodies below say which number it is. | ||
| read_id.py my_job > /dev/null | ||
|
|
||
| title "so a redeploy runs the job again, and fails again" | ||
| musterr trace $CLI bundle deploy | ||
| read_id.py my_run > /dev/null | ||
|
|
||
| # The delete names [MY_RUN_ID], the run that failed first, not the [MY_RUN_ID_2] | ||
| # that replaced it: the recreate discards the run it replaces. | ||
| title "run-now was issued once per deploy, and the recreate deleted the failed run" | ||
| trace print_requests.py --keep //jobs/run-now | ||
| trace print_requests.py //jobs/runs/delete | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # job_runs is a direct-engine-only resource; the Terraform provider has no | ||
| # equivalent, so restrict the matrix to direct. | ||
| EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] | ||
|
|
||
| # Runs the failing job for real, so the message the deploy names the task with is | ||
| # one a workspace reported. Serverless needs Unity Catalog. | ||
| Cloud = true | ||
| RequiresUnityCatalog = true | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought we don't need this anymore? RequiresUnityCatalog |
||
|
|
||
| # databricks.yml is rendered by the script, and the deploy fails mid-way, leaving | ||
| # local deployment state behind. | ||
| Ignore = [ | ||
| ".databricks", | ||
| "databricks.yml", | ||
| ] | ||
|
|
||
| # The host and the workspace selector in the run URL differ per workspace; the URL | ||
| # form itself is covered by libs/workspaceurls. | ||
| [[Repls]] | ||
| Old = 'Run URL: .*' | ||
| New = 'Run URL: [RUN_URL]' | ||
|
|
||
| [[Repls]] | ||
| Old = 'run page: .*' | ||
| New = 'run page: [RUN_URL]' | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| bundle: | ||
| name: job-runs-interrupted-run | ||
|
|
||
| resources: | ||
| jobs: | ||
| my_job: | ||
| name: my-job | ||
| tasks: | ||
| - task_key: main | ||
| notebook_task: | ||
| notebook_path: /Workspace/test | ||
|
|
||
| job_runs: | ||
| my_run: | ||
| job_id: ${resources.jobs.my_job.id} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
|
|
||
| === the deploy stops waiting before the run finishes | ||
| >>> errcode [CLI] bundle deploy | ||
| Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/job-runs-interrupted-run/default/files... | ||
| Deploying resources... | ||
| Error: cannot create resources.job_runs.my_run: waiting after creating id=[MY_RUN_ID]: Fault injected by test. (403 INJECTED) | ||
|
|
||
| Endpoint: GET [DATABRICKS_URL]/api/2.2/jobs/runs/get?run_id=[MY_RUN_ID] | ||
| HTTP Status: 403 Forbidden | ||
| API error_code: INJECTED | ||
| API message: Fault injected by test. | ||
|
|
||
| Updating deployment state... | ||
|
|
||
| Exit code: 1 | ||
|
|
||
| === the abandoned run is still tracked | ||
| >>> read_id.py my_job | ||
| [MY_JOB_ID] | ||
|
|
||
| >>> read_id.py my_run | ||
| [MY_RUN_ID] | ||
|
|
||
| >>> print_requests.py //jobs/run-now | ||
| { | ||
| "method": "POST", | ||
| "path": "/api/2.2/jobs/run-now", | ||
| "body": { | ||
| "job_id": [MY_JOB_ID] | ||
| } | ||
| } | ||
|
|
||
| === a run that is still going is an update | ||
| >>> jq .plan["resources.job_runs.my_run"] tmp.plan.json | ||
| { | ||
| "depends_on": [ | ||
| { | ||
| "node": "resources.jobs.my_job", | ||
| "label": "${resources.jobs.my_job.id}" | ||
| } | ||
| ], | ||
| "action": "update", | ||
| "new_state": { | ||
| "value": { | ||
| "job_id": [MY_JOB_ID], | ||
| "result_state": "SUCCESS" | ||
| } | ||
| }, | ||
| "remote_state": { | ||
| "job_id": [MY_JOB_ID], | ||
| "run_id": [MY_RUN_ID], | ||
| "run_name": "my-job", | ||
| "run_page_url": "[DATABRICKS_URL]/jobs/[MY_JOB_ID]/runs/[MY_RUN_ID]?o=[NUMID]", | ||
| "run_type": "JOB_RUN", | ||
| "state": { | ||
| "life_cycle_state": "RUNNING" | ||
| } | ||
| }, | ||
| "changes": { | ||
| "result_state": { | ||
| "action": "update", | ||
| "reason": "run in progress", | ||
| "old": "SUCCESS", | ||
| "new": "SUCCESS" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| === the deploy adopts that run and waits for it to finish | ||
| >>> [CLI] bundle deploy --plan tmp.plan.json | ||
| Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/job-runs-interrupted-run/default/files... | ||
| Deploying resources... | ||
| job run [MY_RUN_ID]: Run URL: [DATABRICKS_URL]/jobs/[MY_JOB_ID]/runs/[MY_RUN_ID]?o=[NUMID] | ||
| job run [MY_RUN_ID]: SUCCESS | ||
| Updating deployment state... | ||
| Deployment complete! | ||
|
|
||
| === the same run finished, and nothing was cancelled, deleted or triggered | ||
| >>> read_state.py job_runs my_run id | ||
| job_runs my_run id='[MY_RUN_ID]' | ||
|
|
||
| >>> print_requests.py //jobs | ||
|
|
||
| >>> [CLI] bundle destroy --auto-approve | ||
| The following resources will be deleted: | ||
| delete resources.job_runs.my_run | ||
| delete resources.jobs.my_job | ||
|
|
||
| All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/job-runs-interrupted-run/default | ||
|
|
||
| Deleting files... | ||
| Destroy complete! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| cleanup() { | ||
| trace $CLI bundle destroy --auto-approve | ||
| rm -f out.requests.txt | ||
| } | ||
| trap cleanup EXIT | ||
|
|
||
| # The fault hits the poll, not the run: it ends the deploy's wait while leaving the | ||
| # run going, which is what an interrupted deploy leaves behind: the run id is | ||
| # recorded in the state, but no outcome was ever observed. | ||
| fault.py "GET /api/2.2/jobs/runs/get" 403 0 1 | ||
|
|
||
| title "the deploy stops waiting before the run finishes" | ||
| trace errcode $CLI bundle deploy | ||
|
|
||
| title "the abandoned run is still tracked" | ||
| trace read_id.py my_job | ||
| trace read_id.py my_run | ||
| trace print_requests.py //jobs/run-now | ||
|
|
||
| title "a run that is still going is an update" | ||
| $CLI bundle plan -o json > tmp.plan.json | ||
| trace jq '.plan["resources.job_runs.my_run"]' tmp.plan.json | ||
|
|
||
| # The testserver settles a run on its first poll, so the plan above is replayed | ||
| # here: the second read of the run reports it finished. | ||
| title "the deploy adopts that run and waits for it to finish" | ||
| trace $CLI bundle deploy --plan tmp.plan.json | ||
|
|
||
| title "the same run finished, and nothing was cancelled, deleted or triggered" | ||
| trace read_state.py job_runs my_run id | ||
| trace print_requests.py //jobs |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # job_runs is a direct-engine-only resource; the Terraform provider has no | ||
| # equivalent, so restrict the matrix to direct. | ||
| EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] | ||
|
|
||
| # The interruption is staged by injecting a fault into the testserver, so this | ||
| # stays off cloud. | ||
| RecordRequests = true | ||
| Ignore = ["tmp.plan.json"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
q: does this belong in the state?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
discussed offline - we want this to enforce recreate on failed runs but we need to be careful not to recreate in progress runs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Went with keeping the field and handling the in-progress case in OverrideChangeDesc, which downgrades result_state drift to an update while the run is still going. That felt more elegant to me, but I'm open to discussing it:)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even though it is marked "STATE" because it's on StateType, we should still not serialize it (by clearing it).
In your acc tests, could you dump portion of the state related to job run so that we see whether it's actually stored?