Add deferrable mode to Databricks SQL warehouse operators - #71752

Merged
eladkal merged 3 commits into
apache:mainfrom
Vamsi-klu:follow-up-70088-warehouse-deferrable
Aug 26, 2026
Merged

Add deferrable mode to Databricks SQL warehouse operators#71752
eladkal merged 3 commits into
apache:mainfrom
Vamsi-klu:follow-up-70088-warehouse-deferrable

Conversation

@Vamsi-klu

@Vamsi-kluVamsi-klu commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

This adds an additive deferrable path for the Databricks SQL warehouse start and stop operators, so lifecycle waits can run on the triggerer instead of holding a worker.

related: #70088
related: #21377

Problem

Airflow's Databricks provider can start and stop an existing SQL warehouse, but Phase 1 waits hold a worker for the entire poll. Dag authors who want triggerer-based waiting currently have no deferrable path on these operators.

What changed

  • Add DatabricksHook.a_get_warehouse_state as the async GET mirror of get_warehouse_state.
  • Restore WarehouseState.to_json / from_json for trigger event round-trip.
  • Add DatabricksWarehouseStateTrigger next to the provider's existing triggers, with serialize, local monotonic timeout, retry_args validation, and a documented no-op on_kill.
  • Add deferrable on both DatabricksStartWarehouseOperator and DatabricksStopWarehouseOperator using the default_deferrable pattern.
  • Document the deferrable path in the warehouse how-to and add a provider changelog Features note.

No request-body workaround or new dependency is introduced. The implementation follows the Databricks SQL Warehouses API and keeps the Phase 1 wait contract.

Scope

This is the Phase 2 deferrable follow-up agreed on #70088: async hook, serialized trigger, operator completion path, and compatibility tests. Create/delete, edit, warehouse-by-name resolution, and a live Databricks system-test Dag remain outside this PR so the follow-up stays reviewable and independently useful.

Behavior and compatibility

  • deferrable defaults off and honors [operators] default_deferrable.
  • wait_for_termination, polling_period_seconds, and timeout are unchanged.
  • Starting an already RUNNING warehouse and stopping an already STOPPED warehouse remain no-ops and do not defer.
  • wait_for_termination=False still returns after requesting the transition and does not defer.
  • Existing STARTING/STOPPING transitions are reused instead of issuing duplicate requests, then deferred if waiting.
  • A start accepted while the warehouse still reports STOPPING continues on the triggerer; Databricks API transition rejections propagate unchanged.
  • Stale STOPPED while waiting for RUNNING is not terminal; the trigger keeps polling until RUNNING, deletion, or timeout.
  • Waiting uses time.monotonic() locally in the trigger, starts no new poll after the configured deadline, and still honors a target or deletion state returned by a poll that began before the deadline.
  • Clearing a deferred wait does not start or stop the warehouse: Databricks has no cancel API for these transitions.
  • execute_complete maps success / deleted / timeout to the same messages as Phase 1.

Validation

  • uv run --project providers/databricks pytest providers/databricks/tests/unit/databricks/operators/test_warehouse.py providers/databricks/tests/unit/databricks/hooks/test_databricks.py::TestWarehouseLifecycle providers/databricks/tests/unit/databricks/triggers/test_databricks.py::TestDatabricksWarehouseStateTrigger providers/databricks/tests/unit/databricks/triggers/test_databricks.py::test_trigger_init_rejects_non_serializable_retry_args — 70 passed.
  • prek run --from-ref upstream/main --stage pre-commit — passed.

Reviewer evidence

This PR has no UI surface, so before/after screenshots and browser validation are not applicable. The REST boundary is covered with an autospecced async GET assertion, operator deferral is covered with a specced hook, and trigger serialize/run/timeout/on_kill are covered with mocked warehouse state. No Databricks workspace credentials were used or required for these deterministic lifecycle tests.

The Phase 2 scope was posted on #70088 before this follow-up: #70088 (comment)


Was generative AI tooling used to co-author this PR?
  • Yes — Cursor Grok 4.6

Generated-by: Cursor Grok 4.6 following the guidelines

@Vamsi-klu

Vamsi-klu commented Aug 18, 2026

Copy link
Copy Markdown
ContributorAuthor

@eladkal@moomindani ni this is the #70088 deferrable follow-up, could you review when you have a chance?

@moomindanimoomindani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — the Phase 1 contract is preserved carefully here, and the state-machine edge cases we went through on #70088 (stale STOPPED not terminal, deletion mid-wait, the deadline boundary) are all covered by deterministic tests rather than sleeps.

I validated the deferrable paths against a real workspace: Airflow 3.2.2, this branch's provider installed from source, a dedicated throwaway serverless warehouse (2X-Small), triggerer doing the waiting.

scenariotask stateevidence
deferrable start from STOPPEDdeferred -> successtrigger logged is STARTING; waiting for RUNNING, warehouse reached RUNNING
deferrable stopdeferred -> successwarehouse reached STOPPED
start against an already-RUNNING warehousesuccess, never deferredno DeferTask for the task
wait_for_termination=False stopsuccess, never deferredtransition still requested, warehouse stopped afterwards
timeout=1 startdeferred -> failedDatabricksWarehouseError: ... did not reach RUNNING within 1s; last state: STARTING.

So the behavioural claims in the description hold up where I could exercise them. Unit tests reproduce your numbers as well (43 + 14 + 13, and the full provider suite at 922 passed / 12 skipped).

Two things I would like settled before merge, both left inline:

  • The changelog entry is inserted under the 7.18.1 header, which is an already-released version, so a shipped release would advertise a feature it does not contain. Per providers/AGENTS.md and the NOTE TO CONTRIBUTORS block at the top of that file, routine feature entries are collected by the release manager from commit messages anyway — the five lines can just go.
  • The deferrable timeout is a duration recomputed inside every run(), so it restarts from zero on each triggerer restart or HA rebalance, while the synchronous path fails deterministically. DatabricksSQLStatementExecutionTrigger next door serializes an absolute end_time for exactly this reason. Either follow that pattern or state the per-run semantics explicitly — right now the docstring and the PR description claim the opposite of what the code does.

The rest is nits, also inline. One out-of-scope note worth being aware of rather than fixing here: these operators' first deferrable surface inherits #71525, so on core 3.0.x the trigger cannot fetch the connection at all (3.1.0+ is fine). Whether that caveat belongs in this page or centrally is a call for the docs discussion happening on #71667.

Comment threadproviders/databricks/docs/changelog.rst Outdated
@Vamsi-klu

Vamsi-klu commented Aug 19, 2026

Copy link
Copy Markdown
ContributorAuthor

Thanks — the Phase 1 contract is preserved carefully here, and the state-machine edge cases we went through on #70088 (stale STOPPED not terminal, deletion mid-wait, the deadline boundary) are all covered by deterministic tests rather than sleeps.

I validated the deferrable paths against a real workspace: Airflow 3.2.2, this branch's provider installed from source, a dedicated throwaway serverless warehouse (2X-Small), triggerer doing the waiting.

scenariotask stateevidence
deferrable start from STOPPEDdeferred -> successtrigger logged is STARTING; waiting for RUNNING, warehouse reached RUNNING
deferrable stopdeferred -> successwarehouse reached STOPPED
start against an already-RUNNING warehousesuccess, never deferredno DeferTask for the task
wait_for_termination=False stopsuccess, never deferredtransition still requested, warehouse stopped afterwards
timeout=1 startdeferred -> failedDatabricksWarehouseError: ... did not reach RUNNING within 1s; last state: STARTING.

So the behavioural claims in the description hold up where I could exercise them. Unit tests reproduce your numbers as well (43 + 14 + 13, and the full provider suite at 922 passed / 12 skipped).

Two things I would like settled before merge, both left inline:

  • The changelog entry is inserted under the 7.18.1 header, which is an already-released version, so a shipped release would advertise a feature it does not contain. Per providers/AGENTS.md and the NOTE TO CONTRIBUTORS block at the top of that file, routine feature entries are collected by the release manager from commit messages anyway — the five lines can just go.
  • The deferrable timeout is a duration recomputed inside every run(), so it restarts from zero on each triggerer restart or HA rebalance, while the synchronous path fails deterministically. DatabricksSQLStatementExecutionTrigger next door serializes an absolute end_time for exactly this reason. Either follow that pattern or state the per-run semantics explicitly — right now the docstring and the PR description claim the opposite of what the code does.

The rest is nits, also inline. One out-of-scope note worth being aware of rather than fixing here: these operators' first deferrable surface inherits #71525, so on core 3.0.x the trigger cannot fetch the connection at all (3.1.0+ is fine). Whether that caveat belongs in this page or centrally is a call for the docs discussion happening on #71667.

Thanks for the live workspace check @moomindani i responded to comments and commited the latest change

The follow-up is on the branch. Changelog Features under 7.18.1 is gone. The trigger now serializes an absolute end_time the same way DatabricksSQLStatementExecutionTrigger does. a_get_warehouse shares the GET path with get_warehouse. The on_kill override and the unpinned test are gone. The typed state JSON stays for sibling trigger symmetry, and the description now mentions the Phase 1 to_json restore.

The 3.0.x connection fetch caveat from 71525 is still out of scope here.

@moomindanimoomindani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — all five points are addressed, and I re-verified on 38bc535d4a rather than taking it on trust.

The changelog is back to the released 7.18.1 section untouched. The deadline is now an absolute end_time computed by the operator, serialized, and compared with time.time() in run(), matching DatabricksSQLStatementExecutionTrigger — and the two new tests pin it where it matters: the round trip preserves end_time, and a trigger resumed with an already-passed end_time times out immediately instead of opening a fresh window. That was the part I cared about most. on_kill and its vacuous test are gone with the rationale kept in the class docstring, and a_get_warehouse_state now goes through a_get_warehouse; the remaining duplicated path suffix matches how start_warehouse / stop_warehouse declare theirs in that file, so no objection from me. Keeping the typed state payload for sibling symmetry with the reasoning recorded is a fine call.

Real workspace re-run on this head (Airflow 3.2.2, provider installed from the branch, dedicated throwaway serverless warehouse): deferrable start deferred -> success, deferrable stop deferred -> success, and timeout=1deferred -> failed with did not reach RUNNING within 1s. 203 unit tests pass across the three files.

One observation from that last run, not a request: with a timeout shorter than the defer round-trip, the absolute deadline has already passed by the time the triggerer picks the trigger up, so it never polls and the message reports last state: unknown where the synchronous path would have polled once and named a state. That is the correct consequence of honouring an absolute deadline and is invisible at realistic timeouts — just noting that the boundary now differs slightly between the two paths.

LGTM.


Drafted-by: Claude Code (Opus 5); reviewed by @moomindani before posting

@Vamsi-klu

Copy link
Copy Markdown
ContributorAuthor

@eladkal as @moomindani approved the PR, Can i get your thoughts on this please?

@eladkal

Copy link
Copy Markdown
Contributor

can you please update the comment in #21377 (comment) with checklist to the actual steps in the plan and which PR solved which step? That way we can easily track what is left open

Vamsi-kluand others added 3 commits August 24, 2026 00:18
Start and stop waits from apache#70088 held a worker for the whole warehouse
lifecycle. This follow-up lands the agreed additive triggerer path so
those waits no longer occupy a worker slot.
Co-authored-by: Cursor <cursoragent@cursor.com>
A duration recomputed inside the trigger started a fresh wait after every triggerer restart or HA rebalance, so a warehouse that never reached its target could stay deferred well past the configured timeout.
Sphinx treats the HA term as a misspelling and fails the provider docs job.
Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor
cursorBotforce-pushed the follow-up-70088-warehouse-deferrable branch from 16dfb9e to 8f7d2d3CompareAugust 24, 2026 00:19
@Vamsi-klu

Vamsi-klu commented Aug 24, 2026

Copy link
Copy Markdown
ContributorAuthor

Hi @eladkal thanks a ton for the comment

Updated the tracking comment: #21377 (comment)

#70088 is Phase 1. This PR is the deferrable follow-up. Create/delete, name lookup, edit, and a live system-test Dag are still open.

@eladkal
eladkal merged commit c4e6ed3 into apache:mainAug 26, 2026
83 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@Vamsi-klu@eladkal@moomindani@cursoragent
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add deferrable mode to Databricks SQL warehouse operators - #71752

