Skip to content

fix: Filter subscription events by collection before opening txn - #4909

Open
edjroz wants to merge 5 commits into
sourcenetwork:developfrom
edjroz:fix/subscription-collection-filter
Open

fix: Filter subscription events by collection before opening txn#4909
edjroz wants to merge 5 commits into
sourcenetwork:developfrom
edjroz:fix/subscription-collection-filter

Conversation

@edjroz

@edjroz edjroz commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Relevant issue(s)

Resolves #4896

Description

This PR adds a collection-level guard at the existing docID/CID filter site so the event is dropped before any work begins.

Changes

  • Extend subscriptionSelector (internal/db/subscriptions.go:24) with CheckCollectionFilter(collectionID string) bool, mirroring the existing CheckDocIDFilter / CheckCIDFilter shape.
  • Add a sibling interface targetCollectionSetter carrying SetTargetCollectionID(id string) so the subscription handler can stamp the resolved root CollectionID at subscribe-time without polluting the subscriptionSelector contract for non-subscription consumers.
  • On *request.Select: an unexported targetCollectionID field, a setter, and CheckCollectionFilter that returns s.targetCollectionID == "" || s.targetCollectionID == collectionID. Empty default preserves current behaviour for any caller that doesn't opt in.
  • In handleSubscription: resolve selection.Name via db.GetCollectionByName(ctx, name) once at subscribe-time and stamp If the collection doesn't exist the subscription request fails up-front via the existing GQL-error path.
  • Add the new check alongside the existing filters at the event-loop entry

Notes

  • The collection-filter check is opt-in via the targetCollectionSetter interface. A future selector type that implements subscriptionSelector without implementing targetCollectionSetter falls back to the empty-string allow-all default (i.e. current behaviour). This is intentional, we don't want unknown selector types to block legitimate subscriptions — but is worth noting .
  • The targetCollectionID field is written at subscribe-time and read inside the event-loop goroutine. The write happens-before the go launch, so no race today. A future change that moves the write past the goroutine boundary would need a lock.

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?

New tests in internal/db/subscription_collection_filter_test.go:

  • TestHandleSubscription_WrongCollectionEvent_OpensNoTxn — pins that a wrong-collection event does not increment db.previousTxnID. The atomic counter is incremented inside every NewTxn call (db.go:230), so a flat counter across the subscription processing window is direct evidence that subscriptions.go:74 was never reached. Confirmed RED on develop (counter advanced 6 → 7), GREEN with the fix (counter flat).
  • TestHandleSubscription_RightCollectionEvent_StillDelivered — control test pinning that same-collection

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

  • MacOS

edjroz added 2 commits June 8, 2026 20:23
Adds CheckCollectionFilter to *request.Select, resolves the target
collection's root CollectionID once at subscribe-time, and rejects
events from other collections in the existing docID/cid filter site
before opening a transaction.

Fixes sourcenetwork#4896
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 68cb66a5-5c5b-4ca5-b605-22dc87d7d4e0

📥 Commits

Reviewing files that changed from the base of the PR and between c93477d and 6881b67.

📒 Files selected for processing (1)
  • internal/db/subscriptions.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/db/subscriptions.go

📝 Walkthrough

Walkthrough

Adds collection-level event filtering for subscriptions: Select gains a targetCollectionID field and CheckCollectionFilter method; subscriptionSelector now requires CheckCollectionFilter; a new targetCollectionSetter interface enables stamping at subscribe-time; handleSubscription resolves and stamps the collection; the event loop skips mismatched-collection events.

Changes

Subscription collection-level event filtering

Layer / File(s) Summary
Collection filter contract and implementation
client/request/select.go, internal/db/subscriptions.go
Select gains targetCollectionID field, SetTargetCollectionID, and CheckCollectionFilter method. subscriptionSelector interface requires CheckCollectionFilter. New targetCollectionSetter interface allows selectors to be stamped with the target collection root ID at subscribe-time.
Subscribe-time setup and event-loop filtering
internal/db/subscriptions.go
handleSubscription resolves the target collection by name and calls SetTargetCollectionID on selectors implementing targetCollectionSetter. The event loop now skips events unless CheckCollectionFilter, CheckDocIDFilter, and CheckCIDFilter all pass.
Collection filter validation tests
internal/db/subscription_collection_filter_test.go
Two integration tests verify wrong-collection events are ignored without opening transactions or delivering responses, and right-collection events are delivered with data and no errors.

Sequence Diagram

sequenceDiagram
  participant Subscriber
  participant handleSubscription
  participant Selector as Selector (Select)
  participant EventLoop
  Subscriber->>handleSubscription: subscribe with Select
  handleSubscription->>handleSubscription: resolve collection by name
  handleSubscription->>Selector: SetTargetCollectionID(rootCollectionID)
  EventLoop->>Selector: CheckCollectionFilter(evt.CollectionID)
  Selector-->>EventLoop: true/false (accept/skip event)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
