{"record":{"id":"17b6f20be8b1b345","repo":"astrid-runtime/astrid","slug":"mcp-gateway-readiness-changed-before-cleanup-at","errorCode":null,"errorMessage":"MCP gateway readiness changed before cleanup at {}","messagePattern":"MCP gateway readiness changed before cleanup at (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/astrid-cli/src/commands/mcp/lifecycle.rs","lineNumber":350,"sourceCode":"        use std::os::unix::fs::PermissionsExt;\n        std::fs::set_permissions(&temp, std::fs::Permissions::from_mode(0o600))?;\n    }\n    std::fs::rename(&temp, &path)\n        .with_context(|| format!(\"failed to publish {}\", path.display()))?;\n    Ok(())\n}\n\n/// Remove this gateway's readiness marker without deleting a successor's.\npub(crate) fn remove_gateway_ready(record: &GatewayReady) -> Result<()> {\n    let path = gateway_ready_path()?;\n    remove_gateway_ready_at(&path, record)\n}\n\nfn remove_gateway_ready_at(path: &Path, record: &GatewayReady) -> Result<()> {\n    match read_gateway_ready_at(path)? {\n        Some(current) if current == *record => std::fs::remove_file(path)\n            .with_context(|| format!(\"failed to remove {}\", path.display())),\n        Some(_) => anyhow::bail!(\n            \"MCP gateway readiness changed before cleanup at {}\",\n            path.display()\n        ),\n        None => Ok(()),\n    }\n}\n\n/// Create a private runtime directory, preserving the Astrid home boundary.\npub(crate) fn ensure_private_dir(path: &Path) -> Result<()> {\n    std::fs::create_dir_all(path).with_context(|| {\n        format!(\n            \"failed to create private runtime directory {}\",\n            path.display()\n        )\n    })?;\n    #[cfg(unix)]\n    {\n        use std::os::unix::fs::PermissionsExt;","sourceCodeStart":332,"sourceCodeEnd":368,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/mcp/lifecycle.rs#L332-L368","documentation":"remove_gateway_ready_at performs a compare-and-delete: it re-reads the readiness file and only removes it if it still matches the record the caller captured earlier. If the file now holds a different record, a newer gateway generation has taken over and deleting it would break the successor, so this error is raised. If the file is already gone, cleanup succeeds silently.","triggerScenarios":"remove_gateway_ready (or test ready_cleanup_cannot_remove_a_successor_record) attempts cleanup with an old GatewayReady record while the file has been overwritten by a newer gateway with different pid/principal/token.","commonSituations":"Gateway restarted between capturing the record and cleanup; overlapping test runs sharing a runtime dir; delayed shutdown of an old instance after a successor started.","solutions":["Treat as success-for-successor: skip removal since the new generation owns the record","Serialize gateway start/stop so an old instance can't clean up after a new one starts","Re-read the current record before cleanup and only remove if you own it","Use per-generation unique readiness paths if concurrency is expected"],"exampleFix":"// before\nremove_gateway_ready_at(&path, &old_record)?;\n// after\nif let Ok(Some(current)) = read_gateway_ready_at(&path) {\n    if current == old_record { remove_gateway_ready_at(&path, &old_record)?; }\n}","handlingStrategy":"try-catch","validationCode":"let current = read_gateway_ready_at(&path)?;\nif current.as_ref() != Some(record) { /* successor owns the record — skip removal */ }","typeGuard":null,"tryCatchPattern":"match remove_gateway_ready_at(&path, &record) {\n    Ok(()) => {},\n    Err(e) if e.to_string().contains(\"readiness changed\") => {}, // successor gateway owns it; ignore\n    Err(e) => return Err(e),\n}","preventionTips":["Capture the readiness record only from your own gateway startup","Serialize gateway restarts so old instances finish cleanup first","Use unique readiness paths per generation when overlap is possible"],"tags":["lifecycle","concurrency","mcp"],"backgroundTag":"invalid-state-transition","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}