Merged
eladkal merged 3 commits into
apache:mainfrom
Vamsi-klu:follow-up-70088-warehouse-deferrable
Aug 26, 2026
Merged

Add deferrable mode to Databricks SQL warehouse operators#71752
eladkal merged 3 commits into
apache:mainfrom
Vamsi-klu:follow-up-70088-warehouse-deferrable

Conversation

@Vamsi-klu

@Vamsi-kluVamsi-klu commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

This adds an additive deferrable path for the Databricks SQL warehouse start and stop operators, so lifecycle waits can run on the triggerer instead of holding a worker.

related: #70088
related: #21377

Problem

Airflow's Databricks provider can start and stop an existing SQL warehouse, but Phase 1 waits hold a worker for the entire poll. Dag authors who want triggerer-based waiting currently have no deferrable path on these operators.

What changed

  • Add DatabricksHook.a_get_warehouse_state as the async GET mirror of get_warehouse_state.
  • Restore WarehouseState.to_json / from_json for trigger event round-trip.
  • Add DatabricksWarehouseStateTrigger next to the provider's existing triggers, with serialize, local monotonic timeout, retry_args validation, and a documented no-op on_kill.
  • Add deferrable on both DatabricksStartWarehouseOperator and DatabricksStopWarehouseOperator using the default_deferrable pattern.
  • Document the deferrable path in the warehouse how-to and add a provider changelog Features note.

No request-body workaround or new dependency is introduced. The implementation follows the Databricks SQL Warehouses API and keeps the Phase 1 wait contract.

Scope

This is the Phase 2 deferrable follow-up agreed on #70088: async hook, serialized trigger, operator completion path, and compatibility tests. Create/delete, edit, warehouse-by-name resolution, and a live Databricks system-test Dag remain outside this PR so the follow-up stays reviewable and independently useful.

Behavior and compatibility

  • deferrable defaults off and honors [operators] default_deferrable.
  • wait_for_termination, polling_period_seconds, and timeout are unchanged.
  • Starting an already RUNNING warehouse and stopping an already STOPPED warehouse remain no-ops and do not defer.
  • wait_for_termination=False still returns after requesting the transition and does not defer.
  • Existing STARTING/STOPPING transitions are reused instead of issuing duplicate requests, then deferred if waiting.
  • A start accepted while the warehouse still reports STOPPING continues on the triggerer; Databricks API transition rejections propagate unchanged.
  • Stale STOPPED while waiting for RUNNING is not terminal; the trigger keeps polling until RUNNING, deletion, or timeout.
  • Waiting uses time.monotonic() locally in the trigger, starts no new poll after the configured deadline, and still honors a target or deletion state returned by a poll that began before the deadline.
  • Clearing a deferred wait does not start or stop the warehouse: Databricks has no cancel API for these transitions.
  • execute_complete maps success / deleted / timeout to the same messages as Phase 1.

Validation

  • uv run --project providers/databricks pytest providers/databricks/tests/unit/databricks/operators/test_warehouse.py providers/databricks/tests/unit/databricks/hooks/test_databricks.py::TestWarehouseLifecycle providers/databricks/tests/unit/databricks/triggers/test_databricks.py::TestDatabricksWarehouseStateTrigger providers/databricks/tests/unit/databricks/triggers/test_databricks.py::test_trigger_init_rejects_non_serializable_retry_args — 70 passed.
  • prek run --from-ref upstream/main --stage pre-commit — passed.

Reviewer evidence

This PR has no UI surface, so before/after screenshots and browser validation are not applicable. The REST boundary is covered with an autospecced async GET assertion, operator deferral is covered with a specced hook, and trigger serialize/run/timeout/on_kill are covered with mocked warehouse state. No Databricks workspace credentials were used or required for these deterministic lifecycle tests.

The Phase 2 scope was posted on #70088 before this follow-up: #70088 (comment)


Was generative AI tooling used to co-author this PR?
  • Yes — Cursor Grok 4.6

Generated-by: Cursor Grok 4.6 following the guidelines

@Vamsi-klu

Vamsi-klu commented Aug 18, 2026

Copy link
Copy Markdown
ContributorAuthor

@eladkal@moomindani ni this is the #70088 deferrable follow-up, could you review when you have a chance?

@moomindanimoomindani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — the Phase 1 contract is preserved carefully here, and the state-machine edge cases we went through on #70088 (stale STOPPED not terminal, deletion mid-wait, the deadline boundary) are all covered by deterministic tests rather than sleeps.

I validated the deferrable paths against a real workspace: Airflow 3.2.2, this branch's provider installed from source, a dedicated throwaway serverless warehouse (2X-Small), triggerer doing the waiting.

scenariotask stateevidence
deferrable start from STOPPEDdeferred -> successtrigger logged is STARTING; waiting for RUNNING, warehouse reached RUNNING
deferrable stopdeferred -> successwarehouse reached STOPPED
start against an already-RUNNING warehousesuccess, never deferredno DeferTask for the task
wait_for_termination=False stopsuccess, never deferredtransition still requested, warehouse stopped afterwards
timeout=1 startdeferred -> failedDatabricksWarehouseError: ... did not reach RUNNING within 1s; last state: STARTING.

So the behavioural claims in the description hold up where I could exercise them. Unit tests reproduce your numbers as well (43 + 14 + 13, and the full provider suite at 922 passed / 12 skipped).

Two things I would like settled before merge, both left inline:

  • The changelog entry is inserted under the 7.18.1 header, which is an already-released version, so a shipped release would advertise a feature it does not contain. Per providers/AGENTS.md and the NOTE TO CONTRIBUTORS block at the top of that file, routine feature entries are collected by the release manager from commit messages anyway — the five lines can just go.
  • The deferrable timeout is a duration recomputed inside every run(), so it restarts from zero on each triggerer restart or HA rebalance, while the synchronous path fails deterministically. DatabricksSQLStatementExecutionTrigger next door serializes an absolute end_time for exactly this reason. Either follow that pattern or state the per-run semantics explicitly — right now the docstring and the PR description claim the opposite of what the code does.

The rest is nits, also inline. One out-of-scope note worth being aware of rather than fixing here: these operators' first deferrable surface inherits #71525, so on core 3.0.x the trigger cannot fetch the connection at all (3.1.0+ is fine). Whether that caveat belongs in this page or centrally is a call for the docs discussion happening on #71667.

Comment threadproviders/databricks/docs/changelog.rst Outdated
@Vamsi-klu

Vamsi-klu commented Aug 19, 2026

Copy link
Copy Markdown
ContributorAuthor

Thanks — the Phase 1 contract is preserved carefully here, and the state-machine edge cases we went through on #70088 (stale STOPPED not terminal, deletion mid-wait, the deadline boundary) are all covered by deterministic tests rather than sleeps.

I validated the deferrable paths against a real workspace: Airflow 3.2.2, this branch's provider installed from source, a dedicated throwaway serverless warehouse (2X-Small), triggerer doing the waiting.

scenariotask stateevidence
deferrable start from STOPPEDdeferred -> successtrigger logged is STARTING; waiting for RUNNING, warehouse reached RUNNING
deferrable stopdeferred -> successwarehouse reached STOPPED
start against an already-RUNNING warehousesuccess, never deferredno DeferTask for the task
wait_for_termination=False stopsuccess, never deferredtransition still requested, warehouse stopped afterwards
timeout=1 startdeferred -> failedDatabricksWarehouseError: ... did not reach RUNNING within 1s; last state: STARTING.

So the behavioural claims in the description hold up where I could exercise them. Unit tests reproduce your numbers as well (43 + 14 + 13, and the full provider suite at 922 passed / 12 skipped).

Two things I would like settled before merge, both left inline:

  • The changelog entry is inserted under the 7.18.1 header, which is an already-released version, so a shipped release would advertise a feature it does not contain. Per providers/AGENTS.md and the NOTE TO CONTRIBUTORS block at the top of that file, routine feature entries are collected by the release manager from commit messages anyway — the five lines can just go.
  • The deferrable timeout is a duration recomputed inside every run(), so it restarts from zero on each triggerer restart or HA rebalance, while the synchronous path fails deterministically. DatabricksSQLStatementExecutionTrigger next door serializes an absolute end_time for exactly this reason. Either follow that pattern or state the per-run semantics explicitly — right now the docstring and the PR description claim the opposite of what the code does.

The rest is nits, also inline. One out-of-scope note worth being aware of rather than fixing here: these operators' first deferrable surface inherits #71525, so on core 3.0.x the trigger cannot fetch the connection at all (3.1.0+ is fine). Whether that caveat belongs in this page or centrally is a call for the docs discussion happening on #71667.

Thanks for the live workspace check @moomindani i responded to comments and commited the latest change

The follow-up is on the branch. Changelog Features under 7.18.1 is gone. The trigger now serializes an absolute end_time the same way DatabricksSQLStatementExecutionTrigger does. a_get_warehouse shares the GET path with get_warehouse. The on_kill override and the unpinned test are gone. The typed state JSON stays for sibling trigger symmetry, and the description now mentions the Phase 1 to_json restore.

The 3.0.x connection fetch caveat from 71525 is still out of scope here.

@moomindanimoomindani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — all five points are addressed, and I re-verified on 38bc535d4a rather than taking it on trust.

The changelog is back to the released 7.18.1 section untouched. The deadline is now an absolute end_time computed by the operator, serialized, and compared with time.time() in run(), matching DatabricksSQLStatementExecutionTrigger — and the two new tests pin it where it matters: the round trip preserves end_time, and a trigger resumed with an already-passed end_time times out immediately instead of opening a fresh window. That was the part I cared about most. on_kill and its vacuous test are gone with the rationale kept in the class docstring, and a_get_warehouse_state now goes through a_get_warehouse; the remaining duplicated path suffix matches how start_warehouse / stop_warehouse declare theirs in that file, so no objection from me. Keeping the typed state payload for sibling symmetry with the reasoning recorded is a fine call.

Real workspace re-run on this head (Airflow 3.2.2, provider installed from the branch, dedicated throwaway serverless warehouse): deferrable start deferred -> success, deferrable stop deferred -> success, and timeout=1deferred -> failed with did not reach RUNNING within 1s. 203 unit tests pass across the three files.

One observation from that last run, not a request: with a timeout shorter than the defer round-trip, the absolute deadline has already passed by the time the triggerer picks the trigger up, so it never polls and the message reports last state: unknown where the synchronous path would have polled once and named a state. That is the correct consequence of honouring an absolute deadline and is invisible at realistic timeouts — just noting that the boundary now differs slightly between the two paths.

LGTM.


Drafted-by: Claude Code (Opus 5); reviewed by @moomindani before posting

@Vamsi-klu

Copy link
Copy Markdown
ContributorAuthor

@eladkal as @moomindani approved the PR, Can i get your thoughts on this please?

@eladkal

Copy link
Copy Markdown
Contributor

can you please update the comment in #21377 (comment) with checklist to the actual steps in the plan and which PR solved which step? That way we can easily track what is left open

Vamsi-kluand others added 3 commits August 24, 2026 00:18
Start and stop waits from apache#70088 held a worker for the whole warehouse
lifecycle. This follow-up lands the agreed additive triggerer path so
those waits no longer occupy a worker slot.
Co-authored-by: Cursor <cursoragent@cursor.com>
A duration recomputed inside the trigger started a fresh wait after every triggerer restart or HA rebalance, so a warehouse that never reached its target could stay deferred well past the configured timeout.
Sphinx treats the HA term as a misspelling and fails the provider docs job.
Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor
cursorBotforce-pushed the follow-up-70088-warehouse-deferrable branch from 16dfb9e to 8f7d2d3CompareAugust 24, 2026 00:19
@Vamsi-klu

Vamsi-klu commented Aug 24, 2026

Copy link
Copy Markdown
ContributorAuthor

Hi @eladkal thanks a ton for the comment

Updated the tracking comment: #21377 (comment)

#70088 is Phase 1. This PR is the deferrable follow-up. Create/delete, name lookup, edit, and a live system-test Dag are still open.

@eladkal
eladkal merged commit c4e6ed3 into apache:mainAug 26, 2026
83 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@Vamsi-klu@eladkal@moomindani@cursoragent
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add deferrable mode to Databricks SQL warehouse operators - #71752

