Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 61 additions & 4 deletions crates/tracedecay-graph-db/src/generation_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1840,6 +1840,11 @@ impl GraphDb {
self.retire_sealed_generation_store(locator)?;
return Ok(GenerationContentsDeletion::RetentionPending);
}
// Native row deletion may already have committed, but the generation
// is not collected until its independently durable sealed artifact is
// gone. If unlink refuses, keep the in-memory retirement state retryable
// instead of advertising collection while bytes still exist on disk.
self.retire_sealed_generation_store(locator)?;
let mut state = self.wait_verified_generations_write()?;
state.known.remove(locator);
state.quarantined.remove(locator);
Expand All @@ -1858,7 +1863,6 @@ impl GraphDb {
state.heads.remove(&locator.projection);
}
drop(state);
self.retire_sealed_generation_store(locator)?;
Ok(GenerationContentsDeletion::Deleted)
}

Expand Down Expand Up @@ -4001,20 +4005,73 @@ mod tests {
.delete_generation_contents(&locator, &|| Ok(()))
.expect_err("a failed sealed unlink must not complete retention");
assert!(
error
.to_string()
.contains("sealed directory removal failed")
matches!(
&error,
GraphDbError::Unavailable { message }
if message.starts_with("sealed generation store sealed directory removal failed:")
|| message.starts_with("sealed generation store sealed generation retirement refused:")
),
"an unremovable artifact must fail at the sealed-directory unlink boundary: {error}"
);
assert_eq!(std::fs::read(&directory).unwrap(), b"not a directory");
assert!(
!database
.wait_verified_generations_write()
.unwrap()
.collected
.contains(&locator),
"a failed sealed unlink must not mark its generation collected"
);

std::fs::remove_file(&directory).unwrap();
std::fs::create_dir(&directory).unwrap();
// Retirement requires an exact durable identity from the directory
// being removed. A receipt-free synthetic directory must never confer
// deletion authority when the strict collector is integrated.
let identity = generation.identity();
let receipt = serde_json::json!({
"version": 1,
"form": "compact",
"namespace": locator.projection.namespace.as_str(),
"projection": locator.projection.projection.as_str(),
"generation": locator.generation.as_str(),
"physical_namespace": namespace.as_str(),
"recovered_digest": sealed_digest(&generation).as_str(),
"entities": 0,
"relations": 0,
"graph_format": GraphFormatVersion::current().get(),
"row_sum": null,
"base": null,
"source_generation": identity.source_generation.as_str(),
"watermark": identity.watermark.as_str()
});
std::fs::write(
directory.join("sealed.json"),
serde_json::to_vec(&receipt).unwrap(),
)
.unwrap();
std::fs::write(directory.join("derived-rows"), b"retired").unwrap();
let unrelated = directory.parent().unwrap().join("unrelated-entry");
std::fs::create_dir(&unrelated).unwrap();
std::fs::write(unrelated.join("sentinel"), b"untouched").unwrap();
assert!(matches!(
database.delete_generation_contents(&locator, &|| Ok(())),
Ok(GenerationContentsDeletion::Deleted)
));
assert_eq!(
std::fs::read(unrelated.join("sentinel")).unwrap(),
b"untouched",
"retiring one sealed generation must not traverse another child"
);
assert!(!directory.exists());
assert!(
database
.wait_verified_generations_write()
.unwrap()
.collected
.contains(&locator),
"only successful physical retirement may mark the generation collected"
);
// Idempotent replay after the directory is gone still succeeds.
assert!(matches!(
database.delete_generation_contents(&locator, &|| Ok(())),
Expand Down
Loading