diff --git a/changelog.d/20261013-update-check-start-contention.md b/changelog.d/20261013-update-check-start-contention.md new file mode 100644 index 0000000..24d7cf7 --- /dev/null +++ b/changelog.d/20261013-update-check-start-contention.md @@ -0,0 +1,7 @@ +--- +area: manager +type: fixed +audience: admins +title: Starting the server waits for update checks and identifies ongoing maintenance +--- +Downloading an update manifest no longer blocks startup. A start waits up to 30 seconds for a check to finish; other active maintenance still blocks it safely and shows its operation, process and start time. diff --git a/crates/coa-core/src/driver.rs b/crates/coa-core/src/driver.rs index 954f4ef..40c0964 100644 --- a/crates/coa-core/src/driver.rs +++ b/crates/coa-core/src/driver.rs @@ -100,7 +100,7 @@ pub(crate) fn validate_update(root: &Path) -> Result { fn run_inner(root: &Path, verb: Verb, validating_update: bool) -> Result { let root = fsx::canonicalize_lenient(root)?; let _update_lock = if !validating_update && matches!(verb, Verb::StartAll | Verb::StartWorld) { - Some(crate::update::operation_lock(&crate::registry::metadata_dir_for(&root)?)?) + Some(crate::update::operation_lock_for(&crate::registry::metadata_dir_for(&root)?, "starting server", true)?) } else { None }; if !validating_update && matches!(verb, Verb::StartAll | Verb::StartWorld) { crate::update::ensure_recovered(&crate::registry::metadata_dir_for(&root)?)?; diff --git a/crates/coa-core/src/update.rs b/crates/coa-core/src/update.rs index 677546e..7117396 100644 --- a/crates/coa-core/src/update.rs +++ b/crates/coa-core/src/update.rs @@ -308,9 +308,12 @@ pub fn preview(root: &Path, meta: &InstallMeta, source: &Source, trusted_key: &s pub fn preview_with(root: &Path, meta: &InstallMeta, source: &Source, trusted_key: &str, resolutions: &BTreeMap, access: DatabaseAccess) -> Result { refuse_for_docker(root)?; let meta_dir = crate::registry::metadata_dir_for(root)?; - let _lock = operation_lock(&meta_dir)?; - ensure_recovered(&meta_dir)?; + let observed_version = MetaDir::open(&meta_dir)?.1.core.version; let (m, _) = fetch_manifest(source, trusted_key)?; + let _lock = operation_lock_for(&meta_dir, "checking updates", false)?; + ensure_recovered(&meta_dir)?; + let (_, current_meta) = MetaDir::open(&meta_dir)?; + let meta = if current_meta.core.version != observed_version { ¤t_meta } else { meta }; check_manifest(&m)?; reject_downgrade(meta, &m)?; let mut items = plan(root, meta, &m, resolutions, None)?; @@ -780,11 +783,26 @@ pub fn rollback(root: &Path, meta_dir: &Path, id: &str, env: &dyn Env) -> Result } pub(crate) fn operation_lock(meta_dir: &Path) -> Result { + operation_lock_for(meta_dir, "server maintenance", false) +} + +pub(crate) fn operation_lock_for(meta_dir: &Path, operation: &str, wait_for_check: bool) -> Result { fs::create_dir_all(meta_dir)?; let lock = fs::OpenOptions::new().read(true).write(true).create(true).truncate(false).open(meta_dir.join("update.lock"))?; - if !fs4::fs_std::FileExt::try_lock_exclusive(&lock)? { - return Err(Error::Invalid("Another update, repair or recovery is in progress.".into())); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); + loop { + if fs4::fs_std::FileExt::try_lock_exclusive(&lock)? { break; } + let owner = fsx::read_json::(&meta_dir.join("update-lock-owner.json")).ok(); + let checking = owner.as_ref().and_then(|owner| owner.get("operation")).and_then(|value| value.as_str()) == Some("checking updates"); + if wait_for_check && checking && std::time::Instant::now() < deadline { + std::thread::sleep(std::time::Duration::from_millis(100)); + continue; + } + let details = owner.as_ref().and_then(|owner| Some(format!(" Operation: {}; Manager process: {}; started: {}.", owner.get("operation")?.as_str()?, owner.get("pid")?.as_u64()?, owner.get("started")?.as_str()?))).unwrap_or_default(); + return Err(Error::Invalid(format!("Another update, repair or recovery is in progress.{details} Wait for that operation to finish, then retry."))); } + let record = serde_json::json!({"operation": operation, "pid": std::process::id(), "started": chrono::Utc::now().to_rfc3339()}); + fsx::atomic_write_json(&meta_dir.join("update-lock-owner.json"), &record)?; Ok(lock) } @@ -1358,6 +1376,25 @@ mod tests { w } + #[test] + fn startup_waits_for_an_update_check_but_not_for_active_maintenance() { + let temp = tempfile::tempdir().unwrap(); + let checker = operation_lock_for(temp.path(), "checking updates", false).unwrap(); + let root = temp.path().to_path_buf(); + let release = std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(150)); + drop(checker); + }); + let start = operation_lock_for(&root, "starting server", true).unwrap(); + release.join().unwrap(); + let error = operation_lock(&root).unwrap_err().to_string(); + assert!(error.contains("starting server")); + assert!(error.contains(&std::process::id().to_string())); + drop(start); + // Stale owner metadata is harmless once the OS lock is released. + operation_lock(&root).unwrap(); + } + #[test] fn legacy_manager_realm_launcher_is_replaced_during_update() { let w = legacy_launcher_world(false);