Merged
eladkal merged 3 commits into
apache:mainfrom
Vamsi-klu:follow-up-70088-warehouse-deferrable
Aug 26, 2026
Merged

Add deferrable mode to Databricks SQL warehouse operators#71752
eladkal merged 3 commits into
apache:mainfrom
Vamsi-klu:follow-up-70088-warehouse-deferrable

Conversation

@Vamsi-klu

@Vamsi-kluVamsi-klu commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

This adds an additive deferrable path for the Databricks SQL warehouse start and stop operators, so lifecycle waits can run on the triggerer instead of holding a worker.

related: #70088
related: #21377

Problem

Airflow's Databricks provider can start and stop an existing SQL warehouse, but Phase 1 waits hold a worker for the entire poll. Dag authors who want triggerer-based waiting currently have no deferrable path on these operators.

What changed

  • Add DatabricksHook.a_get_warehouse_state as the async GET mirror of get_warehouse_state.
  • Restore WarehouseState.to_json / from_json for trigger event round-trip.
  • Add DatabricksWarehouseStateTrigger next to the provider's existing triggers, with serialize, local monotonic timeout, retry_args validation, and a documented no-op on_kill.
  • Add deferrable on both DatabricksStartWarehouseOperator and DatabricksStopWarehouseOperator using the default_deferrable pattern.
  • Document the deferrable path in the warehouse how-to and add a provider changelog Features note.

No request-body workaround or new dependency is introduced. The implementation follows the Databricks SQL Warehouses API and keeps the Phase 1 wait contract.

Scope

This is the Phase 2 deferrable follow-up agreed on #70088: async hook, serialized trigger, operator completion path, and compatibility tests. Create/delete, edit, warehouse-by-name resolution, and a live Databricks system-test Dag remain outside this PR so the follow-up stays reviewable and independently useful.

Behavior and compatibility

  • deferrable defaults off and honors [operators] default_deferrable.
  • wait_for_termination, polling_period_seconds, and timeout are unchanged.
  • Starting an already RUNNING warehouse and stopping an already STOPPED warehouse remain no-ops and do not defer.
  • wait_for_termination=False still returns after requesting the transition and does not defer.
  • Existing STARTING/STOPPING transitions are reused instead of issuing duplicate requests, then deferred if waiting.
  • A start accepted while the warehouse still reports STOPPING continues on the triggerer; Databricks API transition rejections propagate unchanged.
  • Stale STOPPED while waiting for RUNNING is not terminal; the trigger keeps polling until RUNNING, deletion, or timeout.
  • Waiting uses time.monotonic() locally in the trigger, starts no new poll after the configured deadline, and still honors a target or deletion state returned by a poll that began before the deadline.
  • Clearing a deferred wait does not start or stop the warehouse: Databricks has no cancel API for these transitions.
  • execute_complete maps success / deleted / timeout to the same messages as Phase 1.

Validation

  • uv run --project providers/databricks pytest providers/databricks/tests/unit/databricks/operators/test_warehouse.py providers/databricks/tests/unit/databricks/hooks/test_databricks.py::TestWarehouseLifecycle providers/databricks/tests/unit/databricks/triggers/test_databricks.py::TestDatabricksWarehouseStateTrigger providers/databricks/tests/unit/databricks/triggers/test_databricks.py::test_trigger_init_rejects_non_serializable_retry_args — 70 passed.
  • prek run --from-ref upstream/main --stage pre-commit — passed.

Reviewer evidence

This PR has no UI surface, so before/after screenshots and browser validation are not applicable. The REST boundary is covered with an autospecced async GET assertion, operator deferral is covered with a specced hook, and trigger serialize/run/timeout/on_kill are covered with mocked warehouse state. No Databricks workspace credentials were used or required for these deterministic lifecycle tests.

The Phase 2 scope was posted on #70088 before this follow-up: #70088 (comment)


Was generative AI tooling used to co-author this PR?
  • Yes — Cursor Grok 4.6

Generated-by: Cursor Grok 4.6 following the guidelines

@Vamsi-klu

Vamsi-klu commented Aug 18, 2026

Copy link
Copy Markdown
ContributorAuthor

@eladkal@moomindani ni this is the #70088 deferrable follow-up, could you review when you have a chance?

@moomindanimoomindani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — the Phase 1 contract is preserved carefully here, and the state-machine edge cases we went through on #70088 (stale STOPPED not terminal, deletion mid-wait, the deadline boundary) are all covered by deterministic tests rather than sleeps.

I validated the deferrable paths against a real workspace: Airflow 3.2.2, this branch's provider installed from source, a dedicated throwaway serverless warehouse (2X-Small), triggerer doing the waiting.

scenariotask stateevidence
deferrable start from STOPPEDdeferred -> successtrigger logged is STARTING; waiting for RUNNING, warehouse reached RUNNING
deferrable stopdeferred -> successwarehouse reached STOPPED
start against an already-RUNNING warehousesuccess, never deferredno DeferTask for the task
wait_for_termination=False stopsuccess, never deferredtransition still requested, warehouse stopped afterwards
timeout=1 startdeferred -> failedDatabricksWarehouseError: ... did not reach RUNNING within 1s; last state: STARTING.

So the behavioural claims in the description hold up where I could exercise them. Unit tests reproduce your numbers as well (43 + 14 + 13, and the full provider suite at 922 passed / 12 skipped).

Two things I would like settled before merge, both left inline:

  • The changelog entry is inserted under the 7.18.1 header, which is an already-released version, so a shipped release would advertise a feature it does not contain. Per providers/AGENTS.md and the NOTE TO CONTRIBUTORS block at the top of that file, routine feature entries are collected by the release manager from commit messages anyway — the five lines can just go.
  • The deferrable timeout is a duration recomputed inside every run(), so it restarts from zero on each triggerer restart or HA rebalance, while the synchronous path fails deterministically. DatabricksSQLStatementExecutionTrigger next door serializes an absolute end_time for exactly this reason. Either follow that pattern or state the per-run semantics explicitly — right now the docstring and the PR description claim the opposite of what the code does.

The rest is nits, also inline. One out-of-scope note worth being aware of rather than fixing here: these operators' first deferrable surface inherits #71525, so on core 3.0.x the trigger cannot fetch the connection at all (3.1.0+ is fine). Whether that caveat belongs in this page or centrally is a call for the docs discussion happening on #71667.

Comment threadproviders/databricks/docs/changelog.rst Outdated
@Vamsi-klu

Vamsi-klu commented Aug 19, 2026

Copy link
Copy Markdown
ContributorAuthor

Thanks — the Phase 1 contract is preserved carefully here, and the state-machine edge cases we went through on #70088 (stale STOPPED not terminal, deletion mid-wait, the deadline boundary) are all covered by deterministic tests rather than sleeps.

I validated the deferrable paths against a real workspace: Airflow 3.2.2, this branch's provider installed from source, a dedicated throwaway serverless warehouse (2X-Small), triggerer doing the waiting.

scenariotask stateevidence
deferrable start from STOPPEDdeferred -> successtrigger logged is STARTING; waiting for RUNNING, warehouse reached RUNNING
deferrable stopdeferred -> successwarehouse reached STOPPED
start against an already-RUNNING warehousesuccess, never deferredno DeferTask for the task
wait_for_termination=False stopsuccess, never deferredtransition still requested, warehouse stopped afterwards
timeout=1 startdeferred -> failedDatabricksWarehouseError: ... did not reach RUNNING within 1s; last state: STARTING.

So the behavioural claims in the description hold up where I could exercise them. Unit tests reproduce your numbers as well (43 + 14 + 13, and the full provider suite at 922 passed / 12 skipped).

Two things I would like settled before merge, both left inline:

  • The changelog entry is inserted under the 7.18.1 header, which is an already-released version, so a shipped release would advertise a feature it does not contain. Per providers/AGENTS.md and the NOTE TO CONTRIBUTORS block at the top of that file, routine feature entries are collected by the release manager from commit messages anyway — the five lines can just go.
  • The deferrable timeout is a duration recomputed inside every run(), so it restarts from zero on each triggerer restart or HA rebalance, while the synchronous path fails deterministically. DatabricksSQLStatementExecutionTrigger next door serializes an absolute end_time for exactly this reason. Either follow that pattern or state the per-run semantics explicitly — right now the docstring and the PR description claim the opposite of what the code does.

The rest is nits, also inline. One out-of-scope note worth being aware of rather than fixing here: these operators' first deferrable surface inherits #71525, so on core 3.0.x the trigger cannot fetch the connection at all (3.1.0+ is fine). Whether that caveat belongs in this page or centrally is a call for the docs discussion happening on #71667.

Thanks for the live workspace check @moomindani i responded to comments and commited the latest change

The follow-up is on the branch. Changelog Features under 7.18.1 is gone. The trigger now serializes an absolute end_time the same way DatabricksSQLStatementExecutionTrigger does. a_get_warehouse shares the GET path with get_warehouse. The on_kill override and the unpinned test are gone. The typed state JSON stays for sibling trigger symmetry, and the description now mentions the Phase 1 to_json restore.

The 3.0.x connection fetch caveat from 71525 is still out of scope here.

@moomindanimoomindani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — all five points are addressed, and I re-verified on 38bc535d4a rather than taking it on trust.

The changelog is back to the released 7.18.1 section untouched. The deadline is now an absolute end_time computed by the operator, serialized, and compared with time.time() in run(), matching DatabricksSQLStatementExecutionTrigger — and the two new tests pin it where it matters: the round trip preserves end_time, and a trigger resumed with an already-passed end_time times out immediately instead of opening a fresh window. That was the part I cared about most. on_kill and its vacuous test are gone with the rationale kept in the class docstring, and a_get_warehouse_state now goes through a_get_warehouse; the remaining duplicated path suffix matches how start_warehouse / stop_warehouse declare theirs in that file, so no objection from me. Keeping the typed state payload for sibling symmetry with the reasoning recorded is a fine call.

Real workspace re-run on this head (Airflow 3.2.2, provider installed from the branch, dedicated throwaway serverless warehouse): deferrable start deferred -> success, deferrable stop deferred -> success, and timeout=1deferred -> failed with did not reach RUNNING within 1s. 203 unit tests pass across the three files.

One observation from that last run, not a request: with a timeout shorter than the defer round-trip, the absolute deadline has already passed by the time the triggerer picks the trigger up, so it never polls and the message reports last state: unknown where the synchronous path would have polled once and named a state. That is the correct consequence of honouring an absolute deadline and is invisible at realistic timeouts — just noting that the boundary now differs slightly between the two paths.

LGTM.


Drafted-by: Claude Code (Opus 5); reviewed by @moomindani before posting

@Vamsi-klu

Copy link
Copy Markdown
ContributorAuthor

@eladkal as @moomindani approved the PR, Can i get your thoughts on this please?

@eladkal

Copy link
Copy Markdown
Contributor

can you please update the comment in #21377 (comment) with checklist to the actual steps in the plan and which PR solved which step? That way we can easily track what is left open

Vamsi-kluand others added 3 commits August 24, 2026 00:18
Start and stop waits from apache#70088 held a worker for the whole warehouse
lifecycle. This follow-up lands the agreed additive triggerer path so
those waits no longer occupy a worker slot.
Co-authored-by: Cursor <cursoragent@cursor.com>
A duration recomputed inside the trigger started a fresh wait after every triggerer restart or HA rebalance, so a warehouse that never reached its target could stay deferred well past the configured timeout.
Sphinx treats the HA term as a misspelling and fails the provider docs job.
Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor
cursorBotforce-pushed the follow-up-70088-warehouse-deferrable branch from 16dfb9e to 8f7d2d3CompareAugust 24, 2026 00:19
@Vamsi-klu

Vamsi-klu commented Aug 24, 2026

Copy link
Copy Markdown
ContributorAuthor

Hi @eladkal thanks a ton for the comment

Updated the tracking comment: #21377 (comment)

#70088 is Phase 1. This PR is the deferrable follow-up. Create/delete, name lookup, edit, and a live system-test Dag are still open.

@eladkal
eladkal merged commit c4e6ed3 into apache:mainAug 26, 2026
83 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@Vamsi-klu@eladkal@moomindani@cursoragent
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add deferrable mode to Databricks SQL warehouse operators - #71752