Extend subscriptionSelector interface with CheckCollectionFilter [#4896]
Implement CheckCollectionFilter on Select request types [#4896]
Resolve target collection at subscribe-time and stamp selector [#4896]
Add collection filter guard alongside docID/CID filters in event loop [#4896]

Possibly related PRs

  • sourcenetwork/defradb#4436: Also extends subscriptionSelector with early event-loop filter checks (CheckDocIDFilter, CheckCIDFilter) to skip irrelevant events before planner invocation.

Suggested labels

bug, area/query

Suggested reviewers

  • jsimnz
  • fredcarle

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
internal/db/subscription_collection_filter_test.go (1)

79-98: ⚡ Quick win

Make the “no response” assertion deterministic over a time window.

Line 97 uses an instantaneous default check, so a slightly delayed wrong-collection response can slip through undetected. Prefer a bounded wait loop that continuously asserts both “no message” and “txn counter unchanged.”

Proposed test-shape adjustment
-	mid := db.previousTxnID.Load()
-	time.Sleep(200 * time.Millisecond)
-	after := db.previousTxnID.Load()
-
-	require.Equal(t, mid, after,
-		"subscription must not open a transaction for a wrong-collection event; counter advanced from %d to %d",
-		mid, after)
-
-	// Belt-and-braces: also confirm nothing surfaces on the response channel.
-	select {
-	case got, ok := <-subCh:
-		if !ok {
-			t.Fatalf("subscription channel closed unexpectedly")
-		}
-		t.Fatalf("expected no response for wrong-collection event, got %+v", got)
-	default:
-	}
+	mid := db.previousTxnID.Load()
+	deadline := time.After(300 * time.Millisecond)
+	ticker := time.NewTicker(20 * time.Millisecond)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case got, ok := <-subCh:
+			if !ok {
+				t.Fatalf("subscription channel closed unexpectedly")
+			}
+			t.Fatalf("expected no response for wrong-collection event, got %+v", got)
+		case <-ticker.C:
+			current := db.previousTxnID.Load()
+			require.Equal(t, mid, current,
+				"subscription must not open a transaction for a wrong-collection event; counter advanced from %d to %d",
+				mid, current)
+		case <-deadline:
+			return
+		}
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/db/subscription_collection_filter_test.go` around lines 79 - 98, The
instantaneous default select can miss slightly delayed messages; replace it with
a bounded wait that repeatedly (or for a fixed duration) asserts both that
db.previousTxnID.Load() remains equal to mid and that no message is received on
subCh—for example, use a time.After timeout (e.g., 200ms) with a loop/select
that checks case <-subCh (fail if received) and case <-time.After(shortInterval)
to re-check the txn counter until the overall timeout expires, then succeed if
no messages arrived and the counter stayed unchanged; reference the variables
mid, after (or directly db.previousTxnID.Load()) and the channel subCh in the
updated logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/db/subscription_collection_filter_test.go`:
- Around line 79-98: The instantaneous default select can miss slightly delayed
messages; replace it with a bounded wait that repeatedly (or for a fixed
duration) asserts both that db.previousTxnID.Load() remains equal to mid and
that no message is received on subCh—for example, use a time.After timeout
(e.g., 200ms) with a loop/select that checks case <-subCh (fail if received) and
case <-time.After(shortInterval) to re-check the txn counter until the overall
timeout expires, then succeed if no messages arrived and the counter stayed
unchanged; reference the variables mid, after (or directly
db.previousTxnID.Load()) and the channel subCh in the updated logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c8ebd52e-1db7-4e52-ad27-a87a34458d56

📥 Commits

Reviewing files that changed from the base of the PR and between 0a227b6 and 068371b.

📒 Files selected for processing (3)
  • client/request/select.go
  • internal/db/subscription_collection_filter_test.go
  • internal/db/subscriptions.go
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-03-27T08:24:16.083Z
Learnt from: islamaliev
Repo: sourcenetwork/defradb PR: 4661
File: http/middleware.go:93-93
Timestamp: 2026-03-27T08:24:16.083Z
Learning: When handling `client.ErrNotAuthorizedToPerformOperation` in the defradb repository, treat it as an unauthenticated/missing-identity condition (not insufficient permissions). Map it to HTTP 401 Unauthorized rather than 403 Forbidden; this mapping is intentional and consistent with prior behavior (pre-PR `#4661`).

Applied to files:

  • internal/db/subscription_collection_filter_test.go
  • client/request/select.go
  • internal/db/subscriptions.go
🔇 Additional comments (2)
client/request/select.go (1)

42-56: LGTM!

Also applies to: 164-170

internal/db/subscriptions.go (1)

28-28: LGTM!

Also applies to: 31-37, 51-65, 93-100

edjroz and others added 2 commits June 9, 2026 15:02
Replace the instantaneous default select with a blocking select-with-timeout so a slightly-delayed wrong-collection response can no longer slip through undetected.
@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.33333% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 54.50%. Comparing base (abfbbad) to head (6881b67).
⚠️ Report is 1 commits behind head on develop.

Files with missing lines Patch % Lines
internal/db/subscriptions.go 63.64% 2 Missing and 2 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##           develop    #4909       +/-   ##
============================================
- Coverage    76.27%   54.50%   -21.78%     
============================================
  Files          606      589       -17     
  Lines        46838    44884     -1954     
============================================
- Hits         35724    24460    -11264     
- Misses        8311    17960     +9649     
+ Partials      2803     2464      -339     
Flag Coverage Δ
all-tests 54.50% <73.33%> (-21.78%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
client/request/select.go 60.50% <100.00%> (-27.32%) ⬇️
internal/db/subscriptions.go 70.51% <63.64%> (-1.55%) ⬇️

... and 319 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update abfbbad...6881b67. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

Subscription event loop fans out to wrong-collection events, causing wasted planner work

3 participants