Skip to content

fix(sink): verify durable storage before dropping replays - #4902

Open
PatrickLiu022 wants to merge 4 commits into
openmeterio:mainfrom
PatrickLiu022:fix/sink-exactly-once-v1beta231
Open

fix(sink): verify durable storage before dropping replays#4902
PatrickLiu022 wants to merge 4 commits into
openmeterio:mainfrom
PatrickLiu022:fix/sink-exactly-once-v1beta231

Conversation

@PatrickLiu022

@PatrickLiu022 PatrickLiu022 commented Aug 10, 2026

Copy link
Copy Markdown

Overview

Sink-side dedupe can classify a Kafka replay as already processed even when the corresponding event is absent from durable storage. This creates a post-accept loss window: the API succeeds and Kafka retains the event, but the replacement sink worker drops replay solely because Redis contains the stable ID.

We observed this during an EKS node rollout: 77 seconds of audited usage were absent from ClickHouse despite successful API responses and healthy producer metrics.

This change moves replay classification to flush planning and combines two batched checks:

  • Redis CheckUniqueBatch determines whether each ID is new or already known.
  • A batched ClickHouse existence lookup confirms whether each exact (namespace, source, id) is durable.

The resulting state machine is:

  • unique + not durable: insert, then set dedupe
  • unique + durable: do not insert; restore the missing dedupe key
  • existing + durable: do not insert
  • existing + not durable: reinsert without rewriting the existing dedupe key

Invalid/dropped events stay outside durability checks, and incomplete dedupe classifications fail closed instead of silently discarding an event.

Notes for reviewer

The implementation preserves batch behavior: one Redis classification call, one Redis Set pipeline, and ClickHouse existence queries grouped by namespace/source and chunked to 200 IDs. It removes the unsafe parse-time duplicate drop.

The tests deterministically cover both crash boundaries, dropped/missing identities, state restoration, incomplete classifications, and batch call counts.

Verification:

go test ./openmeter/sink/...

Companion repair tooling: https://github.com/voltagepark/takao/pull/2850

Summary by CodeRabbit

  • Bug Fixes

    • Improved event deduplication during batch processing and replay scenarios.
    • Prevented duplicate events from being persisted when durable storage already contains them.
    • Preserved offset reconciliation when deduplication encounters errors.
    • Skipped invalid or incomplete events safely during deduplication.
  • Performance

    • Added batched storage checks and deduplication writes to reduce redundant operations.
  • Tests

    • Added coverage for durable storage, replay handling, missing identities, invalid results, batching, and error propagation.

Greptile Summary

This PR moves replay classification from parse time into flush planning, combining Redis dedupe classification with ClickHouse durability checks to close the post-accept event-loss window.

  • Adds a four-state flush plan for unique/existing and durable/non-durable events.
  • Adds batched, namespace/source-grouped ClickHouse existence checks.
  • Defers dedupe-key updates until after successful persistence and Kafka offset handling.
  • Adds deterministic tests for replay recovery states, incomplete classifications, batching, and dedupe pipelines.

Confidence Score: 4/5

The PR appears safe to merge, with non-blocking cleanup needed for suppressed dropped-event warnings and an unused helper.

The durability-aware replay paths are coherently implemented and tested, while the accepted issues affect operational diagnostics and maintainability rather than durable event correctness.

Files Needing Attention: openmeter/sink/sink.go

Important Files Changed

Filename Overview
openmeter/sink/sink.go Introduces durability-aware flush planning and removes parse-time replay dropping; also suppresses configured DROP-event warnings and leaves one dead helper.
openmeter/sink/storage.go Extends sink storage with grouped and chunked exact-identity existence checks using the streaming query API.
openmeter/sink/sink_dedupe_test.go Covers the replay state matrix, incomplete dedupe classifications, and batched dedupe behavior.
openmeter/sink/storage_test.go Verifies existence checks are grouped by namespace and source and respect the query batch limit.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Buffered valid event] --> B[Redis CheckUniqueBatch]
  B --> C[ClickHouse HasEvents]
  C --> D{Dedupe and durability state}
  D -->|Unique, not durable| E[Insert event]
  D -->|Unique, durable| F[Skip insert]
  D -->|Existing, not durable| G[Reinsert event]
  D -->|Existing, durable| H[Skip insert]
  E --> I[Store Kafka offset]
  F --> I
  G --> I
  H --> I
  I --> J{Unique in Redis?}
  J -->|Yes| K[Set or restore dedupe key]
  J -->|No| L[Keep existing dedupe key]
Loading

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
### Issue 1
openmeter/sink/sink.go:407-410
**Dropped-event warnings are suppressed**

With a deduplicator configured, filtering `DROP` messages out of the flush plan prevents them from reaching the existing `LogDroppedEvents` branch in `persistToStorage`, so configured warnings disappear while their Kafka offsets still advance.

### Issue 2
openmeter/sink/sink.go:481-491
**Remove the unused dedupe helper**

`dedupeItemsFromMessages` has no callers and duplicates the iteration already performed by `planFlushMessages`, adding misleading maintenance surface without encapsulating any reused behavior.

```suggestion

```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(sink): reject incomplete dedupe clas..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Context used:

PatrickLiu022 and others added 4 commits August 10, 2026 15:45
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@PatrickLiu022
PatrickLiu022 requested a review from a team as a code owner August 10, 2026 22:52
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Flush processing now plans deduplication and durable-storage actions before persistence. It batches uniqueness and durability checks, persists only required messages, stores offsets, and updates deduplication state afterward.

Changes

Sink deduplication flow

Layer / File(s) Summary
Durable event existence checks
openmeter/sink/storage.go, openmeter/sink/storage_test.go
Storage now exposes HasEvents. ClickHouseStorage performs grouped, batched, paginated existence checks and validates query results. Tests cover batching and query filters.
Flush planning and persistence ordering
openmeter/sink/sink.go
Flush processing plans inserts from deduplication and durability results. It stores offsets before setting deduplication items and preserves offset reconciliation when deduplication fails.
Deduplication planning validation
openmeter/sink/sink_dedupe_test.go
Tests cover message classification, durable and non-durable events, invalid deduplication results, batched checks, and single-call deduplication writes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Flush
  participant Deduplicator
  participant ClickHouseStorage
  participant SinkStorage
  participant KafkaOffsets
  Flush->>Deduplicator: Check batch uniqueness
  Flush->>ClickHouseStorage: Check durable events
  Flush->>SinkStorage: Persist planned messages
  Flush->>KafkaOffsets: Store offsets
  Flush->>Deduplicator: Set planned dedupe items
Loading

Possibly related PRs

  • openmeterio/openmeter#3481: Both changes modify sink deduplication flow, including dropped messages and deduplication state updates.