Merged
eladkal merged 3 commits into
apache:mainfrom
Vamsi-klu:follow-up-70088-warehouse-deferrable
Aug 26, 2026
Merged

Add deferrable mode to Databricks SQL warehouse operators#71752
eladkal merged 3 commits into
apache:mainfrom
Vamsi-klu:follow-up-70088-warehouse-deferrable

Conversation

@Vamsi-klu

@Vamsi-kluVamsi-klu commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

This adds an additive deferrable path for the Databricks SQL warehouse start and stop operators, so lifecycle waits can run on the triggerer instead of holding a worker.

related: #70088
related: #21377

Problem

Airflow's Databricks provider can start and stop an existing SQL warehouse, but Phase 1 waits hold a worker for the entire poll. Dag authors who want triggerer-based waiting currently have no deferrable path on these operators.

What changed

  • Add DatabricksHook.a_get_warehouse_state as the async GET mirror of get_warehouse_state.
  • Restore WarehouseState.to_json / from_json for trigger event round-trip.
  • Add DatabricksWarehouseStateTrigger next to the provider's existing triggers, with serialize, local monotonic timeout, retry_args validation, and a documented no-op on_kill.
  • Add deferrable on both DatabricksStartWarehouseOperator and DatabricksStopWarehouseOperator using the default_deferrable pattern.
  • Document the deferrable path in the warehouse how-to and add a provider changelog Features note.

No request-body workaround or new dependency is introduced. The implementation follows the Databricks SQL Warehouses API and keeps the Phase 1 wait contract.

Scope

This is the Phase 2 deferrable follow-up agreed on #70088: async hook, serialized trigger, operator completion path, and compatibility tests. Create/delete, edit, warehouse-by-name resolution, and a live Databricks system-test Dag remain outside this PR so the follow-up stays reviewable and independently useful.

Behavior and compatibility

  • deferrable defaults off and honors [operators] default_deferrable.
  • wait_for_termination, polling_period_seconds, and timeout are unchanged.
  • Starting an already RUNNING warehouse and stopping an already STOPPED warehouse remain no-ops and do not defer.
  • wait_for_termination=False still returns after requesting the transition and does not defer.
  • Existing STARTING/STOPPING transitions are reused instead of issuing duplicate requests, then deferred if waiting.
  • A start accepted while the warehouse still reports STOPPING continues on the triggerer; Databricks API transition rejections propagate unchanged.
  • Stale STOPPED while waiting for RUNNING is not terminal; the trigger keeps polling until RUNNING, deletion, or timeout.
  • Waiting uses time.monotonic() locally in the trigger, starts no new poll after the configured deadline, and still honors a target or deletion state returned by a poll that began before the deadline.
  • Clearing a deferred wait does not start or stop the warehouse: Databricks has no cancel API for these transitions.
  • execute_complete maps success / deleted / timeout to the same messages as Phase 1.

Validation

  • uv run --project providers/databricks pytest providers/databricks/tests/unit/databricks/operators/test_warehouse.py providers/databricks/tests/unit/databricks/hooks/test_databricks.py::TestWarehouseLifecycle providers/databricks/tests/unit/databricks/triggers/test_databricks.py::TestDatabricksWarehouseStateTrigger providers/databricks/tests/unit/databricks/triggers/test_databricks.py::test_trigger_init_rejects_non_serializable_retry_args — 70 passed.
  • prek run --from-ref upstream/main --stage pre-commit — passed.

Reviewer evidence

This PR has no UI surface, so before/after screenshots and browser validation are not applicable. The REST boundary is covered with an autospecced async GET assertion, operator deferral is covered with a specced hook, and trigger serialize/run/timeout/on_kill are covered with mocked warehouse state. No Databricks workspace credentials were used or required for these deterministic lifecycle tests.

The Phase 2 scope was posted on #70088 before this follow-up: #70088 (comment)


Was generative AI tooling used to co-author this PR?
  • Yes — Cursor Grok 4.6

Generated-by: Cursor Grok 4.6 following the guidelines

@Vamsi-klu

Vamsi-klu commented Aug 18, 2026

Copy link
Copy Markdown
ContributorAuthor

@eladkal@moomindani ni this is the #70088 deferrable follow-up, could you review when you have a chance?

@moomindanimoomindani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — the Phase 1 contract is preserved carefully here, and the state-machine edge cases we went through on #70088 (stale STOPPED not terminal, deletion mid-wait, the deadline boundary) are all covered by deterministic tests rather than sleeps.

I validated the deferrable paths against a real workspace: Airflow 3.2.2, this branch's provider installed from source, a dedicated throwaway serverless warehouse (2X-Small), triggerer doing the waiting.

scenariotask stateevidence
deferrable start from STOPPEDdeferred -> successtrigger logged is STARTING; waiting for RUNNING, warehouse reached RUNNING
deferrable stopdeferred -> successwarehouse reached STOPPED
start against an already-RUNNING warehousesuccess, never deferredno DeferTask for the task
wait_for_termination=False stopsuccess, never deferredtransition still requested, warehouse stopped afterwards
timeout=1 startdeferred -> failedDatabricksWarehouseError: ... did not reach RUNNING within 1s; last state: STARTING.

So the behavioural claims in the description hold up where I could exercise them. Unit tests reproduce your numbers as well (43 + 14 + 13, and the full provider suite at 922 passed / 12 skipped).

Two things I would like settled before merge, both left inline:

  • The changelog entry is inserted under the 7.18.1 header, which is an already-released version, so a shipped release would advertise a feature it does not contain. Per providers/AGENTS.md and the NOTE TO CONTRIBUTORS block at the top of that file, routine feature entries are collected by the release manager from commit messages anyway — the five lines can just go.
  • The deferrable timeout is a duration recomputed inside every run(), so it restarts from zero on each triggerer restart or HA rebalance, while the synchronous path fails deterministically. DatabricksSQLStatementExecutionTrigger next door serializes an absolute end_time for exactly this reason. Either follow that pattern or state the per-run semantics explicitly — right now the docstring and the PR description claim the opposite of what the code does.

The rest is nits, also inline. One out-of-scope note worth being aware of rather than fixing here: these operators' first deferrable surface inherits #71525, so on core 3.0.x the trigger cannot fetch the connection at all (3.1.0+ is fine). Whether that caveat belongs in this page or centrally is a call for the docs discussion happening on #71667.

Comment threadproviders/databricks/docs/changelog.rst Outdated
@Vamsi-klu

Vamsi-klu commented Aug 19, 2026

Copy link
Copy Markdown
ContributorAuthor

Thanks — the Phase 1 contract is preserved carefully here, and the state-machine edge cases we went through on #70088 (stale STOPPED not terminal, deletion mid-wait, the deadline boundary) are all covered by deterministic tests rather than sleeps.

I validated the deferrable paths against a real workspace: Airflow 3.2.2, this branch's provider installed from source, a dedicated throwaway serverless warehouse (2X-Small), triggerer doing the waiting.

scenariotask stateevidence
deferrable start from STOPPEDdeferred -> successtrigger logged is STARTING; waiting for RUNNING, warehouse reached RUNNING
deferrable stopdeferred -> successwarehouse reached STOPPED
start against an already-RUNNING warehousesuccess, never deferredno DeferTask for the task
wait_for_termination=False stopsuccess, never deferredtransition still requested, warehouse stopped afterwards
timeout=1 startdeferred -> failedDatabricksWarehouseError: ... did not reach RUNNING within 1s; last state: STARTING.

So the behavioural claims in the description hold up where I could exercise them. Unit tests reproduce your numbers as well (43 + 14 + 13, and the full provider suite at 922 passed / 12 skipped).

Two things I would like settled before merge, both left inline:

  • The changelog entry is inserted under the 7.18.1 header, which is an already-released version, so a shipped release would advertise a feature it does not contain. Per providers/AGENTS.md and the NOTE TO CONTRIBUTORS block at the top of that file, routine feature entries are collected by the release manager from commit messages anyway — the five lines can just go.
  • The deferrable timeout is a duration recomputed inside every run(), so it restarts from zero on each triggerer restart or HA rebalance, while the synchronous path fails deterministically. DatabricksSQLStatementExecutionTrigger next door serializes an absolute end_time for exactly this reason. Either follow that pattern or state the per-run semantics explicitly — right now the docstring and the PR description claim the opposite of what the code does.

The rest is nits, also inline. One out-of-scope note worth being aware of rather than fixing here: these operators' first deferrable surface inherits #71525, so on core 3.0.x the trigger cannot fetch the connection at all (3.1.0+ is fine). Whether that caveat belongs in this page or centrally is a call for the docs discussion happening on #71667.

Thanks for the live workspace check @moomindani i responded to comments and commited the latest change

The follow-up is on the branch. Changelog Features under 7.18.1 is gone. The trigger now serializes an absolute end_time the same way DatabricksSQLStatementExecutionTrigger does. a_get_warehouse shares the GET path with get_warehouse. The on_kill override and the unpinned test are gone. The typed state JSON stays for sibling trigger symmetry, and the description now mentions the Phase 1 to_json restore.

The 3.0.x connection fetch caveat from 71525 is still out of scope here.

@moomindanimoomindani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — all five points are addressed, and I re-verified on 38bc535d4a rather than taking it on trust.

The changelog is back to the released 7.18.1 section untouched. The deadline is now an absolute end_time computed by the operator, serialized, and compared with time.time() in run(), matching DatabricksSQLStatementExecutionTrigger — and the two new tests pin it where it matters: the round trip preserves end_time, and a trigger resumed with an already-passed end_time times out immediately instead of opening a fresh window. That was the part I cared about most. on_kill and its vacuous test are gone with the rationale kept in the class docstring, and a_get_warehouse_state now goes through a_get_warehouse; the remaining duplicated path suffix matches how start_warehouse / stop_warehouse declare theirs in that file, so no objection from me. Keeping the typed state payload for sibling symmetry with the reasoning recorded is a fine call.

Real workspace re-run on this head (Airflow 3.2.2, provider installed from the branch, dedicated throwaway serverless warehouse): deferrable start deferred -> success, deferrable stop deferred -> success, and timeout=1deferred -> failed with did not reach RUNNING within 1s. 203 unit tests pass across the three files.

One observation from that last run, not a request: with a timeout shorter than the defer round-trip, the absolute deadline has already passed by the time the triggerer picks the trigger up, so it never polls and the message reports last state: unknown where the synchronous path would have polled once and named a state. That is the correct consequence of honouring an absolute deadline and is invisible at realistic timeouts — just noting that the boundary now differs slightly between the two paths.

LGTM.


Drafted-by: Claude Code (Opus 5); reviewed by @moomindani before posting

@Vamsi-klu

Copy link
Copy Markdown
ContributorAuthor

@eladkal as @moomindani approved the PR, Can i get your thoughts on this please?

@eladkal

Copy link
Copy Markdown
Contributor

can you please update the comment in #21377 (comment) with checklist to the actual steps in the plan and which PR solved which step? That way we can easily track what is left open

Vamsi-kluand others added 3 commits August 24, 2026 00:18
Start and stop waits from apache#70088 held a worker for the whole warehouse
lifecycle. This follow-up lands the agreed additive triggerer path so
those waits no longer occupy a worker slot.
Co-authored-by: Cursor <cursoragent@cursor.com>
A duration recomputed inside the trigger started a fresh wait after every triggerer restart or HA rebalance, so a warehouse that never reached its target could stay deferred well past the configured timeout.
Sphinx treats the HA term as a misspelling and fails the provider docs job.
Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor
cursorBotforce-pushed the follow-up-70088-warehouse-deferrable branch from 16dfb9e to 8f7d2d3CompareAugust 24, 2026 00:19
@Vamsi-klu

Vamsi-klu commented Aug 24, 2026

Copy link
Copy Markdown
ContributorAuthor

Hi @eladkal thanks a ton for the comment

Updated the tracking comment: #21377 (comment)

#70088 is Phase 1. This PR is the deferrable follow-up. Create/delete, name lookup, edit, and a live system-test Dag are still open.

@eladkal
eladkal merged commit c4e6ed3 into apache:mainAug 26, 2026
83 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@Vamsi-klu@eladkal@moomindani@cursoragent
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add deferrable mode to Databricks SQL warehouse operators - #71752

