Skip to content

Send heartbeats for open DMS deployment versions - #7342

Merged
Sankalp-Mittal merged 9 commits into
mainfrom
sankalp-mittal/heartbeating-for-dms
Nov 9, 2026
Merged

Sankalp-Mittal merged 9 commits into
mainfrom
sankalp-mittal/heartbeating-for-dms

Conversation

@Sankalp-Mittal

@Sankalp-Mittal Sankalp-Mittal commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Why

DMS marks a version IN_PROGRESS at CreateVersion. If the CLI dies before CompleteVersion, nothing refreshes the version and the UI shows it as deploying forever.

Design (meeting notes: Nov 2, Nov 3):

  • The service keeps one last-update time per version, refreshed by every UpdateOperation and by Heartbeat, and exposes it as Version.update_time. CreateVersion still rejects while the previous version's 2-minute lease (last write or heartbeat + 2 min) is live and closes an expired one as LEASE_EXPIRED, so writes and heartbeats both extend that lease. No auto-close; deploy.lock stays for now and will be removed in a later update.
  • The UI shows a version as interrupted once update_time is over 2 minutes old. The state is recoverable.
  • The CLI's job is to keep that time fresh while a version is open. Without heartbeats, a resource that takes over 2 minutes with no state write lets a second deploy take the version and the first one's writes then fail.

What

  • libs/dms/heartbeat.go: a goroutine per open version. It calls POST .../versions/{v}/heartbeat only after 30s with no UpdateOperation started, so a fast deploy sends none. A failed heartbeat is logged at info level and tried again at the next interval; the SDK already retries transient errors inside each call. It never fails or stops the deploy.
  • libs/dms/operation_buffer.go: the buffer owns the heartbeat. Each write resets the timer as it starts, not when it succeeds: UpdateOperation and Heartbeat both update the version row, so this way a heartbeat only overlaps a write that has been in flight a full interval. Stop() ends the heartbeat; it keeps running past Drain so a post-deploy script is still covered.
  • bundle/direct/dstate/state.go: the buffer (and with it the heartbeat) starts right after CreateVersion, so deploy and destroy are both covered, and CompleteVersion calls buf.Stop() before the RPC.
  • No request shapes change; HeartbeatResponse.expire_time is ignored.

Dependencies

  • Server PR databricks-eng/universe#2983937: writes refresh the version timestamp and Version.update_time is exposed. Until it lands, a deploy that writes more often than every 30s sends no heartbeat. Nothing reads the field yet, so this is not a regression.
  • UX PR (separate): show "interrupted" after 2 minutes. Must land after both.

Tests

  • libs/dms/heartbeat_test.go: testing/synctest with the real 30s constants: beats while idle, a write skips the beat, a failure tries again next interval, Stop().
  • acceptance/bundle/dms is unchanged: fixtures finish in under 30s, so no heartbeat is recorded. The fake server already serves the endpoint.
  • No .nextchanges fragment: DMS is experimental and the change is invisible until the UX PR lands.

This pull request and its description were written by Isaac.

DMS marks a version in progress at CreateVersion. If the CLI dies before
CompleteVersion, nothing refreshes the version and the UI shows it as
deploying forever. The service will expose a per-version last update time,
refreshed by UpdateOperation and Heartbeat, and the UI will show a version
as interrupted once that time is over two minutes old.

The CLI now keeps that time fresh while a version is open. A heartbeat is
sent only after 30s with no successful UpdateOperation, so a fast deploy
sends none. Heartbeats are best effort: a failed one is retried quickly,
then reported as a warning, then tried again at the next interval. It
never fails or stops the deploy.

Co-authored-by: Isaac <no-reply@databricks.com>
@github-actions github-actions Bot added the DABs DABs related issues label Nov 4, 2026
Sankalp-Mittal and others added 3 commits November 4, 2026 15:24
Stop is a context cancel instead of a stop channel, a done channel and a
sync.OnceFunc guard. The quick retries after a failed heartbeat are gone:
the SDK already retries transient errors inside each call, and the next
attempt is at most 30s away against a 2-minute UI threshold.

Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
@Sankalp-Mittal
Sankalp-Mittal marked this pull request as ready for review November 5, 2026 15:38
@Sankalp-Mittal
Sankalp-Mittal requested review from a team as code owners November 5, 2026 15:38
@Sankalp-Mittal
Sankalp-Mittal requested review from anton-107, denik and shreyas-goenka and removed request for anton-107 November 5, 2026 15:38
Resolves conflicts with #7151 and #7169: StartOperationBuffer takes both
BufferOptions and the heartbeat, and InitializeOperationBuffer starts the
heartbeat alongside the buffer in every history mode.

