Skip to content

[ENH] Hard delete wal3 collections. - #4840

Merged
rescrv merged 5 commits into
rescrv/integrate-wal3-gcfrom
rescrv/wal3-destroy
Jun 16, 2025
Merged

[ENH] Hard delete wal3 collections.#4840
rescrv merged 5 commits into
rescrv/integrate-wal3-gcfrom
rescrv/wal3-destroy

Conversation

@rescrv

@rescrv rescrv commented Jun 13, 2025

Copy link
Copy Markdown
Contributor

Description of changes

This PR introduces code to destroy a wal3 log by deleting everything it reasonably could be known to
contain.

Test plan

Two new integration tests passed.

Documentation Changes

N/A

@github-actions

Copy link
Copy Markdown

Reviewer Checklist

Please leverage this checklist to ensure your code review is thorough before approving

Testing, Bugs, Errors, Logs, Documentation

  • Can you think of any use case in which the code does not behave as intended? Have they been tested?
  • Can you think of any inputs or external events that could break the code? Is user input validated and safe? Have they been tested?
  • If appropriate, are there adequate property based tests?
  • If appropriate, are there adequate unit tests?
  • Should any logging, debugging, tracing information be added or removed?
  • Are error messages user-friendly?
  • Have all documentation changes needed been made?
  • Have all non-obvious changes been commented?

System Compatibility

  • Are there any potential impacts on other parts of the system or backward compatibility?
  • Does this change intersect with any items on our roadmap, and if so, is there a plan for fitting them together?

Quality

  • Is this code of a unexpectedly high quality (Readability, Modularity, Intuitiveness)

@propel-code-bot

propel-code-bot Bot commented Jun 13, 2025

Copy link
Copy Markdown
Contributor

Implement Hard Delete for wal3 Collections & Enhance Log Contention Handling

This PR introduces a comprehensive 'destroy' mechanism for wal3 logs, enabling hard deletion of all WAL3-managed files in object storage tied to a collection. It modifies, extends, and thoroughly tests the delete path: recursively traversing manifest, snapshots, fragments, cursors, and GC records, ensuring all are removed and reporting failure if any remain. In addition, major changes rework the log writer's contention/retry logic to robustly handle concurrent writers and garbage collection, notably making contention failovers more predictable and recoverable. S3 delete error handling and several test suites are also updated to reflect these new semantics and to harden durability against edge cases.

Key Changes:
• Add destroy.rs with a recursive destroy log routine (collection hard delete for WAL3 objects)
• Export destroy function in wal3 public interface and wire up in GC orchestrator (orchestrator_v2)
• Refactor log writer concurrency logic: add better LogContention error granularity and robust contention recovery/retry (LogWriter, writer.rs)
• Update all error handling, especially for S3 deletion (return NotFound as expected in GC/deletes)
• Add comprehensive integration tests (test_k8s_integration_97_destroy, destroy_wedge, contention) covering destruction, edge cases, and log contentions
• Update manifest and garbage collection file/layout semantics to ensure all generated garbage is cleaned and checked
• Adapt log-contention error handling logic across all callsites, including in log-service and GC orchestrator
• Add path utility helpers and update path conventions to ensure correct file prefixing and deletion semantics

Affected Areas:
• wal3 (destroy.rs, lib.rs, writer.rs, gc.rs, manifest.rs)
• garbage_collector_orchestrator_v2.rs
• log-service (push/read/GC apis and error handling)
• storage/s3.rs (delete error propagation and path prep)
• Integration/CI tests for destroy/contention/gc
• README and utility path generation

This summary was automatically generated by @propel-code-bot