Merged
eladkal merged 3 commits into
apache:mainfrom
Vamsi-klu:follow-up-70088-warehouse-deferrable
Aug 26, 2026
Merged

Add deferrable mode to Databricks SQL warehouse operators#71752
eladkal merged 3 commits into
apache:mainfrom
Vamsi-klu:follow-up-70088-warehouse-deferrable

Conversation

@Vamsi-klu

@Vamsi-kluVamsi-klu commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

This adds an additive deferrable path for the Databricks SQL warehouse start and stop operators, so lifecycle waits can run on the triggerer instead of holding a worker.

related: #70088
related: #21377

Problem

Airflow's Databricks provider can start and stop an existing SQL warehouse, but Phase 1 waits hold a worker for the entire poll. Dag authors who want triggerer-based waiting currently have no deferrable path on these operators.

What changed

  • Add DatabricksHook.a_get_warehouse_state as the async GET mirror of get_warehouse_state.
  • Restore WarehouseState.to_json / from_json for trigger event round-trip.
  • Add DatabricksWarehouseStateTrigger next to the provider's existing triggers, with serialize, local monotonic timeout, retry_args validation, and a documented no-op on_kill.
  • Add deferrable on both DatabricksStartWarehouseOperator and DatabricksStopWarehouseOperator using the default_deferrable pattern.
  • Document the deferrable path in the warehouse how-to and add a provider changelog Features note.

No request-body workaround or new dependency is introduced. The implementation follows the Databricks SQL Warehouses API and keeps the Phase 1 wait contract.

Scope

This is the Phase 2 deferrable follow-up agreed on #70088: async hook, serialized trigger, operator completion path, and compatibility tests. Create/delete, edit, warehouse-by-name resolution, and a live Databricks system-test Dag remain outside this PR so the follow-up stays reviewable and independently useful.

Behavior and compatibility

  • deferrable defaults off and honors [operators] default_deferrable.
  • wait_for_termination, polling_period_seconds, and timeout are unchanged.
  • Starting an already RUNNING warehouse and stopping an already STOPPED warehouse remain no-ops and do not defer.
  • wait_for_termination=False still returns after requesting the transition and does not defer.
  • Existing STARTING/STOPPING transitions are reused instead of issuing duplicate requests, then deferred if waiting.
  • A start accepted while the warehouse still reports STOPPING continues on the triggerer; Databricks API transition rejections propagate unchanged.
  • Stale STOPPED while waiting for RUNNING is not terminal; the trigger keeps polling until RUNNING, deletion, or timeout.
  • Waiting uses time.monotonic() locally in the trigger, starts no new poll after the configured deadline, and still honors a target or deletion state returned by a poll that began before the deadline.
  • Clearing a deferred wait does not start or stop the warehouse: Databricks has no cancel API for these transitions.
  • execute_complete maps success / deleted / timeout to the same messages as Phase 1.

Validation

  • uv run --project providers/databricks pytest providers/databricks/tests/unit/databricks/operators/test_warehouse.py providers/databricks/tests/unit/databricks/hooks/test_databricks.py::TestWarehouseLifecycle providers/databricks/tests/unit/databricks/triggers/test_databricks.py::TestDatabricksWarehouseStateTrigger providers/databricks/tests/unit/databricks/triggers/test_databricks.py::test_trigger_init_rejects_non_serializable_retry_args — 70 passed.
  • prek run --from-ref upstream/main --stage pre-commit — passed.

Reviewer evidence

This PR has no UI surface, so before/after screenshots and browser validation are not applicable. The REST boundary is covered with an autospecced async GET assertion, operator deferral is covered with a specced hook, and trigger serialize/run/timeout/on_kill are covered with mocked warehouse state. No Databricks workspace credentials were used or required for these deterministic lifecycle tests.

The Phase 2 scope was posted on #70088 before this follow-up: #70088 (comment)


Was generative AI tooling used to co-author this PR?
  • Yes — Cursor Grok 4.6

Generated-by: Cursor Grok 4.6 following the guidelines

@Vamsi-klu

Vamsi-klu commented Aug 18, 2026

Copy link
Copy Markdown
ContributorAuthor

@eladkal@moomindani ni this is the #70088 deferrable follow-up, could you review when you have a chance?

@moomindanimoomindani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — the Phase 1 contract is preserved carefully here, and the state-machine edge cases we went through on #70088 (stale STOPPED not terminal, deletion mid-wait, the deadline boundary) are all covered by deterministic tests rather than sleeps.

I validated the deferrable paths against a real workspace: Airflow 3.2.2, this branch's provider installed from source, a dedicated throwaway serverless warehouse (2X-Small), triggerer doing the waiting.

scenariotask stateevidence
deferrable start from STOPPEDdeferred -> successtrigger logged is STARTING; waiting for RUNNING, warehouse reached RUNNING
deferrable stopdeferred -> successwarehouse reached STOPPED
start against an already-RUNNING warehousesuccess, never deferredno DeferTask for the task
wait_for_termination=False stopsuccess, never deferredtransition still requested, warehouse stopped afterwards
timeout=1 startdeferred -> failedDatabricksWarehouseError: ... did not reach RUNNING within 1s; last state: STARTING.

So the behavioural claims in the description hold up where I could exercise them. Unit tests reproduce your numbers as well (43 + 14 + 13, and the full provider suite at 922 passed / 12 skipped).

Two things I would like settled before merge, both left inline:

  • The changelog entry is inserted under the 7.18.1 header, which is an already-released version, so a shipped release would advertise a feature it does not contain. Per providers/AGENTS.md and the NOTE TO CONTRIBUTORS block at the top of that file, routine feature entries are collected by the release manager from commit messages anyway — the five lines can just go.
  • The deferrable timeout is a duration recomputed inside every run(), so it restarts from zero on each triggerer restart or HA rebalance, while the synchronous path fails deterministically. DatabricksSQLStatementExecutionTrigger next door serializes an absolute end_time for exactly this reason. Either follow that pattern or state the per-run semantics explicitly — right now the docstring and the PR description claim the opposite of what the code does.

The rest is nits, also inline. One out-of-scope note worth being aware of rather than fixing here: these operators' first deferrable surface inherits #71525, so on core 3.0.x the trigger cannot fetch the connection at all (3.1.0+ is fine). Whether that caveat belongs in this page or centrally is a call for the docs discussion happening on #71667.

Comment threadproviders/databricks/docs/changelog.rst Outdated
@Vamsi-klu

Vamsi-klu commented Aug 19, 2026

Copy link
Copy Markdown
ContributorAuthor

Thanks — the Phase 1 contract is preserved carefully here, and the state-machine edge cases we went through on #70088 (stale STOPPED not terminal, deletion mid-wait, the deadline boundary) are all covered by deterministic tests rather than sleeps.

I validated the deferrable paths against a real workspace: Airflow 3.2.2, this branch's provider installed from source, a dedicated throwaway serverless warehouse (2X-Small), triggerer doing the waiting.

scenariotask stateevidence
deferrable start from STOPPEDdeferred -> successtrigger logged is STARTING; waiting for RUNNING, warehouse reached RUNNING
deferrable stopdeferred -> successwarehouse reached STOPPED
start against an already-RUNNING warehousesuccess, never deferredno DeferTask for the task
wait_for_termination=False stopsuccess, never deferredtransition still requested, warehouse stopped afterwards
timeout=1 startdeferred -> failedDatabricksWarehouseError: ... did not reach RUNNING within 1s; last state: STARTING.

So the behavioural claims in the description hold up where I could exercise them. Unit tests reproduce your numbers as well (43 + 14 + 13, and the full provider suite at 922 passed / 12 skipped).

Two things I would like settled before merge, both left inline:

  • The changelog entry is inserted under the 7.18.1 header, which is an already-released version, so a shipped release would advertise a feature it does not contain. Per providers/AGENTS.md and the NOTE TO CONTRIBUTORS block at the top of that file, routine feature entries are collected by the release manager from commit messages anyway — the five lines can just go.
  • The deferrable timeout is a duration recomputed inside every run(), so it restarts from zero on each triggerer restart or HA rebalance, while the synchronous path fails deterministically. DatabricksSQLStatementExecutionTrigger next door serializes an absolute end_time for exactly this reason. Either follow that pattern or state the per-run semantics explicitly — right now the docstring and the PR description claim the opposite of what the code does.

The rest is nits, also inline. One out-of-scope note worth being aware of rather than fixing here: these operators' first deferrable surface inherits #71525, so on core 3.0.x the trigger cannot fetch the connection at all (3.1.0+ is fine). Whether that caveat belongs in this page or centrally is a call for the docs discussion happening on #71667.

Thanks for the live workspace check @moomindani i responded to comments and commited the latest change

The follow-up is on the branch. Changelog Features under 7.18.1 is gone. The trigger now serializes an absolute end_time the same way DatabricksSQLStatementExecutionTrigger does. a_get_warehouse shares the GET path with get_warehouse. The on_kill override and the unpinned test are gone. The typed state JSON stays for sibling trigger symmetry, and the description now mentions the Phase 1 to_json restore.

The 3.0.x connection fetch caveat from 71525 is still out of scope here.

@moomindanimoomindani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — all five points are addressed, and I re-verified on 38bc535d4a rather than taking it on trust.

The changelog is back to the released 7.18.1 section untouched. The deadline is now an absolute end_time computed by the operator, serialized, and compared with time.time() in run(), matching DatabricksSQLStatementExecutionTrigger — and the two new tests pin it where it matters: the round trip preserves end_time, and a trigger resumed with an already-passed end_time times out immediately instead of opening a fresh window. That was the part I cared about most. on_kill and its vacuous test are gone with the rationale kept in the class docstring, and a_get_warehouse_state now goes through a_get_warehouse; the remaining duplicated path suffix matches how start_warehouse / stop_warehouse declare theirs in that file, so no objection from me. Keeping the typed state payload for sibling symmetry with the reasoning recorded is a fine call.

Real workspace re-run on this head (Airflow 3.2.2, provider installed from the branch, dedicated throwaway serverless warehouse): deferrable start deferred -> success, deferrable stop deferred -> success, and timeout=1deferred -> failed with did not reach RUNNING within 1s. 203 unit tests pass across the three files.

One observation from that last run, not a request: with a timeout shorter than the defer round-trip, the absolute deadline has already passed by the time the triggerer picks the trigger up, so it never polls and the message reports last state: unknown where the synchronous path would have polled once and named a state. That is the correct consequence of honouring an absolute deadline and is invisible at realistic timeouts — just noting that the boundary now differs slightly between the two paths.

LGTM.


Drafted-by: Claude Code (Opus 5); reviewed by @moomindani before posting

@Vamsi-klu

Copy link
Copy Markdown
ContributorAuthor

@eladkal as @moomindani approved the PR, Can i get your thoughts on this please?

@eladkal

Copy link
Copy Markdown
Contributor

can you please update the comment in #21377 (comment) with checklist to the actual steps in the plan and which PR solved which step? That way we can easily track what is left open

Vamsi-kluand others added 3 commits August 24, 2026 00:18
Start and stop waits from apache#70088 held a worker for the whole warehouse
lifecycle. This follow-up lands the agreed additive triggerer path so
those waits no longer occupy a worker slot.
Co-authored-by: Cursor <cursoragent@cursor.com>
A duration recomputed inside the trigger started a fresh wait after every triggerer restart or HA rebalance, so a warehouse that never reached its target could stay deferred well past the configured timeout.
Sphinx treats the HA term as a misspelling and fails the provider docs job.
Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor
cursorBotforce-pushed the follow-up-70088-warehouse-deferrable branch from 16dfb9e to 8f7d2d3CompareAugust 24, 2026 00:19
@Vamsi-klu

Vamsi-klu commented Aug 24, 2026

Copy link
Copy Markdown
ContributorAuthor

Hi @eladkal thanks a ton for the comment

Updated the tracking comment: #21377 (comment)

#70088 is Phase 1. This PR is the deferrable follow-up. Create/delete, name lookup, edit, and a live system-test Dag are still open.

@eladkal
eladkal merged commit c4e6ed3 into apache:mainAug 26, 2026
83 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@Vamsi-klu@eladkal@moomindani@cursoragent
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add deferrable mode to Databricks SQL warehouse operators - #71752

