fix: archive orphaned workers during migration

This commit is contained in:
2026-09-17 05:12:47 +09:00
parent a4aeb6e7d3
commit f4b34a289e
+87 -1
View File
@@ -24,6 +24,7 @@ const PREVIOUS_SCHEMA_VERSION: u32 = 7;
const LEGACY_SCHEMA_VERSION: u32 = 6; const LEGACY_SCHEMA_VERSION: u32 = 6;
const RUNTIME_FILE: &str = "runtime.json"; const RUNTIME_FILE: &str = "runtime.json";
const WORKERS_DIR: &str = "workers"; const WORKERS_DIR: &str = "workers";
const ORPHANED_WORKERS_DIR: &str = "orphaned-workers";
const WORKER_FILE: &str = "worker.json"; const WORKER_FILE: &str = "worker.json";
const WORKER_EXECUTION_FILE: &str = "execution.json"; const WORKER_EXECUTION_FILE: &str = "execution.json";
const WORKER_METADATA_FILE: &str = "metadata.json"; const WORKER_METADATA_FILE: &str = "metadata.json";
@@ -466,6 +467,7 @@ pub struct FsRuntimeStoreMigrationPlan {
pub target_schema_version: u32, pub target_schema_version: u32,
pub migration_required: bool, pub migration_required: bool,
pub worker_count: usize, pub worker_count: usize,
pub archived_orphan_worker_count: usize,
pub migrated_worker_aggregate_count: usize, pub migrated_worker_aggregate_count: usize,
pub migrated_diagnostic_worker_ref_count: usize, pub migrated_diagnostic_worker_ref_count: usize,
pub cleared_diagnostic_worker_ref_count: usize, pub cleared_diagnostic_worker_ref_count: usize,
@@ -534,6 +536,7 @@ fn plan_runtime_store_migration(
target_schema_version: SCHEMA_VERSION, target_schema_version: SCHEMA_VERSION,
migration_required: false, migration_required: false,
worker_count: 0, worker_count: 0,
archived_orphan_worker_count: 0,
migrated_worker_aggregate_count: 0, migrated_worker_aggregate_count: 0,
migrated_diagnostic_worker_ref_count: 0, migrated_diagnostic_worker_ref_count: 0,
cleared_diagnostic_worker_ref_count: 0, cleared_diagnostic_worker_ref_count: 0,
@@ -563,6 +566,7 @@ fn plan_runtime_store_migration(
.map_err(|error| runtime_io_error("read workers", &workers_dir, error))?; .map_err(|error| runtime_io_error("read workers", &workers_dir, error))?;
entries.sort_by_key(|entry| entry.file_name()); entries.sort_by_key(|entry| entry.file_name());
let mut planned = Vec::with_capacity(entries.len()); let mut planned = Vec::with_capacity(entries.len());
let mut archived_orphan_worker_count = 0;
let mut target_ids = std::collections::BTreeSet::new(); let mut target_ids = std::collections::BTreeSet::new();
for entry in entries { for entry in entries {
let source_dir = entry.path(); let source_dir = entry.path();
@@ -585,6 +589,7 @@ fn plan_runtime_store_migration(
source, source,
})? })?
{ {
archived_orphan_worker_count += 1;
continue; continue;
} }
let snapshot: serde_json::Value = read_json(&snapshot_path, "read Worker record")?; let snapshot: serde_json::Value = read_json(&snapshot_path, "read Worker record")?;
@@ -687,6 +692,7 @@ fn plan_runtime_store_migration(
target_schema_version: SCHEMA_VERSION, target_schema_version: SCHEMA_VERSION,
migration_required: true, migration_required: true,
worker_count: planned.len(), worker_count: planned.len(),
archived_orphan_worker_count,
migrated_worker_aggregate_count, migrated_worker_aggregate_count,
migrated_diagnostic_worker_ref_count: diagnostic_refs.migrated, migrated_diagnostic_worker_ref_count: diagnostic_refs.migrated,
cleared_diagnostic_worker_ref_count: diagnostic_refs.cleared, cleared_diagnostic_worker_ref_count: diagnostic_refs.cleared,
@@ -1440,6 +1446,45 @@ fn migrate_runtime_store(
Ok(plan) Ok(plan)
} }
fn archive_orphaned_worker_directories(root: &Path) -> Result<usize, RuntimeError> {
let workers_dir = root.join(WORKERS_DIR);
let mut orphaned = fs::read_dir(&workers_dir)
.map_err(|source| runtime_io_error("read Worker records", &workers_dir, source))?
.collect::<Result<Vec<_>, _>>()
.map_err(|source| runtime_io_error("read Worker records", &workers_dir, source))?
.into_iter()
.filter_map(|entry| {
let path = entry.path();
(!path.join(WORKER_FILE).is_file()).then_some((entry.file_name(), path))
})
.collect::<Vec<_>>();
orphaned.sort_by(|(left, _), (right, _)| left.cmp(right));
if orphaned.is_empty() {
return Ok(0);
}
let archive_dir = root.join(ORPHANED_WORKERS_DIR);
fs::create_dir_all(&archive_dir).map_err(|source| {
runtime_io_error("create orphaned Worker archive", &archive_dir, source)
})?;
for (name, source_dir) in &orphaned {
let target_dir = archive_dir.join(name);
if target_dir.exists() {
return Err(runtime_store_corrupt(
&target_dir,
format!(
"orphaned Worker archive target already exists for {}",
source_dir.display()
),
));
}
fs::rename(source_dir, &target_dir).map_err(|source| {
runtime_io_error("archive orphaned Worker directory", source_dir, source)
})?;
}
Ok(orphaned.len())
}
fn migrate_runtime_store_in_place( fn migrate_runtime_store_in_place(
root: &Path, root: &Path,
runtime_id: &str, runtime_id: &str,
@@ -1521,6 +1566,16 @@ fn migrate_runtime_store_in_place(
} }
} }
let archived_orphan_worker_count = archive_orphaned_worker_directories(root)?;
if archived_orphan_worker_count != plan.archived_orphan_worker_count {
return Err(runtime_store_corrupt(
root,
format!(
"archived {archived_orphan_worker_count} orphaned Worker directories; migration plan expected {}",
plan.archived_orphan_worker_count
),
));
}
let (document, _) = migrate_runtime_document( let (document, _) = migrate_runtime_document(
document, document,
plan.current_schema_version, plan.current_schema_version,
@@ -2070,7 +2125,7 @@ mod tests {
} }
#[test] #[test]
fn schema_v4_migration_plan_ignores_orphan_worker_directories() { fn schema_v7_migration_archives_orphan_worker_directories() {
let root = tempfile::tempdir().unwrap(); let root = tempfile::tempdir().unwrap();
fs::write( fs::write(
root.path().join(RUNTIME_FILE), root.path().join(RUNTIME_FILE),
@@ -2103,6 +2158,21 @@ mod tests {
assert_eq!(plan.current_schema_version, PREVIOUS_SCHEMA_VERSION); assert_eq!(plan.current_schema_version, PREVIOUS_SCHEMA_VERSION);
assert_eq!(plan.target_schema_version, SCHEMA_VERSION); assert_eq!(plan.target_schema_version, SCHEMA_VERSION);
assert_eq!(plan.worker_count, 0); assert_eq!(plan.worker_count, 0);
assert_eq!(plan.archived_orphan_worker_count, 1);
let opened =
FsRuntimeStore::open_or_create(root.path().to_path_buf(), "runtime-test").unwrap();
assert!(opened.state.unwrap().diagnostics.is_empty());
assert!(!root.path().join(WORKERS_DIR).join("orphan").exists());
assert!(
root.path()
.join(ORPHANED_WORKERS_DIR)
.join("orphan")
.join("session")
.join("history.json")
.is_file()
);
} }
fn schema_v7_worker_document(worker_id: WorkerId) -> serde_json::Value { fn schema_v7_worker_document(worker_id: WorkerId) -> serde_json::Value {
@@ -2335,6 +2405,14 @@ mod tests {
"write test Worker", "write test Worker",
) )
.unwrap(); .unwrap();
let orphan_history = root
.path()
.join(WORKERS_DIR)
.join("legacy-orphan")
.join("session")
.join("history.json");
fs::create_dir_all(orphan_history.parent().unwrap()).unwrap();
fs::write(&orphan_history, b"[]").unwrap();
let opened = let opened =
FsRuntimeStore::open_or_create(root.path().to_path_buf(), "test-runtime").unwrap(); FsRuntimeStore::open_or_create(root.path().to_path_buf(), "test-runtime").unwrap();
@@ -2359,6 +2437,14 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!(execution["schema_version"], SCHEMA_VERSION); assert_eq!(execution["schema_version"], SCHEMA_VERSION);
assert_eq!(execution["binding"], serde_json::json!({})); assert_eq!(execution["binding"], serde_json::json!({}));
assert!(
root.path()
.join(ORPHANED_WORKERS_DIR)
.join("legacy-orphan")
.join("session")
.join("history.json")
.is_file()
);
} }
#[test] #[test]