Skip to content

feat(prefetch): Pre-fetch patches for sibling releases on the same track#357

Draft
lukemmtt wants to merge 4 commits into
shorebirdtech:mainfrom
lukemmtt:prefetch-future-release-patches
Draft

feat(prefetch): Pre-fetch patches for sibling releases on the same track#357
lukemmtt wants to merge 4 commits into
shorebirdtech:mainfrom
lukemmtt:prefetch-future-release-patches

Conversation

@lukemmtt
Copy link
Copy Markdown

@lukemmtt lukemmtt commented May 10, 2026

Closes shorebirdtech/shorebird#3755.

Note: This is a draft, proof-of-concept implementation, built by Claude Opus, offered merely as a scope exploration rather than a ready-to-merge change.

Summary

When a user's native binary auto-updates between Shorebird patch-checks, the device today must boot the unpatched new release until the next patch-check completes. This change lets the running binary park raw deltas for other release_versions on the same track, so that when the user later updates to one of them, the matching delta is already on disk and is promoted into the active boot path during init.

Approach

Built on top of the per-patch lifecycle state machine introduced in #352. A prefetched patch is just a PatchState::Downloaded document stored in a parallel namespace keyed by release_version:

<storage_dir>/prefetched_patches/<rv>/patches/<n>/state.json
<download_dir>/prefetched_patches/<rv>/<n>

This namespace is independent of the per-release patches/ and download_dir/<n> paths the active lifecycle owns, so the release-version reset wipes the active store but leaves the prefetched store intact. An independent PatchLifecycle is constructed against the prefetched roots to reuse record_download_started, record_download_complete, read_state, and decide_start.

Wire shape

  • Server side: extend PatchCheckResponse with optional available_release_versions: Vec<String>, the set of release_versions on the requesting app's track that have a publishable patch. Older clients ignore the field.
  • Client side: after the existing patch-check + install flow runs for the current release, iterate over the advertised siblings (excluding the current release_version), issue one extra /patches/check per sibling with release_version overridden, download the raw delta, and record it as PatchState::Downloaded in the prefetched namespace. No inflation or hash/signature verification happens at prefetch time, since the running binary is the wrong inflation base for any release other than its own.
  • Promotion: handle_prior_boot_failure_if_necessary also tries to promote a prefetched delta whose release_version matches the now-running binary. The compressed bytes are moved into the active download root, a Downloaded state is written into the active patches/<n>/, then the existing inflate → check_hash → record_install_complete → promote_to_next_boot pipeline runs against it. The new binary's patch_public_key is the right key for both hash verification and Strict-mode boot signing; no special handling needed.

Lifecycle and cleanup

  • Cached siblings the server stops advertising are pruned via retain_prefetched_release_versions on every successful patch-check.
  • A prefetched entry that fails inflation, hash check, or matches a known-bad patch number is dropped on attempt so the device doesn't loop. The promoted patch is marked Bad in the active lifecycle so decide_start short-circuits subsequent updates within the release.
  • Errors in the prefetch tail are best-effort and never affect the main update result.

Open items for the Shorebird team

  • Server change to populate available_release_versions from the same-track release set with publishable patches.
  • Decide whether available_release_versions should respect gradual-rollout bucketing for siblings. Current behavior asks the server again per sibling, so per-release rollout decisions ride for free.
  • Promotion currently runs on every init. If you'd rather gate it on a fresh release_version mismatch detected by load_or_new_on_error, that signal would have to be surfaced explicitly.
  • An opt-out flag (prefetchUpcomingPatches: false in shorebird.yaml) for unusual cases like metered networks or embedded devices is not wired up here.

Test plan

  • cargo test --workspace passes 242/242 on this branch.

lukemmtt added 4 commits May 9, 2026 20:31
Closes the patchless-first-launch gap from
shorebirdtech/shorebird#3755. When a user's native binary auto-updates
between Shorebird patch-checks, the device today must boot the unpatched
new release until the next patch-check completes. This change lets the
running binary park raw deltas for *other* release_versions on the same
track, so that when the user later updates to one of them, the matching
delta is already on disk and is promoted into the active boot path
during init.

Built on top of the per-patch lifecycle state machine introduced in
shorebirdtech#352. A prefetched patch is just a `PatchState::Downloaded`
document stored in a parallel namespace keyed by release_version:

  <storage_dir>/prefetched_patches/<rv>/patches/<n>/state.json
  <download_dir>/prefetched_patches/<rv>/<n>

This namespace is independent of the per-release `patches/` and
`download_dir/<n>` paths the active lifecycle owns, so the
release-version reset wipes the active store but leaves the prefetched
store intact. We construct an independent `PatchLifecycle` against the
prefetched roots to reuse `record_download_started`,
`record_download_complete`, `read_state`, and `decide_start` for free.

Wire shape

* Server side: extend `PatchCheckResponse` with an optional
  `available_release_versions: Vec<String>` field. Server-side, this is
  the set of release_versions on the requesting app's track that have a
  publishable patch. Older clients ignore the field.

* Client side: after the existing patch-check + install flow runs for
  the current release, iterate over the advertised siblings (excluding
  the current release_version), issue one extra `/patches/check` per
  sibling with `release_version` overridden, download the raw delta, and
  record it as `PatchState::Downloaded` in the prefetched namespace. No
  inflation or hash/signature verification happens at prefetch time --
  the running binary is the wrong inflation base for any release other
  than its own.

* Promotion: `handle_prior_boot_failure_if_necessary` now also tries to
  promote a prefetched delta whose release_version matches the
  now-running binary. The compressed bytes are moved into the active
  download root, a `Downloaded` state is written into the active
  `patches/<n>/`, then the existing `inflate -> check_hash ->
  record_install_complete -> promote_to_next_boot` pipeline runs against
  it. The new binary's `patch_public_key` is the right key for both
  hash verification and Strict-mode boot signing; no special handling
  needed.

Lifecycle and cleanup

* Cached siblings the server stops advertising are pruned via
  `retain_prefetched_release_versions` on every successful patch-check.
* A prefetched entry that fails inflation, hash check, or matches a
  known-bad patch number is dropped on attempt so the device doesn't
  loop. The promoted patch is marked `Bad` in the active lifecycle so
  `decide_start` short-circuits subsequent updates within the release.
* Errors in the prefetch tail are best-effort; they never affect the
  main update result.

Open items for the Shorebird team

* Server change to populate `available_release_versions` from the
  same-track release set with publishable patches.
* Decide whether `available_release_versions` should respect
  gradual-rollout bucketing for siblings (current behavior asks the
  server again per sibling, so per-release rollout decisions ride for
  free).
* Promotion currently runs on every init; if you'd rather gate it on a
  fresh `release_version` mismatch detected by `load_or_new_on_error`,
  that signal would have to be surfaced explicitly.
…ease-patches

# Conflicts:
#	library/src/updater.rs
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.

feat: pre-fetch patches for newer release versions to eliminate "unpatched window" after native updates

1 participant