Merged
eladkal merged 3 commits into
apache:mainfrom
Vamsi-klu:follow-up-70088-warehouse-deferrable
Aug 26, 2026
Merged

Add deferrable mode to Databricks SQL warehouse operators#71752
eladkal merged 3 commits into
apache:mainfrom
Vamsi-klu:follow-up-70088-warehouse-deferrable

Conversation

@Vamsi-klu

@Vamsi-kluVamsi-klu commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

This adds an additive deferrable path for the Databricks SQL warehouse start and stop operators, so lifecycle waits can run on the triggerer instead of holding a worker.

related: #70088
related: #21377

Problem

Airflow's Databricks provider can start and stop an existing SQL warehouse, but Phase 1 waits hold a worker for the entire poll. Dag authors who want triggerer-based waiting currently have no deferrable path on these operators.

What changed

  • Add DatabricksHook.a_get_warehouse_state as the async GET mirror of get_warehouse_state.
  • Restore WarehouseState.to_json / from_json for trigger event round-trip.
  • Add DatabricksWarehouseStateTrigger next to the provider's existing triggers, with serialize, local monotonic timeout, retry_args validation, and a documented no-op on_kill.
  • Add deferrable on both DatabricksStartWarehouseOperator and DatabricksStopWarehouseOperator using the default_deferrable pattern.
  • Document the deferrable path in the warehouse how-to and add a provider changelog Features note.

No request-body workaround or new dependency is introduced. The implementation follows the Databricks SQL Warehouses API and keeps the Phase 1 wait contract.

Scope

This is the Phase 2 deferrable follow-up agreed on #70088: async hook, serialized trigger, operator completion path, and compatibility tests. Create/delete, edit, warehouse-by-name resolution, and a live Databricks system-test Dag remain outside this PR so the follow-up stays reviewable and independently useful.

Behavior and compatibility

  • deferrable defaults off and honors [operators] default_deferrable.
  • wait_for_termination, polling_period_seconds, and timeout are unchanged.
  • Starting an already RUNNING warehouse and stopping an already STOPPED warehouse remain no-ops and do not defer.
  • wait_for_termination=False still returns after requesting the transition and does not defer.
  • Existing STARTING/STOPPING transitions are reused instead of issuing duplicate requests, then deferred if waiting.
  • A start accepted while the warehouse still reports STOPPING continues on the triggerer; Databricks API transition rejections propagate unchanged.
  • Stale STOPPED while waiting for RUNNING is not terminal; the trigger keeps polling until RUNNING, deletion, or timeout.
  • Waiting uses time.monotonic() locally in the trigger, starts no new poll after the configured deadline, and still honors a target or deletion state returned by a poll that began before the deadline.
  • Clearing a deferred wait does not start or stop the warehouse: Databricks has no cancel API for these transitions.
  • execute_complete maps success / deleted / timeout to the same messages as Phase 1.

Validation

  • uv run --project providers/databricks pytest providers/databricks/tests/unit/databricks/operators/test_warehouse.py providers/databricks/tests/unit/databricks/hooks/test_databricks.py::TestWarehouseLifecycle providers/databricks/tests/unit/databricks/triggers/test_databricks.py::TestDatabricksWarehouseStateTrigger providers/databricks/tests/unit/databricks/triggers/test_databricks.py::test_trigger_init_rejects_non_serializable_retry_args — 70 passed.
  • prek run --from-ref upstream/main --stage pre-commit — passed.

Reviewer evidence

This PR has no UI surface, so before/after screenshots and browser validation are not applicable. The REST boundary is covered with an autospecced async GET assertion, operator deferral is covered with a specced hook, and trigger serialize/run/timeout/on_kill are covered with mocked warehouse state. No Databricks workspace credentials were used or required for these deterministic lifecycle tests.

The Phase 2 scope was posted on #70088 before this follow-up: #70088 (comment)


Was generative AI tooling used to co-author this PR?
  • Yes — Cursor Grok 4.6

Generated-by: Cursor Grok 4.6 following the guidelines

@Vamsi-klu

Vamsi-klu commented Aug 18, 2026

Copy link
Copy Markdown
ContributorAuthor

@eladkal@moomindani ni this is the #70088 deferrable follow-up, could you review when you have a chance?

@moomindanimoomindani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — the Phase 1 contract is preserved carefully here, and the state-machine edge cases we went through on #70088 (stale STOPPED not terminal, deletion mid-wait, the deadline boundary) are all covered by deterministic tests rather than sleeps.

I validated the deferrable paths against a real workspace: Airflow 3.2.2, this branch's provider installed from source, a dedicated throwaway serverless warehouse (2X-Small), triggerer doing the waiting.

scenariotask stateevidence
deferrable start from STOPPEDdeferred -> successtrigger logged is STARTING; waiting for RUNNING, warehouse reached RUNNING
deferrable stopdeferred -> successwarehouse reached STOPPED
start against an already-RUNNING warehousesuccess, never deferredno DeferTask for the task
wait_for_termination=False stopsuccess, never deferredtransition still requested, warehouse stopped afterwards
timeout=1 startdeferred -> failedDatabricksWarehouseError: ... did not reach RUNNING within 1s; last state: STARTING.

So the behavioural claims in the description hold up where I could exercise them. Unit tests reproduce your numbers as well (43 + 14 + 13, and the full provider suite at 922 passed / 12 skipped).

Two things I would like settled before merge, both left inline:

  • The changelog entry is inserted under the 7.18.1 header, which is an already-released version, so a shipped release would advertise a feature it does not contain. Per providers/AGENTS.md and the NOTE TO CONTRIBUTORS block at the top of that file, routine feature entries are collected by the release manager from commit messages anyway — the five lines can just go.
  • The deferrable timeout is a duration recomputed inside every run(), so it restarts from zero on each triggerer restart or HA rebalance, while the synchronous path fails deterministically. DatabricksSQLStatementExecutionTrigger next door serializes an absolute end_time for exactly this reason. Either follow that pattern or state the per-run semantics explicitly — right now the docstring and the PR description claim the opposite of what the code does.

The rest is nits, also inline. One out-of-scope note worth being aware of rather than fixing here: these operators' first deferrable surface inherits #71525, so on core 3.0.x the trigger cannot fetch the connection at all (3.1.0+ is fine). Whether that caveat belongs in this page or centrally is a call for the docs discussion happening on #71667.

Comment threadproviders/databricks/docs/changelog.rst Outdated
@Vamsi-klu

Vamsi-klu commented Aug 19, 2026

Copy link
Copy Markdown
ContributorAuthor

Thanks — the Phase 1 contract is preserved carefully here, and the state-machine edge cases we went through on #70088 (stale STOPPED not terminal, deletion mid-wait, the deadline boundary) are all covered by deterministic tests rather than sleeps.

I validated the deferrable paths against a real workspace: Airflow 3.2.2, this branch's provider installed from source, a dedicated throwaway serverless warehouse (2X-Small), triggerer doing the waiting.

scenariotask stateevidence
deferrable start from STOPPEDdeferred -> successtrigger logged is STARTING; waiting for RUNNING, warehouse reached RUNNING
deferrable stopdeferred -> successwarehouse reached STOPPED
start against an already-RUNNING warehousesuccess, never deferredno DeferTask for the task
wait_for_termination=False stopsuccess, never deferredtransition still requested, warehouse stopped afterwards
timeout=1 startdeferred -> failedDatabricksWarehouseError: ... did not reach RUNNING within 1s; last state: STARTING.

So the behavioural claims in the description hold up where I could exercise them. Unit tests reproduce your numbers as well (43 + 14 + 13, and the full provider suite at 922 passed / 12 skipped).

Two things I would like settled before merge, both left inline:

  • The changelog entry is inserted under the 7.18.1 header, which is an already-released version, so a shipped release would advertise a feature it does not contain. Per providers/AGENTS.md and the NOTE TO CONTRIBUTORS block at the top of that file, routine feature entries are collected by the release manager from commit messages anyway — the five lines can just go.
  • The deferrable timeout is a duration recomputed inside every run(), so it restarts from zero on each triggerer restart or HA rebalance, while the synchronous path fails deterministically. DatabricksSQLStatementExecutionTrigger next door serializes an absolute end_time for exactly this reason. Either follow that pattern or state the per-run semantics explicitly — right now the docstring and the PR description claim the opposite of what the code does.

The rest is nits, also inline. One out-of-scope note worth being aware of rather than fixing here: these operators' first deferrable surface inherits #71525, so on core 3.0.x the trigger cannot fetch the connection at all (3.1.0+ is fine). Whether that caveat belongs in this page or centrally is a call for the docs discussion happening on #71667.

Thanks for the live workspace check @moomindani i responded to comments and commited the latest change

The follow-up is on the branch. Changelog Features under 7.18.1 is gone. The trigger now serializes an absolute end_time the same way DatabricksSQLStatementExecutionTrigger does. a_get_warehouse shares the GET path with get_warehouse. The on_kill override and the unpinned test are gone. The typed state JSON stays for sibling trigger symmetry, and the description now mentions the Phase 1 to_json restore.

The 3.0.x connection fetch caveat from 71525 is still out of scope here.

@moomindanimoomindani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — all five points are addressed, and I re-verified on 38bc535d4a rather than taking it on trust.

The changelog is back to the released 7.18.1 section untouched. The deadline is now an absolute end_time computed by the operator, serialized, and compared with time.time() in run(), matching DatabricksSQLStatementExecutionTrigger — and the two new tests pin it where it matters: the round trip preserves end_time, and a trigger resumed with an already-passed end_time times out immediately instead of opening a fresh window. That was the part I cared about most. on_kill and its vacuous test are gone with the rationale kept in the class docstring, and a_get_warehouse_state now goes through a_get_warehouse; the remaining duplicated path suffix matches how start_warehouse / stop_warehouse declare theirs in that file, so no objection from me. Keeping the typed state payload for sibling symmetry with the reasoning recorded is a fine call.

Real workspace re-run on this head (Airflow 3.2.2, provider installed from the branch, dedicated throwaway serverless warehouse): deferrable start deferred -> success, deferrable stop deferred -> success, and timeout=1deferred -> failed with did not reach RUNNING within 1s. 203 unit tests pass across the three files.

One observation from that last run, not a request: with a timeout shorter than the defer round-trip, the absolute deadline has already passed by the time the triggerer picks the trigger up, so it never polls and the message reports last state: unknown where the synchronous path would have polled once and named a state. That is the correct consequence of honouring an absolute deadline and is invisible at realistic timeouts — just noting that the boundary now differs slightly between the two paths.

LGTM.


Drafted-by: Claude Code (Opus 5); reviewed by @moomindani before posting

@Vamsi-klu

Copy link
Copy Markdown
ContributorAuthor

@eladkal as @moomindani approved the PR, Can i get your thoughts on this please?

@eladkal

Copy link
Copy Markdown
Contributor

can you please update the comment in #21377 (comment) with checklist to the actual steps in the plan and which PR solved which step? That way we can easily track what is left open

Vamsi-kluand others added 3 commits August 24, 2026 00:18
Start and stop waits from apache#70088 held a worker for the whole warehouse
lifecycle. This follow-up lands the agreed additive triggerer path so
those waits no longer occupy a worker slot.
Co-authored-by: Cursor <cursoragent@cursor.com>
A duration recomputed inside the trigger started a fresh wait after every triggerer restart or HA rebalance, so a warehouse that never reached its target could stay deferred well past the configured timeout.
Sphinx treats the HA term as a misspelling and fails the provider docs job.
Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor
cursorBotforce-pushed the follow-up-70088-warehouse-deferrable branch from 16dfb9e to 8f7d2d3CompareAugust 24, 2026 00:19
@Vamsi-klu

Vamsi-klu commented Aug 24, 2026

Copy link
Copy Markdown
ContributorAuthor

Hi @eladkal thanks a ton for the comment

Updated the tracking comment: #21377 (comment)

#70088 is Phase 1. This PR is the deferrable follow-up. Create/delete, name lookup, edit, and a live system-test Dag are still open.

@eladkal
eladkal merged commit c4e6ed3 into apache:mainAug 26, 2026
83 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@Vamsi-klu@eladkal@moomindani@cursoragent
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add deferrable mode to Databricks SQL warehouse operators - #71752