Suggested reviewers: tothandras

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes verifying durable storage before dropping replayed events, which is the main change.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Comment thread openmeter/sink/sink.go
Comment on lines +407 to +410
for _, message := range messages {
item, ok := dedupeItemFromMessage(message)
if !ok {
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.

P2 Dropped-event warnings are suppressed

With a deduplicator configured, filtering DROP messages out of the flush plan prevents them from reaching the existing LogDroppedEvents branch in persistToStorage, so configured warnings disappear while their Kafka offsets still advance.

Prompt To Fix With AI
This is a comment left during a code review.
Path: openmeter/sink/sink.go
Line: 407-410

Comment:
**Dropped-event warnings are suppressed**

With a deduplicator configured, filtering `DROP` messages out of the flush plan prevents them from reaching the existing `LogDroppedEvents` branch in `persistToStorage`, so configured warnings disappear while their Kafka offsets still advance.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

Comment thread openmeter/sink/sink.go
Comment on lines +481 to +491
func dedupeItemsFromMessages(messages []sinkmodels.SinkMessage) []dedupe.Item {
items := make([]dedupe.Item, 0, len(messages))
for _, message := range messages {
item, ok := dedupeItemFromMessage(message)
if ok {
items = append(items, item)
}
}

return items
}

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.

P2 Remove the unused dedupe helper

dedupeItemsFromMessages has no callers and duplicates the iteration already performed by planFlushMessages, adding misleading maintenance surface without encapsulating any reused behavior.

Suggested change
func dedupeItemsFromMessages(messages []sinkmodels.SinkMessage) []dedupe.Item {
items := make([]dedupe.Item, 0, len(messages))
for _, message := range messages {
item, ok := dedupeItemFromMessage(message)
if ok {
items = append(items, item)
}
}
return items
}

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: openmeter/sink/sink.go
Line: 481-491

Comment:
**Remove the unused dedupe helper**

`dedupeItemsFromMessages` has no callers and duplicates the iteration already performed by `planFlushMessages`, adding misleading maintenance surface without encapsulating any reused behavior.

```suggestion

```

**Context Used:** AGENTS.md ([source](https://github.com/openmeterio/openmeter/blob/main/AGENTS.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (8)
openmeter/sink/storage.go (1)

106-123: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Query fan-out scales with distinct (namespace, source) pairs.

HasEvents issues at least one ClickHouse query per (namespace, source) group per flush, and this now runs on every flush cycle. A mixed-tenant buffer with many sources produces many sequential round trips before any insert happens, which adds latency directly to the flush critical path.

If the flush batch commonly spans many groups, consider a single query with a namespace filter plus an ID In list, or bounded parallel queries across groups. This is a follow-up idea, not a blocker.

🤖 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 `@openmeter/sink/storage.go` around lines 106 - 123, Reduce query fan-out in
HasEvents by avoiding one sequential query per groupedItems (namespace, source)
pair; consolidate existence checks into a single namespace/ID-filtered query or
bounded parallel queries while preserving durableItems results and error
propagation.

Source: Path instructions

openmeter/sink/sink_dedupe_test.go (3)

156-234: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add short intent comments to the replay-transition tests.

The four state transitions encode non-obvious crash semantics. For example, unique + durable means the process crashed after the ClickHouse write and before the Redis key was set. The test names carry the outcome but not the reason.

A one-line given/when/then per test makes the replay intent readable without cross-referencing planFlushMessages. Example for lines 186-203:

// given a replay whose event is already durable but whose dedupe key was lost in a crash
// when the flush is planned
// then the sink restores the dedupe key and skips a duplicate insert

Based on coding guidelines: "Begin non-trivial service or lifecycle subtests with concise given, when, and then intent comments."

🤖 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 `@openmeter/sink/sink_dedupe_test.go` around lines 156 - 234, Add concise
three-line given/when/then intent comments to the four replay-transition tests:
TestPlanFlushMessages_IgnoresDropMissingIdentity,
TestPlanFlushMessages_UniqueDurableSetsDedupeWithoutInsert,
TestPlanFlushMessages_ExistingNotDurableReinsertsWithoutSet, and
TestPlanFlushMessages_RejectsUnclassifiedDedupeResult. Describe the relevant
crash/replay state, the planFlushMessages action, and the expected outcome
without changing test behavior.

Source: Coding guidelines


284-303: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse newTestSink here.

This builds the same Sink inline. dedupeSet never touches Storage, so newTestSink(deduplicator, nil) covers it.

♻️ Suggested tweak
 	deduplicator := newCountingDeduplicator(nil)
-	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
-	s := &Sink{
-		config: SinkConfig{
-			Logger:       logger,
-			Tracer:       noop.NewTracerProvider().Tracer("test"),
-			Deduplicator: deduplicator,
-		},
-	}
+	s := newTestSink(deduplicator, nil)
🤖 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 `@openmeter/sink/sink_dedupe_test.go` around lines 284 - 303, Update
TestDedupeSet_UsesSingleBatchCall to construct the sink with
newTestSink(deduplicator, nil) instead of duplicating the inline Sink and logger
configuration. Preserve the existing dedupeSet call and assertions.

106-141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an error hook to countingStorage for the durability-check failure path.

planFlushMessages wraps a HasEvents failure into "failed to verify durable state for flush messages" and aborts the flush. That path is new in this PR and no test reaches it. countingStorage has no way to return an error, so the fail-closed guarantee is unverified.

💚 Suggested addition
 type countingStorage struct {
 	durable        dedupe.ItemSet
+	err            error
 	hasEventsCalls int
 	hasEventsSizes []int
 	hasEventsItems [][]dedupe.Item
 }
@@
 	s.hasEventsItems = append(s.hasEventsItems, slices.Clone(items))
 
+	if s.err != nil {
+		return nil, s.err
+	}
+
 	result := dedupe.ItemSet{}
func TestPlanFlushMessages_FailsWhenDurabilityCheckFails(t *testing.T) {
	// given a storage that cannot answer the durability question
	storage := newCountingStorage(nil)
	storage.err = errors.New("clickhouse unavailable")

	s := newTestSink(newCountingDeduplicator(nil), storage)

	// when planning a flush
	_, err := s.planFlushMessages(t.Context(), []sinkmodels.SinkMessage{
		testSinkMessage("tenant-a", "evt-1", "gateway"),
	})

	// then planning fails closed instead of inserting blindly
	require.ErrorContains(t, err, "failed to verify durable state")
}
🤖 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 `@openmeter/sink/sink_dedupe_test.go` around lines 106 - 141, Extend
countingStorage with an err field and make HasEvents return that error when
configured, while preserving its existing call tracking. Add
TestPlanFlushMessages_FailsWhenDurabilityCheckFails to configure the error,
invoke planFlushMessages with a test message, and assert the returned error
contains “failed to verify durable state”.

Source: Path instructions

openmeter/sink/storage_test.go (2)

68-102: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Nice grouping test. Two behaviors from this PR are still uncovered.

The grouping and filter assertions look solid. The new HasEvents contract also introduces two behaviors that no test currently reaches:

  • Validation: an item with an empty Namespace, Source, or ID must return the "namespace, source and id are required" error.
  • Error propagation: when ListEventsV2 fails, HasEvents must return the wrapped "failed to check event durability" error. The err field on hasEventStreamingStub already exists for exactly this.

A chunking case with more than existsQueryBatchSize IDs would also confirm that the 200-ID batching produces multiple calls, since the current test only asserts the upper bound.

💚 Sketch of the missing cases
func TestClickHouseStorageHasEvents_RejectsIncompleteItems(t *testing.T) {
	stub := newHasEventStreamingStub(t)
	storage := &ClickHouseStorage{config: ClickHouseStorageConfig{Streaming: stub}}

	_, err := storage.HasEvents(t.Context(), []dedupe.Item{{Namespace: "tenant-a", ID: "evt-1"}})
	require.ErrorContains(t, err, "namespace, source and id are required")
	require.Empty(t, stub.calls)
}

func TestClickHouseStorageHasEvents_PropagatesQueryError(t *testing.T) {
	stub := newHasEventStreamingStub(t)
	stub.err = errors.New("clickhouse down")
	storage := &ClickHouseStorage{config: ClickHouseStorageConfig{Streaming: stub}}

	_, err := storage.HasEvents(t.Context(), []dedupe.Item{
		{Namespace: "tenant-a", Source: "gateway", ID: "evt-1"},
	})
	require.ErrorContains(t, err, "failed to check event durability")
}

Error propagation matters most here: planFlushMessages turns that error into a flush failure, so it is a real replay-safety path.

🤖 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 `@openmeter/sink/storage_test.go` around lines 68 - 102, Extend the HasEvents
tests with coverage for incomplete dedupe.Item values, asserting the
required-fields error and no streaming calls; add a ListEventsV2 failure case
using hasEventStreamingStub.err and assert the wrapped “failed to check event
durability” error. Also add a batch-size case with more than
existsQueryBatchSize IDs and verify multiple calls are made.

Source: Path instructions


31-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The stub ignores Limit and Cursor, and mutates the caller's slice.

Two small things here:

  1. ListEventsV2 never honors params.Limit or params.Cursor, so the pagination branch in queryExistingItemsByNamespaceAndSource is never exercised. That is also why the loop-safety concern flagged in openmeter/sink/storage.go is invisible to this suite.
  2. Line 45: lo.FromPtr(params.ID.In) returns the caller's slice, and sort.Strings sorts it in place. Today HasEvents already sorts, so nothing breaks. A stub that mutates production input can still hide a future regression.
♻️ Suggested tweak
 	events := []streaming.RawEvent{}
-	ids := lo.FromPtr(params.ID.In)
-	sort.Strings(ids)
+	ids := slices.Clone(lo.FromPtr(params.ID.In))
+	slices.Sort(ids)

The err field on the struct is also never set by any test right now.

🤖 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 `@openmeter/sink/storage_test.go` around lines 31 - 66, Update
hasEventStreamingStub.ListEventsV2 to copy params.ID.In before sorting, then
implement params.Cursor and params.Limit pagination semantics so the stub
exercises queryExistingItemsByNamespaceAndSource pagination and loop-safety
paths. Preserve existing filtering and error behavior, and ensure the stub’s err
field can be configured by tests rather than remaining unused.
openmeter/sink/sink.go (2)

438-442: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse dedupeItems instead of recomputing the item.

validMessages and dedupeItems are built together in the same loop, so they stay index-aligned. Calling message.GetDedupeItem() again creates a second source for the same value. The map lookups against dedupeResults and durableItems only work while both paths agree.

♻️ Suggested tweak
-	for _, message := range validMessages {
-		item := message.GetDedupeItem()
+	for i, message := range validMessages {
+		item := dedupeItems[i]
 		_, isUnique := dedupeResults.UniqueItems[item]
🤖 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 `@openmeter/sink/sink.go` around lines 438 - 442, Update the loop over
validMessages to reuse the corresponding precomputed entry from dedupeItems
instead of calling message.GetDedupeItem() again. Preserve the existing
UniqueItems, AlreadyProcessedItems, and durableItems lookups using that aligned
dedupe item.

481-491: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused dedupeItemsFromMessages helper. The repository contains only its declaration; planFlushMessages performs this conversion inline.

🤖 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 `@openmeter/sink/sink.go` around lines 481 - 491, Remove the unused
dedupeItemsFromMessages helper and its declaration, leaving the inline
conversion in planFlushMessages unchanged.

Source: Coding guidelines

🤖 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 `@openmeter/sink/sink_dedupe_test.go`:
- Around line 156-234: Add concise three-line given/when/then intent comments to
the four replay-transition tests:
TestPlanFlushMessages_IgnoresDropMissingIdentity,
TestPlanFlushMessages_UniqueDurableSetsDedupeWithoutInsert,
TestPlanFlushMessages_ExistingNotDurableReinsertsWithoutSet, and
TestPlanFlushMessages_RejectsUnclassifiedDedupeResult. Describe the relevant
crash/replay state, the planFlushMessages action, and the expected outcome
without changing test behavior.
- Around line 284-303: Update TestDedupeSet_UsesSingleBatchCall to construct the
sink with newTestSink(deduplicator, nil) instead of duplicating the inline Sink
and logger configuration. Preserve the existing dedupeSet call and assertions.
- Around line 106-141: Extend countingStorage with an err field and make
HasEvents return that error when configured, while preserving its existing call
tracking. Add TestPlanFlushMessages_FailsWhenDurabilityCheckFails to configure
the error, invoke planFlushMessages with a test message, and assert the returned
error contains “failed to verify durable state”.

In `@openmeter/sink/sink.go`:
- Around line 438-442: Update the loop over validMessages to reuse the
corresponding precomputed entry from dedupeItems instead of calling
message.GetDedupeItem() again. Preserve the existing UniqueItems,
AlreadyProcessedItems, and durableItems lookups using that aligned dedupe item.
- Around line 481-491: Remove the unused dedupeItemsFromMessages helper and its
declaration, leaving the inline conversion in planFlushMessages unchanged.

In `@openmeter/sink/storage_test.go`:
- Around line 68-102: Extend the HasEvents tests with coverage for incomplete
dedupe.Item values, asserting the required-fields error and no streaming calls;
add a ListEventsV2 failure case using hasEventStreamingStub.err and assert the
wrapped “failed to check event durability” error. Also add a batch-size case
with more than existsQueryBatchSize IDs and verify multiple calls are made.
- Around line 31-66: Update hasEventStreamingStub.ListEventsV2 to copy
params.ID.In before sorting, then implement params.Cursor and params.Limit
pagination semantics so the stub exercises
queryExistingItemsByNamespaceAndSource pagination and loop-safety paths.
Preserve existing filtering and error behavior, and ensure the stub’s err field
can be configured by tests rather than remaining unused.

In `@openmeter/sink/storage.go`:
- Around line 106-123: Reduce query fan-out in HasEvents by avoiding one
sequential query per groupedItems (namespace, source) pair; consolidate
existence checks into a single namespace/ID-filtered query or bounded parallel
queries while preserving durableItems results and error propagation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 10bef959-7a9e-4844-a2a0-c5a11f6db3c3

📥 Commits

Reviewing files that changed from the base of the PR and between f5d9efe and e95fd48.

📒 Files selected for processing (4)
  • openmeter/sink/sink.go
  • openmeter/sink/sink_dedupe_test.go
  • openmeter/sink/storage.go
  • openmeter/sink/storage_test.go

@turip
turip requested a review from chrisgacsal August 11, 2026 08:27
@chrisgacsal chrisgacsal self-assigned this Aug 11, 2026
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