fix(startup): wait for checks and report operation lock owners

This commit is contained in:
Дмитрий committed 2026-10-13 06:17:17 +05:00
1 parent d86a1676fe
commit 0c0d2db368
3 files changed
+49 -5

No files matched your search

@@ -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.
+1 -1
View File
@@ -100,7 +100,7 @@ pub(crate) fn validate_update(root: &Path) -> Result<DriverOutcome> {
fn run_inner(root: &Path, verb: Verb, validating_update: bool) -> Result<DriverOutcome> {
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)?)?;
+41 -4
View File
@@ -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<String, Resolution>, access: DatabaseAccess) -> Result<Preview> {
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 { &current_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<fs::File> {
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::File> {
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::<serde_json::Value>(&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);