Comment thread rust/wal3/src/lib.rs Outdated
Comment on lines +551 to +553
pub fn prefixed_fragment_path(prefix: &str, fragment_seq_no: FragmentSeqNo) -> String {
format!(
"{}/log/Bucket={:016x}/FragmentSeqNo={:016x}.parquet",
"{}{}/Bucket={:016x}/FragmentSeqNo={:016x}.parquet",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BestPractice]

Path concatenation is missing a separator - resulting paths will look like "<prefix>log/..." instead of "<prefix>/log/...".

format!(
-        "{}{}/Bucket={:016x}/FragmentSeqNo={:016x}.parquet",
-        prefix,
-        fragment_prefix(),
+        "{}/{}/Bucket={:016x}/FragmentSeqNo={:016x}.parquet",
+        prefix,
+        fragment_prefix(),
         fragment_seq_no.bucket(),
         fragment_seq_no.0,
     )

@rescrv
rescrv force-pushed the rescrv/integrate-wal3-gc branch from 08b3261 to 966390c Compare June 13, 2025 04:08
@rescrv
rescrv force-pushed the rescrv/wal3-destroy branch from 5bff3ed to f3f6d28 Compare June 13, 2025 04:09

for collection_id in ordered_soft_deleted_to_hard_delete_collections {
// TODO(rescrv): add wal3 hard delete here.
if let Err(err) = wal3::destroy(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[PerformanceOptimization]

[performance] Arc::new(self.storage.clone()) is rebuilt for every iteration even though the Storage instance is immutable. Construct the Arc once before the loop to avoid repeated allocation and reference-count churn.

Suggested change
if let Err(err) = wal3::destroy(
let storage = Arc::new(self.storage.clone());
for collection_id in ordered_soft_deleted_to_hard_delete_collections {
if let Err(err) = wal3::destroy(
Arc::clone(&storage),
collection_id.storage_prefix_for_log(),
)
.await
{
tracing::error!("could not destroy/hard delete wal3 instance: {err:?}");
// NOTE(rescrv): The alternative is to have a dead letter queue for this
// collection. Once hard deleted from sysdb it'll be "impossible" to find the
// garbage later, so leave the soft-deleted state to drive this state machine
// to resolution, probably with operator involvement as this should never
// happen and is tested.
continue;
}
self.sysdb_client
.finish_collection_deletion(
self.tenant.clone().ok_or(GarbageCollectorError::InvariantViolation(
"Expected tenant to be set".to_string(),
))?,
self.database_name.clone().ok_or(GarbageCollectorError::InvariantViolation(
"Expected database to be set".to_string(),
))?,
collection_id,
)
.await?;
}

Committable suggestion

Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

@rescrv
rescrv force-pushed the rescrv/integrate-wal3-gc branch from 966390c to 4487dfa Compare June 13, 2025 04:44
@rescrv
rescrv requested review from HammadB and Sicheng-Pan June 13, 2025 04:44
// garbage later, so leave the soft-deleted state to drive this state machine
// to resolution, probably with operator involvement as this should never
// happen and is tested.
continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: if log delete fails should we continue instead of return Err(...)? I suppose if we let it pass here then this collection entry got removed from sysdb, and the log dangles in object store

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose to let it hard delete the next collection in the list. Returning error stops that possibility. What would you do instead?

async move {
storage.delete(&file).await.unwrap();
storage
.delete(&file, DeleteOptions::default())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: do we need to think more carefully about what delete options to use here (and in other places as well)

@rescrv
rescrv force-pushed the rescrv/integrate-wal3-gc branch from 4487dfa to 85bde07 Compare June 14, 2025 06:02
@rescrv
rescrv force-pushed the rescrv/wal3-destroy branch from 3b98e8e to 97e142a Compare June 14, 2025 06:02
## Description of changes

The LogContention Error could manifest if two concurrent writers or a
writer and a garbage collector race.  This would fail CI cluster tests.

To remedy this problem, this PR reworks the retry logic to make sure
that when there is contention the writers will always reset to a good
place.  It does this by making a pair of writers alternate.  If either
writer gets stuck the test fails.

## Test plan

CI

## Documentation Changes

N/A
@rescrv
rescrv merged commit d811e8c into rescrv/integrate-wal3-gc Jun 16, 2025
108 of 114 checks passed
@rescrv
rescrv deleted the rescrv/wal3-destroy branch June 16, 2025 19:38
rescrv added a commit that referenced this pull request Jun 16, 2025
## Description of changes

This PR introduces code to destroy a wal3 log by deleting everything it
reasonably could be known to
contain.

## Test plan

Two new integration tests passed.

## Documentation Changes

N/A
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants