fix: return UpdateInProgress status instead of erroring when another update is running (#335)

When `update()` is called while another update (typically the automatic
updater thread) is already running, the Rust updater previously bailed
with `UpdateError::UpdateAlreadyInProgress`, which surfaced in Dart as
`UpdateException: Update already in progress (unknown)`. This is the
single highest-volume `UpdateException` in customer telemetry, yet the
underlying situation is benign — the in-flight update continues on its
own, the caller simply did not start a new one.

Add a new `UpdateStatus::UpdateInProgress` variant and matching C
status code `SHOREBIRD_UPDATE_IN_PROGRESS = 4`. `updater::update()`
catches the `UpdateAlreadyInProgress` error from the lock helper and
maps it to `Ok(UpdateStatus::UpdateInProgress)`. The Dart wrapper
treats the new status as a successful return alongside
`SHOREBIRD_UPDATE_INSTALLED`, so `update()` no longer throws for this
case.

Update the existing `usage_during_hung_update` c_api test to assert the
new contract, and add a Dart test covering the in-progress return path.

Version skew: new Dart on an old engine still sees the legacy
`SHOREBIRD_UPDATE_ERROR` + "Update already in progress" message and
will still throw. The fix lands once both sides ship.

Partially addresses shorebirdtech/shorebird#3682 — does not resolve the
broader asymmetry of `update()` semantics (it still does not wait for
someone else's in-flight update to finish), which remains as v2 design
work in shorebirdtech/shorebird#3684.
This commit is contained in:
Eric Seidel
2026-04-07 18:22:04 -07:00
committed by GitHub
parent 2ae3760b95
commit 563f1b773a
6 changed files with 78 additions and 7 deletions
+7
View File
@@ -41,6 +41,13 @@
*/
#define SHOREBIRD_UPDATE_IS_BAD_PATCH 3
/**
* Another update was already in progress when this call was made. The
* already-running update will continue; the caller did not start a new one.
* This is a benign outcome, not an error.
*/
#define SHOREBIRD_UPDATE_IN_PROGRESS 4
/**
* Struct containing configuration parameters for the updater.
* Passed to all updater functions.
+12 -1
View File
@@ -63,6 +63,11 @@ pub const SHOREBIRD_UPDATE_HAD_ERROR: i32 = 2;
/// The downloaded patch was not installed because it was invalid.
pub const SHOREBIRD_UPDATE_IS_BAD_PATCH: i32 = 3;
/// Another update was already in progress when this call was made. The
/// already-running update will continue; the caller did not start a new one.
/// This is a benign outcome, not an error.
pub const SHOREBIRD_UPDATE_IN_PROGRESS: i32 = 4;
#[repr(C)]
pub struct UpdateResult {
pub status: i32,
@@ -1007,7 +1012,13 @@ mod test {
shorebird_start_update_thread();
// Wait for the thread to start.
std::thread::sleep(std::time::Duration::from_millis(100));
assert!(updater::update(None).is_err());
// When another update is already in progress, `update()` returns
// `UpdateStatus::UpdateInProgress` rather than surfacing an error.
// The in-flight update continues on its own.
assert_eq!(
updater::update(None).unwrap(),
crate::UpdateStatus::UpdateInProgress
);
}
// Unlock the lock, and wait for the thread to finish.
std::thread::sleep(std::time::Duration::from_millis(100));
+22 -1
View File
@@ -30,6 +30,10 @@ pub enum UpdateStatus {
UpdateInstalled,
UpdateHadError,
UpdateIsBadPatch,
// Another update was already in progress when this call was made. The
// already-running update will continue; the caller did not start a new
// one. This is a benign outcome, not an error.
UpdateInProgress,
}
impl Display for UpdateStatus {
@@ -42,6 +46,7 @@ impl Display for UpdateStatus {
f,
"Update available but previously failed to install. Not installing."
),
UpdateStatus::UpdateInProgress => write!(f, "Update already in progress"),
}
}
}
@@ -651,7 +656,23 @@ fn cleanup_download_artifacts(download_path: &Path) {
/// Synchronously checks for an update and downloads and installs it if available.
pub fn update(channel: Option<&str>) -> anyhow::Result<UpdateStatus> {
with_updater_thread_lock(|lock_state| update_internal(lock_state, channel))
match with_updater_thread_lock(|lock_state| update_internal(lock_state, channel)) {
Ok(status) => Ok(status),
Err(e) => {
// "Another update is already running" is a benign outcome — the
// in-progress update (typically the automatic updater thread) will
// continue on its own. Surface it as a non-error status so callers
// that monitor `update()` exceptions do not see it as a failure.
if matches!(
e.downcast_ref::<UpdateError>(),
Some(UpdateError::UpdateAlreadyInProgress)
) {
Ok(UpdateStatus::UpdateInProgress)
} else {
Err(e)
}
}
}
}
/// The first 4 bytes of any zstd compressed frame.