Merged
eladkal merged 3 commits into
apache:mainfrom
Vamsi-klu:follow-up-70088-warehouse-deferrable
Aug 26, 2026
Merged

Add deferrable mode to Databricks SQL warehouse operators#71752
eladkal merged 3 commits into
apache:mainfrom
Vamsi-klu:follow-up-70088-warehouse-deferrable

Conversation

@Vamsi-klu

@Vamsi-kluVamsi-klu commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

This adds an additive deferrable path for the Databricks SQL warehouse start and stop operators, so lifecycle waits can run on the triggerer instead of holding a worker.

related: #70088
related: #21377

Problem

Airflow's Databricks provider can start and stop an existing SQL warehouse, but Phase 1 waits hold a worker for the entire poll. Dag authors who want triggerer-based waiting currently have no deferrable path on these operators.

What changed

  • Add DatabricksHook.a_get_warehouse_state as the async GET mirror of get_warehouse_state.
  • Restore WarehouseState.to_json / from_json for trigger event round-trip.
  • Add DatabricksWarehouseStateTrigger next to the provider's existing triggers, with serialize, local monotonic timeout, retry_args validation, and a documented no-op on_kill.
  • Add deferrable on both DatabricksStartWarehouseOperator and DatabricksStopWarehouseOperator using the default_deferrable pattern.
  • Document the deferrable path in the warehouse how-to and add a provider changelog Features note.

No request-body workaround or new dependency is introduced. The implementation follows the Databricks SQL Warehouses API and keeps the Phase 1 wait contract.

Scope

This is the Phase 2 deferrable follow-up agreed on #70088: async hook, serialized trigger, operator completion path, and compatibility tests. Create/delete, edit, warehouse-by-name resolution, and a live Databricks system-test Dag remain outside this PR so the follow-up stays reviewable and independently useful.

Behavior and compatibility

  • deferrable defaults off and honors [operators] default_deferrable.
  • wait_for_termination, polling_period_seconds, and timeout are unchanged.
  • Starting an already RUNNING warehouse and stopping an already STOPPED warehouse remain no-ops and do not defer.
  • wait_for_termination=False still returns after requesting the transition and does not defer.
  • Existing STARTING/STOPPING transitions are reused instead of issuing duplicate requests, then deferred if waiting.
  • A start accepted while the warehouse still reports STOPPING continues on the triggerer; Databricks API transition rejections propagate unchanged.
  • Stale STOPPED while waiting for RUNNING is not terminal; the trigger keeps polling until RUNNING, deletion, or timeout.
  • Waiting uses time.monotonic() locally in the trigger, starts no new poll after the configured deadline, and still honors a target or deletion state returned by a poll that began before the deadline.
  • Clearing a deferred wait does not start or stop the warehouse: Databricks has no cancel API for these transitions.
  • execute_complete maps success / deleted / timeout to the same messages as Phase 1.

Validation

  • uv run --project providers/databricks pytest providers/databricks/tests/unit/databricks/operators/test_warehouse.py providers/databricks/tests/unit/databricks/hooks/test_databricks.py::TestWarehouseLifecycle providers/databricks/tests/unit/databricks/triggers/test_databricks.py::TestDatabricksWarehouseStateTrigger providers/databricks/tests/unit/databricks/triggers/test_databricks.py::test_trigger_init_rejects_non_serializable_retry_args — 70 passed.
  • prek run --from-ref upstream/main --stage pre-commit — passed.

Reviewer evidence

This PR has no UI surface, so before/after screenshots and browser validation are not applicable. The REST boundary is covered with an autospecced async GET assertion, operator deferral is covered with a specced hook, and trigger serialize/run/timeout/on_kill are covered with mocked warehouse state. No Databricks workspace credentials were used or required for these deterministic lifecycle tests.

The Phase 2 scope was posted on #70088 before this follow-up: #70088 (comment)


Was generative AI tooling used to co-author this PR?
  • Yes — Cursor Grok 4.6

Generated-by: Cursor Grok 4.6 following the guidelines

@Vamsi-klu

Vamsi-klu commented Aug 18, 2026

Copy link
Copy Markdown
ContributorAuthor

@eladkal@moomindani ni this is the #70088 deferrable follow-up, could you review when you have a chance?

@moomindanimoomindani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — the Phase 1 contract is preserved carefully here, and the state-machine edge cases we went through on #70088 (stale STOPPED not terminal, deletion mid-wait, the deadline boundary) are all covered by deterministic tests rather than sleeps.

I validated the deferrable paths against a real workspace: Airflow 3.2.2, this branch's provider installed from source, a dedicated throwaway serverless warehouse (2X-Small), triggerer doing the waiting.

scenariotask stateevidence
deferrable start from STOPPEDdeferred -> successtrigger logged is STARTING; waiting for RUNNING, warehouse reached RUNNING
deferrable stopdeferred -> successwarehouse reached STOPPED
start against an already-RUNNING warehousesuccess, never deferredno DeferTask for the task
wait_for_termination=False stopsuccess, never deferredtransition still requested, warehouse stopped afterwards
timeout=1 startdeferred -> failedDatabricksWarehouseError: ... did not reach RUNNING within 1s; last state: STARTING.

So the behavioural claims in the description hold up where I could exercise them. Unit tests reproduce your numbers as well (43 + 14 + 13, and the full provider suite at 922 passed / 12 skipped).

Two things I would like settled before merge, both left inline:

  • The changelog entry is inserted under the 7.18.1 header, which is an already-released version, so a shipped release would advertise a feature it does not contain. Per providers/AGENTS.md and the NOTE TO CONTRIBUTORS block at the top of that file, routine feature entries are collected by the release manager from commit messages anyway — the five lines can just go.
  • The deferrable timeout is a duration recomputed inside every run(), so it restarts from zero on each triggerer restart or HA rebalance, while the synchronous path fails deterministically. DatabricksSQLStatementExecutionTrigger next door serializes an absolute end_time for exactly this reason. Either follow that pattern or state the per-run semantics explicitly — right now the docstring and the PR description claim the opposite of what the code does.

The rest is nits, also inline. One out-of-scope note worth being aware of rather than fixing here: these operators' first deferrable surface inherits #71525, so on core 3.0.x the trigger cannot fetch the connection at all (3.1.0+ is fine). Whether that caveat belongs in this page or centrally is a call for the docs discussion happening on #71667.

Comment threadproviders/databricks/docs/changelog.rst Outdated
@Vamsi-klu

Vamsi-klu commented Aug 19, 2026

Copy link
Copy Markdown
ContributorAuthor

Thanks — the Phase 1 contract is preserved carefully here, and the state-machine edge cases we went through on #70088 (stale STOPPED not terminal, deletion mid-wait, the deadline boundary) are all covered by deterministic tests rather than sleeps.

I validated the deferrable paths against a real workspace: Airflow 3.2.2, this branch's provider installed from source, a dedicated throwaway serverless warehouse (2X-Small), triggerer doing the waiting.

scenariotask stateevidence
deferrable start from STOPPEDdeferred -> successtrigger logged is STARTING; waiting for RUNNING, warehouse reached RUNNING
deferrable stopdeferred -> successwarehouse reached STOPPED
start against an already-RUNNING warehousesuccess, never deferredno DeferTask for the task
wait_for_termination=False stopsuccess, never deferredtransition still requested, warehouse stopped afterwards
timeout=1 startdeferred -> failedDatabricksWarehouseError: ... did not reach RUNNING within 1s; last state: STARTING.

So the behavioural claims in the description hold up where I could exercise them. Unit tests reproduce your numbers as well (43 + 14 + 13, and the full provider suite at 922 passed / 12 skipped).

Two things I would like settled before merge, both left inline:

  • The changelog entry is inserted under the 7.18.1 header, which is an already-released version, so a shipped release would advertise a feature it does not contain. Per providers/AGENTS.md and the NOTE TO CONTRIBUTORS block at the top of that file, routine feature entries are collected by the release manager from commit messages anyway — the five lines can just go.
  • The deferrable timeout is a duration recomputed inside every run(), so it restarts from zero on each triggerer restart or HA rebalance, while the synchronous path fails deterministically. DatabricksSQLStatementExecutionTrigger next door serializes an absolute end_time for exactly this reason. Either follow that pattern or state the per-run semantics explicitly — right now the docstring and the PR description claim the opposite of what the code does.

The rest is nits, also inline. One out-of-scope note worth being aware of rather than fixing here: these operators' first deferrable surface inherits #71525, so on core 3.0.x the trigger cannot fetch the connection at all (3.1.0+ is fine). Whether that caveat belongs in this page or centrally is a call for the docs discussion happening on #71667.

Thanks for the live workspace check @moomindani i responded to comments and commited the latest change

The follow-up is on the branch. Changelog Features under 7.18.1 is gone. The trigger now serializes an absolute end_time the same way DatabricksSQLStatementExecutionTrigger does. a_get_warehouse shares the GET path with get_warehouse. The on_kill override and the unpinned test are gone. The typed state JSON stays for sibling trigger symmetry, and the description now mentions the Phase 1 to_json restore.

The 3.0.x connection fetch caveat from 71525 is still out of scope here.

@moomindanimoomindani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — all five points are addressed, and I re-verified on 38bc535d4a rather than taking it on trust.

The changelog is back to the released 7.18.1 section untouched. The deadline is now an absolute end_time computed by the operator, serialized, and compared with time.time() in run(), matching DatabricksSQLStatementExecutionTrigger — and the two new tests pin it where it matters: the round trip preserves end_time, and a trigger resumed with an already-passed end_time times out immediately instead of opening a fresh window. That was the part I cared about most. on_kill and its vacuous test are gone with the rationale kept in the class docstring, and a_get_warehouse_state now goes through a_get_warehouse; the remaining duplicated path suffix matches how start_warehouse / stop_warehouse declare theirs in that file, so no objection from me. Keeping the typed state payload for sibling symmetry with the reasoning recorded is a fine call.

Real workspace re-run on this head (Airflow 3.2.2, provider installed from the branch, dedicated throwaway serverless warehouse): deferrable start deferred -> success, deferrable stop deferred -> success, and timeout=1deferred -> failed with did not reach RUNNING within 1s. 203 unit tests pass across the three files.

One observation from that last run, not a request: with a timeout shorter than the defer round-trip, the absolute deadline has already passed by the time the triggerer picks the trigger up, so it never polls and the message reports last state: unknown where the synchronous path would have polled once and named a state. That is the correct consequence of honouring an absolute deadline and is invisible at realistic timeouts — just noting that the boundary now differs slightly between the two paths.

LGTM.


Drafted-by: Claude Code (Opus 5); reviewed by @moomindani before posting

@Vamsi-klu

Copy link
Copy Markdown
ContributorAuthor

@eladkal as @moomindani approved the PR, Can i get your thoughts on this please?

@eladkal

Copy link
Copy Markdown
Contributor

can you please update the comment in #21377 (comment) with checklist to the actual steps in the plan and which PR solved which step? That way we can easily track what is left open

Vamsi-kluand others added 3 commits August 24, 2026 00:18
Start and stop waits from apache#70088 held a worker for the whole warehouse
lifecycle. This follow-up lands the agreed additive triggerer path so
those waits no longer occupy a worker slot.
Co-authored-by: Cursor <cursoragent@cursor.com>
A duration recomputed inside the trigger started a fresh wait after every triggerer restart or HA rebalance, so a warehouse that never reached its target could stay deferred well past the configured timeout.
Sphinx treats the HA term as a misspelling and fails the provider docs job.
Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor
cursorBotforce-pushed the follow-up-70088-warehouse-deferrable branch from 16dfb9e to 8f7d2d3CompareAugust 24, 2026 00:19
@Vamsi-klu

Vamsi-klu commented Aug 24, 2026

Copy link
Copy Markdown
ContributorAuthor

Hi @eladkal thanks a ton for the comment

Updated the tracking comment: #21377 (comment)

#70088 is Phase 1. This PR is the deferrable follow-up. Create/delete, name lookup, edit, and a live system-test Dag are still open.

@eladkal
eladkal merged commit c4e6ed3 into apache:mainAug 26, 2026
83 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@Vamsi-klu@eladkal@moomindani@cursoragent
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add deferrable mode to Databricks SQL warehouse operators - #71752

