fix: preserve schema 6 runtime migration path

This commit is contained in:
2026-09-17 05:01:53 +09:00
parent 96592b1a24
commit a4aeb6e7d3
2 changed files with 204 additions and 6 deletions
+203 -5
View File
@@ -21,6 +21,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
const SCHEMA_VERSION: u32 = 8; const SCHEMA_VERSION: u32 = 8;
const PREVIOUS_SCHEMA_VERSION: u32 = 7; const PREVIOUS_SCHEMA_VERSION: u32 = 7;
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 WORKER_FILE: &str = "worker.json"; const WORKER_FILE: &str = "worker.json";
@@ -542,11 +543,14 @@ fn plan_runtime_store_migration(
}; };
return Ok((plan, Vec::new())); return Ok((plan, Vec::new()));
} }
if current_schema_version != PREVIOUS_SCHEMA_VERSION { if !matches!(
current_schema_version,
LEGACY_SCHEMA_VERSION | PREVIOUS_SCHEMA_VERSION
) {
return Err(runtime_store_corrupt( return Err(runtime_store_corrupt(
&runtime_path, &runtime_path,
format!( format!(
"unsupported Runtime store schema version {schema_version}; expected {PREVIOUS_SCHEMA_VERSION} or {SCHEMA_VERSION}" "unsupported Runtime store schema version {schema_version}; expected {LEGACY_SCHEMA_VERSION}, {PREVIOUS_SCHEMA_VERSION}, or {SCHEMA_VERSION}"
), ),
)); ));
} }
@@ -705,17 +709,83 @@ struct MigratedWorkerDocuments {
execution: serde_json::Value, execution: serde_json::Value,
} }
fn migrate_worker_document( fn migrate_schema_v6_worker_document_to_v7(
mut document: serde_json::Value, mut document: serde_json::Value,
identity_path: &Path,
) -> Result<serde_json::Value, RuntimeError> {
let object = document.as_object_mut().ok_or_else(|| {
runtime_store_corrupt(
identity_path,
"Worker identity must be an object".to_string(),
)
})?;
let execution = object
.get_mut("execution")
.and_then(serde_json::Value::as_object_mut)
.ok_or_else(|| {
runtime_store_corrupt(
identity_path,
"Worker execution must be an object".to_string(),
)
})?;
let last_run_generation = execution
.remove("last_run_generation")
.and_then(|value| value.as_u64())
.ok_or_else(|| {
runtime_store_corrupt(
identity_path,
"Worker execution last_run_generation must be an unsigned integer".to_string(),
)
})?;
if let Some(binding) = execution
.get_mut("binding")
.and_then(serde_json::Value::as_object_mut)
{
let binding_run_generation = binding
.remove("run_generation")
.and_then(|value| value.as_u64())
.ok_or_else(|| {
runtime_store_corrupt(
identity_path,
"Worker execution binding run_generation must be an unsigned integer"
.to_string(),
)
})?;
if binding_run_generation != last_run_generation {
return Err(runtime_store_corrupt(
identity_path,
format!(
"Worker execution binding run_generation {binding_run_generation} does not match last_run_generation {last_run_generation}"
),
));
}
}
object.insert(
"schema_version".to_string(),
serde_json::Value::from(PREVIOUS_SCHEMA_VERSION),
);
Ok(document)
}
fn migrate_worker_document(
document: serde_json::Value,
source_schema_version: u32, source_schema_version: u32,
_mapping: Option<&LegacyWorkerIdentityMapping>, _mapping: Option<&LegacyWorkerIdentityMapping>,
identity_path: &Path, identity_path: &Path,
) -> Result<MigratedWorkerDocuments, RuntimeError> { ) -> Result<MigratedWorkerDocuments, RuntimeError> {
if source_schema_version != PREVIOUS_SCHEMA_VERSION { let mut document = if source_schema_version == LEGACY_SCHEMA_VERSION {
migrate_schema_v6_worker_document_to_v7(document, identity_path)?
} else {
document
};
if !matches!(
source_schema_version,
LEGACY_SCHEMA_VERSION | PREVIOUS_SCHEMA_VERSION
) {
return Err(runtime_store_corrupt( return Err(runtime_store_corrupt(
identity_path, identity_path,
format!( format!(
"unsupported Worker identity schema {source_schema_version}; expected {PREVIOUS_SCHEMA_VERSION}" "unsupported Worker identity schema {source_schema_version}; expected {LEGACY_SCHEMA_VERSION} or {PREVIOUS_SCHEMA_VERSION}"
), ),
)); ));
} }
@@ -2067,6 +2137,14 @@ mod tests {
}) })
} }
fn schema_v6_worker_document(worker_id: WorkerId) -> serde_json::Value {
let mut document = schema_v7_worker_document(worker_id);
document["schema_version"] = serde_json::json!(LEGACY_SCHEMA_VERSION);
document["execution"]["last_run_generation"] = serde_json::json!(7);
document["execution"]["binding"] = serde_json::json!({ "run_generation": 7 });
document
}
#[test] #[test]
fn startup_recovers_staging_created_before_source_backup() { fn startup_recovers_staging_created_before_source_backup() {
let temp = tempfile::tempdir().unwrap(); let temp = tempfile::tempdir().unwrap();
@@ -2163,6 +2241,126 @@ mod tests {
assert!(error.to_string().contains("exceeds limit")); assert!(error.to_string().contains("exceeds limit"));
} }
#[test]
fn schema_v6_worker_migrates_through_v7_to_split_v8_records() {
let worker_id = WorkerId::now_v7();
let root = tempfile::tempdir().unwrap();
let identity_path = root
.path()
.join(WORKERS_DIR)
.join(worker_id.to_string())
.join(WORKER_FILE);
let migrated = migrate_worker_document(
schema_v6_worker_document(worker_id),
LEGACY_SCHEMA_VERSION,
None,
&identity_path,
)
.unwrap();
assert_eq!(migrated.identity["schema_version"], SCHEMA_VERSION);
assert!(migrated.identity.get("request").is_none());
assert!(migrated.identity.get("execution").is_none());
assert_eq!(migrated.execution["schema_version"], SCHEMA_VERSION);
assert_eq!(
migrated.execution["request"]["worker_id"],
worker_id.to_string()
);
assert_eq!(migrated.execution["binding"], serde_json::json!({}));
assert!(
migrated.execution.get("last_run_generation").is_none(),
"schema 7 removed the duplicated run generation authority"
);
}
#[test]
fn schema_v6_stopped_worker_preserves_explicit_restore_without_binding() {
let worker_id = WorkerId::now_v7();
let root = tempfile::tempdir().unwrap();
let identity_path = root
.path()
.join(WORKERS_DIR)
.join(worker_id.to_string())
.join(WORKER_FILE);
let mut source = schema_v6_worker_document(worker_id);
source["status"] = serde_json::json!("stopped");
source["execution"]["binding"] = serde_json::Value::Null;
source["execution"]["restore_intent"] = serde_json::json!("explicit");
let migrated =
migrate_worker_document(source, LEGACY_SCHEMA_VERSION, None, &identity_path).unwrap();
assert_eq!(migrated.identity["status"], "stopped");
assert_eq!(migrated.execution["binding"], serde_json::Value::Null);
assert_eq!(migrated.execution["restore_intent"], "explicit");
}
#[test]
fn schema_v6_worker_rejects_mismatched_run_generation_authority() {
let worker_id = WorkerId::now_v7();
let mut source = schema_v6_worker_document(worker_id);
source["execution"]["binding"]["run_generation"] = serde_json::json!(6);
let error = migrate_worker_document(
source,
LEGACY_SCHEMA_VERSION,
None,
Path::new("worker.json"),
)
.unwrap_err();
assert!(
error
.to_string()
.contains("does not match last_run_generation 7")
);
}
#[test]
fn schema_v6_runtime_store_is_migrated_in_place() {
let root = tempfile::tempdir().unwrap();
write_empty_schema_v7_store(root.path());
let runtime_path = root.path().join(RUNTIME_FILE);
let mut runtime_document: serde_json::Value =
read_json(&runtime_path, "read test Runtime").unwrap();
runtime_document["schema_version"] = serde_json::json!(LEGACY_SCHEMA_VERSION);
atomic_write_json(&runtime_path, &runtime_document, "write test Runtime").unwrap();
let worker_id = WorkerId::now_v7();
let worker_dir = root.path().join(WORKERS_DIR).join(worker_id.to_string());
fs::create_dir_all(&worker_dir).unwrap();
atomic_write_json(
&worker_dir.join(WORKER_FILE),
&schema_v6_worker_document(worker_id.clone()),
"write test Worker",
)
.unwrap();
let opened =
FsRuntimeStore::open_or_create(root.path().to_path_buf(), "test-runtime").unwrap();
assert_eq!(
stored_schema_version(root.path()),
u64::from(SCHEMA_VERSION)
);
assert!(
opened
.store
.load_runtime_state()
.unwrap()
.workers
.contains_key(&worker_id)
);
assert!(worker_dir.join(WORKER_EXECUTION_FILE).exists());
let execution: serde_json::Value = read_json(
&worker_dir.join(WORKER_EXECUTION_FILE),
"read migrated test execution",
)
.unwrap();
assert_eq!(execution["schema_version"], SCHEMA_VERSION);
assert_eq!(execution["binding"], serde_json::json!({}));
}
#[test] #[test]
fn schema_v7_worker_migration_splits_identity_from_execution_metadata() { fn schema_v7_worker_migration_splits_identity_from_execution_metadata() {
let root = tempfile::tempdir().unwrap(); let root = tempfile::tempdir().unwrap();
+1 -1
View File
@@ -6998,7 +6998,7 @@ mod tests {
assert!( assert!(
error error
.to_string() .to_string()
.contains("unsupported Runtime store schema version 2; expected 7 or 8") .contains("unsupported Runtime store schema version 2; expected 6, 7, or 8")
); );
let _ = std::fs::remove_dir_all(root); let _ = std::fs::remove_dir_all(root);