Co-authored-by: Isaac <no-reply@databricks.com>
@eng-dev-ecosystem-bot

eng-dev-ecosystem-bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Integration test report

Commit: 421bd8a

Run: 42647829709

Env ✅​pass 🙈​skip Time
✅​ aws linux-2core-8gb 271 15 4:41
✅​ aws-windows-latest-4core-16gb 273 13 3:39
✅​ azure linux-2core-8gb 270 15 4:30
✅​ azure-windows-latest-4core-16gb 272 13 3:59
✅​ gcp linux-2core-8gb 271 15 4:40
✅​ gcp-windows-latest-4core-16gb 273 13 3:52
Top 3 slowest tests (at least 2 minutes):
duration env testname
3:12 gcp-windows-latest-4core-16gb TestAccept
3:07 aws-windows-latest-4core-16gb TestAccept
3:05 azure-windows-latest-4core-16gb TestAccept

Comment thread libs/dms/heartbeat.go Resolved
Comment thread bundle/direct/dstate/state.go Resolved
Sankalp-Mittal and others added 4 commits November 9, 2026 13:00
A failed heartbeat is not config-related and needs no user action, so it
is logged with log.Infof instead of a warning diagnostic.

Co-authored-by: Isaac <no-reply@databricks.com>
DeploymentState held both the buffer and the heartbeat for the open
version and had to wire them together. The buffer now starts the
heartbeat itself and stops it in Stop, so DeploymentState keeps one
field. The heartbeat still runs past Drain until CompleteVersion.

Co-authored-by: Isaac <no-reply@databricks.com>
Both UpdateOperation and Heartbeat update the version row, so running
them at the same time can conflict on the server. Renewing when a write
starts, instead of when it succeeds, means a heartbeat only overlaps a
write that has been in flight for a full interval.

Co-authored-by: Isaac <no-reply@databricks.com>
Resolves conflicts with #7232: write takes a bufferedUpdate and keeps
renewing the heartbeat as it starts, and Stop sits next to the new Stats.

Co-authored-by: Isaac <no-reply@databricks.com>
@Sankalp-Mittal
Sankalp-Mittal added this pull request to the merge queue Nov 9, 2026
Merged via the queue into main with commit 943d3af Nov 9, 2026
27 checks passed
@Sankalp-Mittal
Sankalp-Mittal deleted the sankalp-mittal/heartbeating-for-dms branch November 9, 2026 16:13
@eng-dev-ecosystem-bot

Copy link
Copy Markdown
Collaborator

❌ Integration test report

Commit: 943d3af

Run: 42652680044

