Skip to content

experimental/air: upload incremental code snapshots - #6691

Draft
ben-hansen-db wants to merge 7 commits into
mainfrom
ben/air-incremental-code-snapshots
Draft

ben-hansen-db wants to merge 7 commits into
mainfrom
ben/air-incremental-code-snapshots

Conversation

@ben-hansen-db

Copy link
Copy Markdown
Contributor

Summary

Adds incremental anchor-and-overlay uploads for repeated local databricks experimental air run submissions with large plain_tar code sources.

  • Records branch-local anchor state from path, size, mtime, mode/type, and symlink target metadata.
  • Uploads immutable full anchors, then whole-file overlays containing only changed/new files and deletion markers.
  • Re-anchors after 30 days, when the remote anchor is missing, or when a delta crosses the initial path/byte/ratio thresholds.
  • Uses the existing full-snapshot path for trees below 64 MiB, Git archives, UC Volumes, and --no-cache.
  • Re-stats the selected tree after packaging and fails the submission if it changed concurrently.
  • Includes the content-addressed and warm-cache foundations from experimental/air: content-address the plain_tar upload #6578 and experimental/air: warm snapshot cache for the plain_tar path #6572; this draft is intended to evaluate the combined submission path.

Node-side materialization is implemented by the paired draft: https://github.com/databricks-eng/universe/pull/2617554

Validation

  • go test ./experimental/air/cmd -run "Snapshot|PlainTar|Overlay"

  • go tool -modfile=tools/go.mod golangci-lint run --allow-parallel-runners -j=4 ./experimental/air/cmd — 0 issues

  • ./task build

  • End-to-end on e2-dogfood with a 96 MiB incompressible fixture:

    submission wall time package publish uploaded bytes run
    cold anchor 5.62 s 108 ms 2.576 s 100,672,722 817015759230716 — SUCCESS
    one-line overlay 1.52 s 0.965 ms 166 ms 459 707821459718410 — SUCCESS
    matched full-upload baseline 3.72 s full archive 864409177771333 — last observed RUNNING

The complete go test ./experimental/air/cmd run reaches one current-main failure in TestSubmitWorkload: the SDK now populates ComputeSpec.ForceSendFields, while the base test compares the full struct against an expected value without that internal field. The snapshot-focused suite passes.

Notes

This is intentionally WSFS-only initially. Remote snapshot garbage collection is out of scope; selecting a newer anchor does not delete historical objects. Initial eligibility and re-anchor thresholds are experiment inputs and should be tuned from end-to-end measurements.

ben-hansen-db and others added 7 commits September 15, 2026 17:49
git_archive snapshots are already content-addressed: a repeat submission at the
same commit reuses the uploaded tarball and skips packaging + upload. plain_tar
(dirty working tree) used a timestamped name, so it re-packaged and re-uploaded
the full tarball on every submission, even when nothing changed.

Name the plain_tar tarball by a working-tree fingerprint (sha256 over each file's
path, size and mtime) and run the same snapshotExists skip for both modes. An
unchanged resubmit now reuses the remote object and moves no bytes. The listing
is captured once and threaded into packaging, so the tree is walked only once.
The fingerprint is size+mtime, not content, matching DABs file-sync.

Verified on df1: a second submission of an unchanged tree logs
"snapshot upload skipped; reusing ..." and returns the identical remote path.

Co-authored-by: Isaac <no-reply@databricks.com>
- Strengthen the dedup test: count import-file calls and assert the second
  (unchanged) submit adds zero, instead of asserting a path-keyed set has one
  entry. The set couldn't distinguish a skipped submit from a re-upload to the
  same content-addressed name; the counter can (verified it fails when the skip
  is disabled).
- Fold snapshotPackagingVersion into computePlainTarKey so a packaging-logic
  bump invalidates plain_tar keys too, not just plainTarKeyVersion.
- Fix stale comments now that plain_tar is content-addressed: modePlainTar
  ("not cacheable") and snapshotExists ("git_archive" only).

Co-authored-by: Isaac <no-reply@databricks.com>
Add a local warm cache for the plain_tar snapshot path, keyed by
(repo, config, include_paths) under $TMPDIR/databricks/.air/<key>: an
uncompressed snapshot.tar plus a manifest of each file's size+mtime and byte
range. Later runs stat the file set, copy unchanged members verbatim from the
warm tar, and re-read only changed files before gzipping the upload. --no-cache
bypasses it and re-packs from scratch. The cache engages only above 64 MiB.

The cache's payoff is largest when the working set does not fit the OS page
cache: cold, scattered small-file reads cost seconds for a large folder versus
~ms to read the warm tar sequentially. When the tree is already warm in RAM,
parallel gzip accounts for most of the gain and the cache adds little.

Co-authored-by: Isaac <no-reply@databricks.com>
Match the parent PR: DefaultCompression rather than BestSpeed in newGzFile, so
the cached tarball is re-gzipped at the same level as the --no-cache path and the
upload stays small. Parallel compression makes the higher level nearly free.

Co-authored-by: Isaac <no-reply@databricks.com>
The new --no-cache flag on `air run` adds a line to its --help output, which the
experimental/air/config-help acceptance test pins. Regenerate the golden.

Co-authored-by: Isaac <no-reply@databricks.com>
Isaac Review flagged two MAJOR correctness bugs in the warm cache:
- Concurrent `air run` on the same cache key wrote the same snapshot.tar.tmp and
  raced the rename plus a non-atomic manifest write, interleaving into a corrupt
  tar/manifest pair.
- rebuildWarmSnapshot renamed the new tar into place before saving the manifest,
  so a crash between the two left a manifest whose byte offsets described a
  different tar layout -- silently corrupting a later verbatim-reuse rebuild.

Fix both by binding the manifest to a per-build, uniquely named tar
(snapshot.<id>.tar) that is never overwritten, and installing the manifest
atomically (unique temp + rename) only after its tar is durable. A manifest and
the tar it indexes are therefore always a consistent pair: there is no window
where offsets describe a mismatched tar, and concurrent rebuilds are
last-writer-wins on the manifest rather than interleaving, so no lock is needed.
Superseded and orphaned tars are cleaned up best-effort. Adds a rotation test.

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

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: d40316b

Run: 35004901089

Env 💚​RECOVERED ✅​pass 🙈​skip Time
💚​ aws linux 1 275 16 5:27
💚​ aws windows 1 277 14 3:48
💚​ azure linux 1 274 16 5:22
💚​ azure windows 1 276 14 5:05
💚​ gcp linux 1 275 16 6:36
💚​ gcp windows 1 277 14 5:26
Test Name aws linux aws windows azure linux azure windows gcp linux gcp windows
💚​ TestAccept 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
Top 3 slowest tests (at least 2 minutes):
duration env testname
3:44 gcp windows TestAccept
3:17 azure windows TestAccept
3:06 aws windows TestAccept

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants