fix: archive orphaned workers during migration
This commit is contained in:
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user