Merged
eladkal merged 3 commits into
apache:mainfrom
Vamsi-klu:follow-up-70088-warehouse-deferrable
Aug 26, 2026
Merged

Add deferrable mode to Databricks SQL warehouse operators#71752
eladkal merged 3 commits into
apache:mainfrom
Vamsi-klu:follow-up-70088-warehouse-deferrable

Conversation

@Vamsi-klu

@Vamsi-kluVamsi-klu commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

This adds an additive deferrable path for the Databricks SQL warehouse start and stop operators, so lifecycle waits can run on the triggerer instead of holding a worker.

related: #70088
related: #21377

Problem

Airflow's Databricks provider can start and stop an existing SQL warehouse, but Phase 1 waits hold a worker for the entire poll. Dag authors who want triggerer-based waiting currently have no deferrable path on these operators.

What changed

  • Add DatabricksHook.a_get_warehouse_state as the async GET mirror of get_warehouse_state.
  • Restore WarehouseState.to_json / from_json for trigger event round-trip.
  • Add DatabricksWarehouseStateTrigger next to the provider's existing triggers, with serialize, local monotonic timeout, retry_args validation, and a documented no-op on_kill.
  • Add deferrable on both DatabricksStartWarehouseOperator and DatabricksStopWarehouseOperator using the default_deferrable pattern.
  • Document the deferrable path in the warehouse how-to and add a provider changelog Features note.

No request-body workaround or new dependency is introduced. The implementation follows the Databricks SQL Warehouses API and keeps the Phase 1 wait contract.

Scope

This is the Phase 2 deferrable follow-up agreed on #70088: async hook, serialized trigger, operator completion path, and compatibility tests. Create/delete, edit, warehouse-by-name resolution, and a live Databricks system-test Dag remain outside this PR so the follow-up stays reviewable and independently useful.

Behavior and compatibility

  • deferrable defaults off and honors [operators] default_deferrable.
  • wait_for_termination, polling_period_seconds, and timeout are unchanged.
  • Starting an already RUNNING warehouse and stopping an already STOPPED warehouse remain no-ops and do not defer.
  • wait_for_termination=False still returns after requesting the transition and does not defer.
  • Existing STARTING/STOPPING transitions are reused instead of issuing duplicate requests, then deferred if waiting.
  • A start accepted while the warehouse still reports STOPPING continues on the triggerer; Databricks API transition rejections propagate unchanged.
  • Stale STOPPED while waiting for RUNNING is not terminal; the trigger keeps polling until RUNNING, deletion, or timeout.
  • Waiting uses time.monotonic() locally in the trigger, starts no new poll after the configured deadline, and still honors a target or deletion state returned by a poll that began before the deadline.
  • Clearing a deferred wait does not start or stop the warehouse: Databricks has no cancel API for these transitions.
  • execute_complete maps success / deleted / timeout to the same messages as Phase 1.

Validation

  • uv run --project providers/databricks pytest providers/databricks/tests/unit/databricks/operators/test_warehouse.py providers/databricks/tests/unit/databricks/hooks/test_databricks.py::TestWarehouseLifecycle providers/databricks/tests/unit/databricks/triggers/test_databricks.py::TestDatabricksWarehouseStateTrigger providers/databricks/tests/unit/databricks/triggers/test_databricks.py::test_trigger_init_rejects_non_serializable_retry_args — 70 passed.
  • prek run --from-ref upstream/main --stage pre-commit — passed.

Reviewer evidence

This PR has no UI surface, so before/after screenshots and browser validation are not applicable. The REST boundary is covered with an autospecced async GET assertion, operator deferral is covered with a specced hook, and trigger serialize/run/timeout/on_kill are covered with mocked warehouse state. No Databricks workspace credentials were used or required for these deterministic lifecycle tests.

The Phase 2 scope was posted on #70088 before this follow-up: #70088 (comment)


Was generative AI tooling used to co-author this PR?
  • Yes — Cursor Grok 4.6

Generated-by: Cursor Grok 4.6 following the guidelines

@Vamsi-klu

Vamsi-klu commented Aug 18, 2026

Copy link
Copy Markdown
ContributorAuthor

@eladkal@moomindani ni this is the #70088 deferrable follow-up, could you review when you have a chance?

@moomindanimoomindani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — the Phase 1 contract is preserved carefully here, and the state-machine edge cases we went through on #70088 (stale STOPPED not terminal, deletion mid-wait, the deadline boundary) are all covered by deterministic tests rather than sleeps.

I validated the deferrable paths against a real workspace: Airflow 3.2.2, this branch's provider installed from source, a dedicated throwaway serverless warehouse (2X-Small), triggerer doing the waiting.

scenariotask stateevidence
deferrable start from STOPPEDdeferred -> successtrigger logged is STARTING; waiting for RUNNING, warehouse reached RUNNING
deferrable stopdeferred -> successwarehouse reached STOPPED
start against an already-RUNNING warehousesuccess, never deferredno DeferTask for the task
wait_for_termination=False stopsuccess, never deferredtransition still requested, warehouse stopped afterwards
timeout=1 startdeferred -> failedDatabricksWarehouseError: ... did not reach RUNNING within 1s; last state: STARTING.

So the behavioural claims in the description hold up where I could exercise them. Unit tests reproduce your numbers as well (43 + 14 + 13, and the full provider suite at 922 passed / 12 skipped).

Two things I would like settled before merge, both left inline:

  • The changelog entry is inserted under the 7.18.1 header, which is an already-released version, so a shipped release would advertise a feature it does not contain. Per providers/AGENTS.md and the NOTE TO CONTRIBUTORS block at the top of that file, routine feature entries are collected by the release manager from commit messages anyway — the five lines can just go.
  • The deferrable timeout is a duration recomputed inside every run(), so it restarts from zero on each triggerer restart or HA rebalance, while the synchronous path fails deterministically. DatabricksSQLStatementExecutionTrigger next door serializes an absolute end_time for exactly this reason. Either follow that pattern or state the per-run semantics explicitly — right now the docstring and the PR description claim the opposite of what the code does.

The rest is nits, also inline. One out-of-scope note worth being aware of rather than fixing here: these operators' first deferrable surface inherits #71525, so on core 3.0.x the trigger cannot fetch the connection at all (3.1.0+ is fine). Whether that caveat belongs in this page or centrally is a call for the docs discussion happening on #71667.

Comment threadproviders/databricks/docs/changelog.rst Outdated
@Vamsi-klu

Vamsi-klu commented Aug 19, 2026

Copy link
Copy Markdown
ContributorAuthor

Thanks — the Phase 1 contract is preserved carefully here, and the state-machine edge cases we went through on #70088 (stale STOPPED not terminal, deletion mid-wait, the deadline boundary) are all covered by deterministic tests rather than sleeps.

I validated the deferrable paths against a real workspace: Airflow 3.2.2, this branch's provider installed from source, a dedicated throwaway serverless warehouse (2X-Small), triggerer doing the waiting.

scenariotask stateevidence
deferrable start from STOPPEDdeferred -> successtrigger logged is STARTING; waiting for RUNNING, warehouse reached RUNNING
deferrable stopdeferred -> successwarehouse reached STOPPED
start against an already-RUNNING warehousesuccess, never deferredno DeferTask for the task
wait_for_termination=False stopsuccess, never deferredtransition still requested, warehouse stopped afterwards
timeout=1 startdeferred -> failedDatabricksWarehouseError: ... did not reach RUNNING within 1s; last state: STARTING.

So the behavioural claims in the description hold up where I could exercise them. Unit tests reproduce your numbers as well (43 + 14 + 13, and the full provider suite at 922 passed / 12 skipped).

Two things I would like settled before merge, both left inline:

  • The changelog entry is inserted under the 7.18.1 header, which is an already-released version, so a shipped release would advertise a feature it does not contain. Per providers/AGENTS.md and the NOTE TO CONTRIBUTORS block at the top of that file, routine feature entries are collected by the release manager from commit messages anyway — the five lines can just go.
  • The deferrable timeout is a duration recomputed inside every run(), so it restarts from zero on each triggerer restart or HA rebalance, while the synchronous path fails deterministically. DatabricksSQLStatementExecutionTrigger next door serializes an absolute end_time for exactly this reason. Either follow that pattern or state the per-run semantics explicitly — right now the docstring and the PR description claim the opposite of what the code does.

The rest is nits, also inline. One out-of-scope note worth being aware of rather than fixing here: these operators' first deferrable surface inherits #71525, so on core 3.0.x the trigger cannot fetch the connection at all (3.1.0+ is fine). Whether that caveat belongs in this page or centrally is a call for the docs discussion happening on #71667.

Thanks for the live workspace check @moomindani i responded to comments and commited the latest change

The follow-up is on the branch. Changelog Features under 7.18.1 is gone. The trigger now serializes an absolute end_time the same way DatabricksSQLStatementExecutionTrigger does. a_get_warehouse shares the GET path with get_warehouse. The on_kill override and the unpinned test are gone. The typed state JSON stays for sibling trigger symmetry, and the description now mentions the Phase 1 to_json restore.

The 3.0.x connection fetch caveat from 71525 is still out of scope here.

@moomindanimoomindani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — all five points are addressed, and I re-verified on 38bc535d4a rather than taking it on trust.

The changelog is back to the released 7.18.1 section untouched. The deadline is now an absolute end_time computed by the operator, serialized, and compared with time.time() in run(), matching DatabricksSQLStatementExecutionTrigger — and the two new tests pin it where it matters: the round trip preserves end_time, and a trigger resumed with an already-passed end_time times out immediately instead of opening a fresh window. That was the part I cared about most. on_kill and its vacuous test are gone with the rationale kept in the class docstring, and a_get_warehouse_state now goes through a_get_warehouse; the remaining duplicated path suffix matches how start_warehouse / stop_warehouse declare theirs in that file, so no objection from me. Keeping the typed state payload for sibling symmetry with the reasoning recorded is a fine call.

Real workspace re-run on this head (Airflow 3.2.2, provider installed from the branch, dedicated throwaway serverless warehouse): deferrable start deferred -> success, deferrable stop deferred -> success, and timeout=1deferred -> failed with did not reach RUNNING within 1s. 203 unit tests pass across the three files.

One observation from that last run, not a request: with a timeout shorter than the defer round-trip, the absolute deadline has already passed by the time the triggerer picks the trigger up, so it never polls and the message reports last state: unknown where the synchronous path would have polled once and named a state. That is the correct consequence of honouring an absolute deadline and is invisible at realistic timeouts — just noting that the boundary now differs slightly between the two paths.

LGTM.


Drafted-by: Claude Code (Opus 5); reviewed by @moomindani before posting

@Vamsi-klu

Copy link
Copy Markdown
ContributorAuthor

@eladkal as @moomindani approved the PR, Can i get your thoughts on this please?

@eladkal

Copy link
Copy Markdown
Contributor

can you please update the comment in #21377 (comment) with checklist to the actual steps in the plan and which PR solved which step? That way we can easily track what is left open

Vamsi-kluand others added 3 commits August 24, 2026 00:18
Start and stop waits from apache#70088 held a worker for the whole warehouse
lifecycle. This follow-up lands the agreed additive triggerer path so
those waits no longer occupy a worker slot.
Co-authored-by: Cursor <cursoragent@cursor.com>
A duration recomputed inside the trigger started a fresh wait after every triggerer restart or HA rebalance, so a warehouse that never reached its target could stay deferred well past the configured timeout.
Sphinx treats the HA term as a misspelling and fails the provider docs job.
Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor
cursorBotforce-pushed the follow-up-70088-warehouse-deferrable branch from 16dfb9e to 8f7d2d3CompareAugust 24, 2026 00:19
@Vamsi-klu

Vamsi-klu commented Aug 24, 2026

Copy link
Copy Markdown
ContributorAuthor

Hi @eladkal thanks a ton for the comment

Updated the tracking comment: #21377 (comment)

#70088 is Phase 1. This PR is the deferrable follow-up. Create/delete, name lookup, edit, and a live system-test Dag are still open.

@eladkal
eladkal merged commit c4e6ed3 into apache:mainAug 26, 2026
83 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@Vamsi-klu@eladkal@moomindani@cursoragent