Env ❌​FAIL ✅​pass 🙈​skip Time
❌​ aws linux-2core-8gb 35 1496 1263 141:20
❌​ aws-windows-latest-4core-16gb 38 1412 1290 166:53
❌​ azure linux-2core-8gb 37 1474 1271 126:06
❌​ azure-windows-latest-4core-16gb 35 1395 1298 135:17
❌​ gcp linux-2core-8gb 35 1483 1269 136:48
❌​ gcp-windows-latest-4core-16gb 35 1402 1296 157:13
40 interesting tests: 40 FAIL
Test Name aws linux-2core-8gb aws-windows-latest-4core-16gb azure linux-2core-8gb azure-windows-latest-4core-16gb gcp linux-2core-8gb gcp-windows-latest-4core-16gb
❌​ TestAccept ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/config-remote-sync/external_cluster_policy_cross_target ✅​p 🙈​s ❌​F 🙈​s ✅​p 🙈​s
❌​ TestAccept/bundle/config-remote-sync/external_cluster_policy_cross_target/DMS=true ✅​p ❌​F ✅​p
❌​ TestAccept/bundle/resources/postgres_synced_tables/recreate ✅​p ❌​F ✅​p ✅​p ✅​p ✅​p
❌​ TestAccept/bundle/resources/postgres_synced_tables/recreate/DMS= ✅​p ❌​F ✅​p ✅​p ✅​p ✅​p
❌​ TestAccept/bundle/resources/postgres_synced_tables/recreate/DMS=true ✅​p ❌​F ✅​p ✅​p ✅​p ✅​p
❌​ TestAccept/bundle/templates/default-python/combinations/classic ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/classic/DLT=no/DMS=/NBOOK=no/PY=no/READPLAN= ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/classic/DLT=no/DMS=/NBOOK=no/PY=no/READPLAN=1 ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/classic/DLT=no/DMS=/NBOOK=no/PY=yes/READPLAN= ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/classic/DLT=no/DMS=/NBOOK=no/PY=yes/READPLAN=1 ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/classic/DLT=no/DMS=/NBOOK=yes/PY=no/READPLAN= ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/classic/DLT=no/DMS=/NBOOK=yes/PY=no/READPLAN=1 ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/classic/DLT=no/DMS=/NBOOK=yes/PY=yes/READPLAN= ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/classic/DLT=no/DMS=/NBOOK=yes/PY=yes/READPLAN=1 ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/classic/DLT=yes/DMS=/NBOOK=no/PY=no/READPLAN= ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/classic/DLT=yes/DMS=/NBOOK=no/PY=no/READPLAN=1 ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/classic/DLT=yes/DMS=/NBOOK=no/PY=yes/READPLAN= ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/classic/DLT=yes/DMS=/NBOOK=no/PY=yes/READPLAN=1 ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/classic/DLT=yes/DMS=/NBOOK=yes/PY=no/READPLAN= ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/classic/DLT=yes/DMS=/NBOOK=yes/PY=no/READPLAN=1 ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/classic/DLT=yes/DMS=/NBOOK=yes/PY=yes/READPLAN= ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/classic/DLT=yes/DMS=/NBOOK=yes/PY=yes/READPLAN=1 ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/serverless ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/serverless/DLT=no/DMS=/NBOOK=no/PY=no/READPLAN= ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/serverless/DLT=no/DMS=/NBOOK=no/PY=no/READPLAN=1 ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/serverless/DLT=no/DMS=/NBOOK=no/PY=yes/READPLAN= ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/serverless/DLT=no/DMS=/NBOOK=no/PY=yes/READPLAN=1 ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/serverless/DLT=no/DMS=/NBOOK=yes/PY=no/READPLAN= ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/serverless/DLT=no/DMS=/NBOOK=yes/PY=no/READPLAN=1 ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/serverless/DLT=no/DMS=/NBOOK=yes/PY=yes/READPLAN= ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/serverless/DLT=no/DMS=/NBOOK=yes/PY=yes/READPLAN=1 ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/serverless/DLT=yes/DMS=/NBOOK=no/PY=no/READPLAN= ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/serverless/DLT=yes/DMS=/NBOOK=no/PY=no/READPLAN=1 ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/serverless/DLT=yes/DMS=/NBOOK=no/PY=yes/READPLAN= ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/serverless/DLT=yes/DMS=/NBOOK=no/PY=yes/READPLAN=1 ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/serverless/DLT=yes/DMS=/NBOOK=yes/PY=no/READPLAN= ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/serverless/DLT=yes/DMS=/NBOOK=yes/PY=no/READPLAN=1 ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/serverless/DLT=yes/DMS=/NBOOK=yes/PY=yes/READPLAN= ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
❌​ TestAccept/bundle/templates/default-python/combinations/serverless/DLT=yes/DMS=/NBOOK=yes/PY=yes/READPLAN=1 ❌​F ❌​F ❌​F ❌​F ❌​F ❌​F
Top 50 slowest tests (at least 2 minutes):
duration env testname
19:03 aws-windows-latest-4core-16gb TestAccept/bundle/resources/clusters/lifecycle-started/DMS=
14:41 gcp-windows-latest-4core-16gb TestAccept/bundle/resources/apps/lifecycle-started/DMS=true
14:38 aws linux-2core-8gb TestAccept/bundle/resources/clusters/lifecycle-started/DMS=
14:33 aws linux-2core-8gb TestAccept/bundle/resources/clusters/lifecycle-started-toggle/DMS=true
14:21 gcp-windows-latest-4core-16gb TestAccept/bundle/resources/apps/lifecycle-started/DMS=
12:57 gcp linux-2core-8gb TestAccept/bundle/resources/apps/lifecycle-started/DMS=
12:27 aws linux-2core-8gb TestAccept/bundle/resources/clusters/lifecycle-started/DMS=true
12:15 aws-windows-latest-4core-16gb TestAccept/bundle/resources/clusters/lifecycle-started/DMS=true
11:33 azure linux-2core-8gb TestAccept/bundle/resources/clusters/lifecycle-started/DMS=
11:29 gcp linux-2core-8gb TestAccept/bundle/resources/apps/lifecycle-started/DMS=true
9:12 azure-windows-latest-4core-16gb TestAccept/bundle/resources/apps/lifecycle-started/DMS=
9:11 azure-windows-latest-4core-16gb TestAccept/bundle/resources/apps/lifecycle-started/DMS=true
9:05 aws linux-2core-8gb TestAccept/bundle/resources/apps/lifecycle-started/DMS=true
9:02 aws linux-2core-8gb TestAccept/bundle/resources/apps/lifecycle-started/DMS=
8:53 aws-windows-latest-4core-16gb TestAccept/bundle/resources/apps/lifecycle-started/DMS=
8:29 azure linux-2core-8gb TestAccept/bundle/resources/clusters/resize-terminated-fallback/DMS=
8:08 aws-windows-latest-4core-16gb TestAccept/bundle/resources/apps/lifecycle-started/DMS=true
8:08 aws-windows-latest-4core-16gb TestAccept/bundle/resources/clusters/lifecycle-started-toggle/DMS=true
8:05 azure linux-2core-8gb TestAccept/bundle/resources/apps/lifecycle-started/DMS=true
8:04 azure linux-2core-8gb TestAccept/bundle/resources/apps/lifecycle-started/DMS=
7:59 azure linux-2core-8gb TestAccept/bundle/resources/clusters/lifecycle-started-toggle/DMS=
7:30 aws linux-2core-8gb TestAccept/bundle/resources/clusters/lifecycle-started-toggle/DMS=
7:26 gcp-windows-latest-4core-16gb TestAccept/bundle/invariant/destroy_idempotent/DMS=/INPUT_CONFIG=cluster_libraries.yml.tmpl/READPLAN=1
7:25 gcp linux-2core-8gb TestAccept/bundle/resources/clusters/deploy/local_ssd_count/DMS=
7:24 gcp-windows-latest-4core-16gb TestAccept/bundle/resources/clusters/lifecycle-started/DMS=true
7:18 gcp-windows-latest-4core-16gb TestAccept/bundle/invariant/destroy_idempotent/DMS=/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=1
7:18 aws-windows-latest-4core-16gb TestAccept/bundle/invariant/no_drift/DMS=true/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=1
7:15 aws-windows-latest-4core-16gb TestAccept/bundle/invariant/delete_idempotent/DMS=/INPUT_CONFIG=cluster_libraries.yml.tmpl/READPLAN=1
7:12 gcp linux-2core-8gb TestAccept/bundle/resources/clusters/lifecycle-started/DMS=true
7:12 gcp-windows-latest-4core-16gb TestAccept/bundle/invariant/destroy_idempotent/DMS=/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=
7:12 gcp-windows-latest-4core-16gb TestAccept/bundle/resources/clusters/deploy/local_ssd_count/DMS=true
7:09 aws-windows-latest-4core-16gb TestAccept/bundle/resources/clusters/lifecycle-started-toggle/DMS=
7:07 gcp-windows-latest-4core-16gb TestAccept/bundle/resources/clusters/deploy/local_ssd_count/DMS=
7:05 gcp linux-2core-8gb TestAccept/bundle/resources/clusters/lifecycle-started/DMS=
7:00 aws linux-2core-8gb TestAccept/bundle/invariant/destroy_idempotent/DMS=/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=
7:00 aws-windows-latest-4core-16gb TestAccept/bundle/invariant/delete_idempotent/DMS=/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=1
7:00 gcp linux-2core-8gb TestAccept/bundle/config-remote-sync/multiple_resources/DMS=
6:54 aws linux-2core-8gb TestAccept/bundle/invariant/destroy_idempotent/DMS=/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=1
6:54 aws-windows-latest-4core-16gb TestAccept/bundle/resources/clusters/deploy/data_security_mode/DMS=
6:46 aws-windows-latest-4core-16gb TestAccept/bundle/invariant/delete_idempotent/DMS=/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=
6:46 gcp-windows-latest-4core-16gb TestAccept/bundle/resources/clusters/lifecycle-started/DMS=
6:42 azure linux-2core-8gb TestAccept/bundle/resources/clusters/lifecycle-started-toggle/DMS=true
6:42 azure linux-2core-8gb TestAccept/bundle/config-remote-sync/multiple_resources/DMS=
6:37 aws linux-2core-8gb TestAccept/bundle/config-remote-sync/multiple_resources/DMS=true
6:21 aws-windows-latest-4core-16gb TestAccept/bundle/invariant/no_drift/DMS=true/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=
6:21 gcp linux-2core-8gb TestAccept/bundle/config-remote-sync/multiple_resources/DMS=true
6:20 aws linux-2core-8gb TestAccept/bundle/config-remote-sync/multiple_resources/DMS=
6:16 azure linux-2core-8gb TestAccept/bundle/config-remote-sync/multiple_resources/DMS=true
5:58 gcp-windows-latest-4core-16gb TestAccept/bundle/resources/apps/lifecycle-started-toggle/DMS=true
5:50 gcp-windows-latest-4core-16gb TestAccept/bundle/resources/clusters/lifecycle-started-toggle/DMS=true

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DABs DABs related issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants