Skip to content

fix: Resolve storage leak upon document deletion#4451

Open
SharanRP wants to merge 2 commits intosourcenetwork:developfrom
SharanRP:fix/storage-leak-4419
Open

fix: Resolve storage leak upon document deletion#4451
SharanRP wants to merge 2 commits intosourcenetwork:developfrom
SharanRP:fix/storage-leak-4419

Conversation

@SharanRP
Copy link
Contributor

Relevant issue(s)

Resolves #4419

Description

Fixes a storage leak where document data (DataStore keys and secondary indexes) persisted after deletion. Updated applyDelete to explicitly purge Primary, Value, and Field keys from the projected state while preserving DAG history for P2P sync.

The fix targets the local projected state (DataStore). BlockStore blocks (DAG history) are intentionally preserved to allow P2P sync of the deletion event, which is standard CRDT behavior but may result in non-zero growth of the BlockStore.

Tasks

  • I made sure the code is well commented, particularly hard-to-understand areas.
  • I made sure the repository-held documentation is changed accordingly.
  • I made sure the pull request title adheres to the conventional commit style (the subset used in the project can be found in tools/configs/chglog/config.yml).
  • I made sure to discuss its limitations such as threats to validity, vulnerability to mistake and misuse, robustness to invalidation of assumptions, resource requirements, ...

How has this been tested?

Added TestStorageFreedUponDelete regression test. It performs a raw iteration of the Rootstore to verify that no active data keys remain for a deleted document.

Run: go test ./tests/integration/mutation/delete/ -v -run TestStorageFreedUponDelete

Specify the platform(s) on which this was tested:

  • Linux

@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

📝 Walkthrough

Walkthrough

Deletes indexed document, primary key (DS entry), ValueKey, and all FieldKey entries during applyDelete; adds an integration test that enumerates root store keys after deletion to assert data keys are freed across storage backends.

Changes

Cohort / File(s) Summary
Delete Operation Storage Cleanup
internal/db/collection_delete.go
In applyDelete: parse docID from primaryKey and delete indexed document; remove primary key (DS entry); delete short-collection ValueKey (object marker); iterate datastore keys with the document prefix and delete all associated FieldKey entries (including composite/legacy).
Storage Leak Validation Tests
tests/integration/mutation/delete/storage_leak_test.go
Adds TestStorageFreedUponDelete and helpers (storageCheckAction, assertNoDataKeys) to enumerate RootStore keys post-delete and assert only allowed DeletedKey/PriorityKey remain; runs across Badger and file-backed stores via Go client.

Sequence Diagram(s)

sequenceDiagram
  participant TestRunner as Test Runner
  participant GoClient as Go Client
  participant ApplyDelete as applyDelete
  participant RootStore as RootStore
  participant IndexStore as IndexStore

  TestRunner->>GoClient: request Delete(doc primaryKey)
  GoClient->>ApplyDelete: invoke applyDelete(transaction, primaryKey)
  ApplyDelete->>IndexStore: parse primaryKey -> delete indexed document
  ApplyDelete->>RootStore: begin txn -> delete primary key (DS entry)
  ApplyDelete->>RootStore: compute short collection ID -> delete ValueKey (object marker)
  ApplyDelete->>RootStore: iterate keys with doc prefix -> delete FieldKey entries
  ApplyDelete->>RootStore: commit txn
  GoClient->>TestRunner: return delete result
  TestRunner->>GoClient: run storageCheckAction
  GoClient->>RootStore: iterate all keys -> return key list
  TestRunner->>TestRunner: assertNoDataKeys verifies only DeletedKey/PriorityKey remain
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
Test for storage leaks — ensure space is freed on delete [#4419]

Suggested reviewers

  • fredcarle
  • shahzadlone

🧹 Recent nitpick comments
internal/db/collection_delete.go (1)

194-208: Consider reusing shortID to avoid redundant lookups.

GetShortCollectionID is called here at line 195, and again at line 265 for branchable collections. Since the collection ID doesn't change within this function, you could store the result from the first call and reuse it in the branchable section.

♻️ Proposed refactor to reuse shortID
 // Delete ValueKey (Object Marker)
-shortID, err := id.GetShortCollectionID(ctx, c.Version().CollectionID)
-if err != nil {
-	return err
-}
+// shortID is reused later for branchable collections

Then at line 265, reuse the already-retrieved shortID:

 if c.def.IsBranchable {
-	shortID, err := id.GetShortCollectionID(ctx, c.Version().CollectionID)
-	if err != nil {
-		return err
-	}
+	// shortID already retrieved above
 
 	collectionCRDT := crdt.NewCollection(
📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b7d920 and c5a4df1.

📒 Files selected for processing (2)
  • internal/db/collection_delete.go
  • tests/integration/mutation/delete/storage_leak_test.go
🧰 Additional context used
🧬 Code graph analysis (2)
internal/db/collection_delete.go (3)
internal/db/id/collection.go (1)
  • GetShortCollectionID (24-50)
internal/keys/datastore_doc.go (4)
  • DataStoreKey (37-42)
  • InstanceType (23-23)
  • ValueKey (27-27)
  • NewDataStoreKey (54-56)
internal/datastore/keyedstore.go (1)
  • IterOptions (55-98)
tests/integration/mutation/delete/storage_leak_test.go (2)
internal/datastore/keyedstore.go (1)
  • IterOptions (55-98)
internal/keys/datastore_doc.go (4)
  • DecodeDataStoreKey (221-271)
  • InstanceType (23-23)
  • DeletedKey (31-31)
  • PriorityKey (29-29)
🔇 Additional comments (8)
internal/db/collection_delete.go (4)

174-181: LGTM! Indexed document deletion is correctly placed.

The indexed document is properly deleted before proceeding with other data cleanup. Error handling is consistent with the existing patterns in this function.


188-192: LGTM! Primary key deletion is correctly implemented.

The primary key is deleted from the datastore with proper error handling.


210-227: LGTM! Prefix-based iteration correctly addresses composite and legacy field cleanup.

The approach of iterating over all keys with the document prefix ensures that composite namespace keys and any historical field IDs from earlier schema versions are caught, addressing the concern from the previous review about keys leaking.


229-251: LGTM! Collect-then-delete pattern avoids iterator invalidation.

Collecting keys into a slice before deleting is the correct approach to avoid modifying the datastore while iterating. The error handling is consistent.

tests/integration/mutation/delete/storage_leak_test.go (4)

17-24: LGTM! Clean action struct design.

The storageCheckAction struct follows the test action pattern with SetState and state reference. The AssertKeys callback provides flexibility for different assertion types.


25-59: LGTM! Execute implementation is robust.

The implementation correctly:

  • Uses type assertion to verify Rootstore() availability
  • Properly closes the iterator with defer
  • Handles iteration errors appropriately
  • Fails fast with clear error messages

61-87: LGTM! Leak detection logic correctly allows expected post-delete keys.

The assertion properly:

  • Filters to only check data keys (prefixed with 'd')
  • Fails on decode errors (addressing the previous review feedback)
  • Allows DeletedKey (tombstones) and PriorityKey entries to remain
  • Treats any other active key (Value, Field, Primary) as a leak

89-116: LGTM! Test coverage is appropriate for the storage leak fix.

The test:

  • Uses a simple schema to minimize noise
  • Exercises the full create-then-delete flow
  • Runs across multiple storage backends (BadgerIMType, BadgerFileType, DefraIMType)
  • Verifies the specific objective from issue #4419

Consider adding additional test cases in the future to cover:

  • Multiple documents (delete one, verify others remain)
  • Documents with indexed fields

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@internal/db/collection_delete.go`:
- Around line 189-227: The current deletion loop only removes fields listed in
c.Version().Fields and can miss composite or legacy field keys (e.g., the
DocComposite namespace) and historical field IDs; update the deletion logic in
the collection delete flow to also remove the composite key and any remaining
legacy/unknown field keys by performing a prefix delete for the document
keyspace: after computing shortID and primaryKey.DocID (used in primaryKey and
valueKey), construct and delete the explicit composite key (DocComposite
namespace) using keys.DataStoreKey, and then call the datastore
prefix/scan+delete for the document's field key prefix (using CollectionShortID
and DocID) to remove any leftover historical field entries; ensure these
deletions use txn.Datastore().Delete or the datastore's prefix delete API and
handle errors similarly to existing deletes.
🧹 Nitpick comments (1)
tests/integration/mutation/delete/storage_leak_test.go (1)

62-94: Fail fast on unexpected datastore key decoding.

If a key has the datastore prefix but DecodeDataStoreKey fails, the current continue can mask leaks or key-format regressions. Consider surfacing the error to keep the test strict.

💡 Suggested tweak
-				dsKey, err := keys.DecodeDataStoreKey([]byte(k[1:]))
-				if err != nil {
-					continue
-				}
+				dsKey, err := keys.DecodeDataStoreKey([]byte(k[1:]))
+				if err != nil {
+					return fmt.Errorf("unexpected datastore key format: %s (%v)", k, err)
+				}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bffff75 and 4d1729b.

📒 Files selected for processing (2)
  • internal/db/collection_delete.go
  • tests/integration/mutation/delete/storage_leak_test.go
🧰 Additional context used
🧬 Code graph analysis (2)
tests/integration/mutation/delete/storage_leak_test.go (2)
internal/keys/datastore_doc.go (3)
  • DecodeDataStoreKey (221-271)
  • InstanceType (23-23)
  • ValueKey (27-27)
internal/keys/systemstore_field_id.go (1)
  • FieldID (21-24)
internal/db/collection_delete.go (5)
internal/core/block/store.go (1)
  • AddDelta (51-112)
internal/db/id/collection.go (1)
  • GetShortCollectionID (48-64)
internal/keys/datastore_doc.go (3)
  • DataStoreKey (37-42)
  • InstanceType (23-23)
  • ValueKey (27-27)
internal/db/id/field.go (1)
  • GetShortFieldID (28-86)
internal/keys/systemstore_field_id.go (1)
  • FieldID (21-24)
🔇 Additional comments (3)
tests/integration/mutation/delete/storage_leak_test.go (2)

17-60: Nice hook for rootstore inspection.

The storageCheckAction encapsulation and Rootstore iterator usage are clear and make the assertion plug‑in friendly.


97-124: Test scenario coverage looks good.

The end-to-end flow (schema → create → delete → storage check) maps well to the regression objective.

internal/db/collection_delete.go (1)

175-182: No action required. Index deletion is properly scoped within the transaction and cannot leave the index out of sync with the datastore. All index and datastore operations use the same transactional context passed through the call chain.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@SharanRP SharanRP force-pushed the fix/storage-leak-4419 branch from 4d1729b to 3b7d920 Compare January 30, 2026 05:12
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tests/integration/mutation/delete/storage_leak_test.go`:
- Around line 61-85: The leak-check currently ignores decode failures in
assertNoDataKeys's AssertKeys, which can hide problematic keys; change the
behavior in storageCheckAction.AssertKeys (inside assertNoDataKeys) to treat
errors from keys.DecodeDataStoreKey([]byte(k[1:])) as failures instead of
continuing — return a descriptive error including the key and decode error so
the test fails fast; keep the existing allowances for keys.DeletedKey and
keys.PriorityKey and only treat remaining active types as leaks.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d1729b and 3b7d920.

📒 Files selected for processing (2)
  • internal/db/collection_delete.go
  • tests/integration/mutation/delete/storage_leak_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/db/collection_delete.go
🧰 Additional context used
🧬 Code graph analysis (1)
tests/integration/mutation/delete/storage_leak_test.go (1)
internal/keys/datastore_doc.go (4)
  • DecodeDataStoreKey (221-271)
  • InstanceType (23-23)
  • DeletedKey (31-31)
  • PriorityKey (29-29)
🔇 Additional comments (3)
tests/integration/mutation/delete/storage_leak_test.go (3)

17-24: LGTM: clean test action wiring.


25-59: LGTM: rootstore scan and error handling look solid.


89-116: LGTM: good end-to-end coverage for storage cleanup.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@SharanRP SharanRP force-pushed the fix/storage-leak-4419 branch from 3b7d920 to c5a4df1 Compare January 30, 2026 05:22
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.

Test for storage leaks

2 participants