Skip to content

fix(subscriptions): deduplicate gap metrics source reads - #615

Open
nmummau wants to merge 7 commits into
Eventuous:devfrom
nmummau:fix/subscription-gap-source-deduplication
Open

nmummau wants to merge 7 commits into
Eventuous:devfrom
nmummau:fix/subscription-gap-source-deduplication

Conversation

@nmummau

@nmummau nmummau commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Subscription gap metrics currently read the same source position once per subscription. This change shares that read across subscriptions using the same source during each metrics collection, while preserving individual checkpoints and refreshing the source position on the next collection.

Custom subclasses remain independent unless they opt in. Gaps are clamped to zero when a checkpoint advances beyond the observed source position.

Adds core and provider-key regression tests, plus integration tests verifying query/request counts against SQL Server, PostgreSQL, SQLite, and KurrentDB.

Fixes #614

Share source-position reads within each metrics collection while preserving per-subscription checkpoints and refreshing on the next collection.

Keep custom subclasses independent unless they opt in, and floor gaps at zero when checkpoints advance beyond the observed source position.

Add core and provider-key regression coverage plus real SQL Server, PostgreSQL, SQLite, and KurrentDB query/request-count tests.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Deduplicate subscription gap source reads per metrics collection

🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Read each shared subscription source once per collection, while retaining individual checkpoints
 and refreshing on the next collection.
• Keep custom subscriptions independent by default and clamp gaps to zero when checkpoints pass the
 observed tail.
• Verify source keys and real query or request counts across SQL Server, PostgreSQL, SQLite, and
 KurrentDB.
Diagram

graph TD
  P["Provider source keys"] --> R["DI registration"] --> K{"Source keyed?"} -->|yes| S["Collection-local read"] --> C["Individual checkpoints"] --> G["Gap gauges"]
  K -->|no| I["Independent read"] --> C
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Explicit shared source-reader abstraction
  • ➕ Could make source identity and tail-reading contracts more explicit.
  • ➖ Would require broader provider and registration API changes for the same per-collection optimization.

Recommendation: Keep the opt-in source-key approach: it preserves existing delegate registrations and leaves custom implementations independent. An explicit reader abstraction may be useful if source sharing grows beyond gap metrics, but is disproportionate for this fix.

Files changed (28) +1081 / -18

Enhancement (2) +11 / -0
IMessageSubscription.csAdd opt-in end-of-stream source identity +8/-0

Add opt-in end-of-stream source identity

• Adds a nullable default interface property for identifying shareable source reads. Existing implementations remain independent without opting in.

src/Core/src/Eventuous.Subscriptions/IMessageSubscription.cs

SqlSubscriptionBase.csLeave relational custom subscriptions unkeyed by default +3/-0

Leave relational custom subscriptions unkeyed by default

• Adds an overridable nullable source key to the shared SQL subscription base so implementations must explicitly opt into sharing.

src/Relational/src/Eventuous.Sql.Base/Subscriptions/SqlSubscriptionBase.cs

Bug fix (16) +109 / -15
SubscriptionGapMeasure.csAttach source identity to registered measure delegates +10/-1

Attach source identity to registered measure delegates

• Adds an internal delegate target carrying the subscription ID and source key without changing the existing measure delegate signature.

src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionGapMeasure.cs

SubscriptionMetrics.csShare tail reads within each gap collection +38/-13

Share tail reads within each gap collection

• Caches keyed source results only for the current collection, then calculates gaps against each subscription's checkpoint. Keeps unkeyed reads independent, clamps negative gaps to zero, and uses a concurrent dictionary for observed stream positions.

src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionMetrics.cs

SubscriptionRegistrationExtensions.csPreserve source metadata during DI registration +5/-1

Preserve source metadata during DI registration

• Wraps measures from keyed subscriptions with their registration metadata. Unkeyed subscriptions retain their original delegates.

src/Core/src/Eventuous.Subscriptions/Registrations/SubscriptionRegistrationExtensions.cs

AllPersistentSubscription.csKey persistent all-stream tail reads +5/-0

Key persistent all-stream tail reads

• Identifies the client-backed global tail shared with all-stream subscriptions. Derived types remain unkeyed unless they opt in.

src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/AllPersistentSubscription.cs

AllStreamSubscription.csKey all-stream tail reads +5/-0

Key all-stream tail reads

• Uses the measure type and client to identify a shareable global tail, while excluding derived subscriptions by default.

src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/AllStreamSubscription.cs

StreamPersistentSubscription.csKey persistent stream tail reads +5/-0

Key persistent stream tail reads

• Identifies stream tails by measure type, client, and stream name. Derived subscriptions remain independent.

src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/StreamPersistentSubscription.cs

StreamSubscription.csKey stream subscription tail reads +5/-0

Key stream subscription tail reads

• Adds a client-and-stream source key compatible with persistent stream subscriptions, with explicit opt-in required for derived types.

src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/StreamSubscription.cs

PostgresAllStreamSubscription.csKey PostgreSQL all-stream tail reads +4/-0

Key PostgreSQL all-stream tail reads

• Opts the concrete all-stream subscription into sharing by its effective PostgreSQL source and schema, but not derived types.

src/Postgres/src/Eventuous.Postgresql/Subscriptions/PostgresAllStreamSubscription.cs

PostgresStreamSubscription.csKey PostgreSQL stream tail reads +4/-0

Key PostgreSQL stream tail reads

• Opts the concrete stream subscription into sharing with subscriptions reading the same source, schema, and stream.

src/Postgres/src/Eventuous.Postgresql/Subscriptions/PostgresStreamSubscription.cs

PostgresSubscriptionBase.csConstruct PostgreSQL source identities +4/-0

Construct PostgreSQL source identities

• Adds a helper that keys reads by concrete type, data source, effective schema, and optional stream name.

src/Postgres/src/Eventuous.Postgresql/Subscriptions/PostgresSubscriptionBase.cs

SqlServerAllStreamSubscription.csKey SQL Server all-stream tail reads +4/-0

Key SQL Server all-stream tail reads

• Opts the concrete all-stream subscription into sharing by its effective connection and schema, excluding derived implementations.

src/SqlServer/src/Eventuous.SqlServer/Subscriptions/SqlServerAllStreamSubscription.cs

SqlServerStreamSubscription.csKey SQL Server stream tail reads +4/-0

Key SQL Server stream tail reads

• Opts the concrete stream subscription into sharing with subscriptions reading the same connection, schema, and stream.

src/SqlServer/src/Eventuous.SqlServer/Subscriptions/SqlServerStreamSubscription.cs

SqlServerSubscriptionBase.csConstruct SQL Server source identities +4/-0

Construct SQL Server source identities

• Adds a helper keyed by concrete type, effective connection string, schema, and optional stream name.

src/SqlServer/src/Eventuous.SqlServer/Subscriptions/SqlServerSubscriptionBase.cs

SqliteAllStreamSubscription.csKey SQLite all-stream tail reads +4/-0

Key SQLite all-stream tail reads

• Opts the concrete all-stream subscription into sharing by its effective database and messages table while leaving derived types independent.

src/Sqlite/src/Eventuous.Sqlite/Subscriptions/SqliteAllStreamSubscription.cs

SqliteStreamSubscription.csKey SQLite stream tail reads +4/-0

Key SQLite stream tail reads

• Opts the concrete stream subscription into sharing when database, messages table, and stream match.

src/Sqlite/src/Eventuous.Sqlite/Subscriptions/SqliteStreamSubscription.cs

SqliteSubscriptionBase.csConstruct SQLite source identities +4/-0

Construct SQLite source identities

• Adds a helper keyed by concrete type, effective connection string, messages table, and optional stream name.

src/Sqlite/src/Eventuous.Sqlite/Subscriptions/SqliteSubscriptionBase.cs

Tests (10) +961 / -3
SubscriptionGapMetricsTests.csCover shared reads and per-subscription gap behavior +171/-0

Cover shared reads and per-subscription gap behavior

• Tests collection-local reuse, individual checkpoints, refreshed tails, failures, independent delegates, tags, time metrics, and zero-clamped gaps.

src/Core/test/Eventuous.Tests.Subscriptions/SubscriptionGapMetricsTests.cs

KurrentDBContainer.csExpose configurable KurrentDB container builder +4/-3

Expose configurable KurrentDB container builder

• Separates container builder creation from building so tests can customize server settings while preserving the existing Create method.

src/KurrentDB/test/Eventuous.Tests.KurrentDB/Fixtures/KurrentDBContainer.cs

SharedSourceGapMetricsTests.csCount real KurrentDB tail requests +118/-0

Count real KurrentDB tail requests

• Verifies distinct subscriptions produce individual gaps with one shared gRPC tail read per collection, then observe a refreshed tail on the next collection.

src/KurrentDB/test/Eventuous.Tests.KurrentDB/Metrics/SharedSourceGapMetricsTests.cs

EndOfStreamSourceKeyTests.csVerify KurrentDB source-key boundaries +48/-0

Verify KurrentDB source-key boundaries

• Checks sharing between catch-up and persistent subscriptions and separation by client or stream. Confirms derived types remain unkeyed.

src/KurrentDB/test/Eventuous.Tests.KurrentDB/Subscriptions/EndOfStreamSourceKeyTests.cs

SharedSourceGapMetricsTests.csCount real PostgreSQL tail queries +133/-0

Count real PostgreSQL tail queries

• Uses Npgsql activity data to verify one successful tail query per collection and distinct checkpoint-based gaps after tail updates.

src/Postgres/test/Eventuous.Tests.Postgres/Metrics/SharedSourceGapMetricsTests.cs

EndOfStreamSourceKeyTests.csVerify PostgreSQL source-key boundaries +56/-0

Verify PostgreSQL source-key boundaries

• Tests equality across matching sources and separation by data source, schema, or stream, including effective configuration overrides and derived types.

src/Postgres/test/Eventuous.Tests.Postgres/Subscriptions/EndOfStreamSourceKeyTests.cs

SharedSourceGapMetricsTests.csCount real SQL Server tail queries +137/-0

Count real SQL Server tail queries

• Uses SqlClient diagnostics to verify one successful tail query per collection and refreshed, subscription-specific gaps.

src/SqlServer/test/Eventuous.Tests.SqlServer/Metrics/SharedSourceGapMetricsTests.cs

EndOfStreamSourceKeyTests.csVerify SQL Server source-key boundaries +55/-0

Verify SQL Server source-key boundaries

• Tests sharing and separation by effective connection, schema, and stream, plus independent behavior for derived subscriptions.

src/SqlServer/test/Eventuous.Tests.SqlServer/Subscriptions/EndOfStreamSourceKeyTests.cs

SharedSourceGapMetricsTests.csCount real SQLite tail queries +141/-0

Count real SQLite tail queries

• Profiles production SQLite statement executions in an isolated in-memory database to verify one tail query per collection and refreshed individual gaps.

src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/SharedSourceGapMetricsTests.cs

EndOfStreamSourceKeyTests.csVerify SQLite keys and custom-query isolation +98/-0

Verify SQLite keys and custom-query isolation

• Tests source-key boundaries, effective connection overrides, independent custom-query measurements, and explicit opt-in by a derived subscription.

src/Sqlite/test/Eventuous.Tests.Sqlite/Subscriptions/EndOfStreamSourceKeyTests.cs

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Four gap tests omit context-free awaits ✓ Resolved
Description
TailQueryCounter.Create awaits SQLite connection opens and closes without .NoContext(), and the
new PostgreSQL, SQL Server, and KurrentDB tests likewise omit it on container or database I/O. These
awaits run during integration-test setup and collection, so their continuations capture a
synchronization context when one is present.
Code

src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/SharedSourceGapMetricsTests.cs[127]

+                await connection.OpenAsync(cancellationToken);
Evidence
Rule 5 requires applicable awaits to avoid synchronization-context capture. The cited new tests
await SQLite connection I/O, database initialization, and container startup without .NoContext().

CLAUDE.md: Use Asynchronous I/O Without Capturing Context: CLAUDE.md: Use Asynchronous I/O Without Capturing Context: CLAUDE.md: Use Asynchronous I/O Without Capturing Context: CLAUDE.md: Use Asynchronous I/O Without Capturing Context
src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/SharedSourceGapMetricsTests.cs[124-135]
src/Postgres/test/Eventuous.Tests.Postgres/Metrics/SharedSourceGapMetricsTests.cs[22-29]
src/SqlServer/test/Eventuous.Tests.SqlServer/Metrics/SharedSourceGapMetricsTests.cs[22-30]
src/KurrentDB/test/Eventuous.Tests.KurrentDB/Metrics/SharedSourceGapMetricsTests.cs[21-25]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New gap-metrics integration tests await asynchronous I/O without `.NoContext()`.
## Fix Focus Areas
- src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/SharedSourceGapMetricsTests.cs[20-149]
- src/Postgres/test/Eventuous.Tests.Postgres/Metrics/SharedSourceGapMetricsTests.cs[21-102]
- src/SqlServer/test/Eventuous.Tests.SqlServer/Metrics/SharedSourceGapMetricsTests.cs[21-100]
- src/KurrentDB/test/Eventuous.Tests.KurrentDB/Metrics/SharedSourceGapMetricsTests.cs[19-25]
## Recommended Fix
Apply `.NoContext()` to applicable container and database I/O awaits, including SQLite connection setup and cleanup. Leave test assertion awaits unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. A test append captures the caller context ✓ Resolved
Description
Append awaits writer.AppendToStreamAsync() without .NoContext(). Each event written by the new
KurrentDB integration test resumes its helper on the captured synchronization context, unlike the
transport helper in the same test.
Code

src/KurrentDB/test/Eventuous.Tests.KurrentDB/Metrics/SharedSourceGapMetricsTests.cs[R87-90]

+            var result = await writer.AppendToStreamAsync(
+                stream, StreamState.Any, [new EventData(Uuid.NewUuid(), "shared-gap-event", "{}"u8.ToArray())],
+                cancellationToken: cancellationToken
+            );
Evidence
Rule 9 requires applicable awaited I/O to use .NoContext(). The new Append helper directly
awaits the asynchronous client write without it.

CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext
src/KurrentDB/test/Eventuous.Tests.KurrentDB/Metrics/SharedSourceGapMetricsTests.cs[86-92]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new KurrentDB test helper awaits a network append without `.NoContext()`.
## Fix Focus Areas
- src/KurrentDB/test/Eventuous.Tests.KurrentDB/Metrics/SharedSourceGapMetricsTests.cs[86-92]
## Recommended Fix
Apply `.NoContext()` to the `AppendToStreamAsync` operation before awaiting it.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. SQLite test setup blocks runner threads ✓ Resolved
Description
TailQueryCounter calls synchronous SqliteConnection.Open() even though OpenAsync() is
available. The counter opens a connection during test setup and again during disposal, so both paths
can block a test-runner thread while waiting for SQLite I/O.
Code

src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/SharedSourceGapMetricsTests.cs[116]

+            _connection.Open();
Evidence
Rule 6 requires asynchronous I/O where available. The new counter calls Open() during construction
and again during disposal.

CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext
src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/SharedSourceGapMetricsTests.cs[114-116]
src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/SharedSourceGapMetricsTests.cs[129-133]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new SQLite query counter opens its connection synchronously during setup and disposal.
## Fix Focus Areas
- src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/SharedSourceGapMetricsTests.cs[114-137]
## Recommended Fix
Initialize the counter through an async factory that awaits OpenAsync(). Use asynchronous cleanup for the second open while preserving the profiler removal and pool cleanup.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View action required (2)
4. The request helper omits NoContext ✓ Resolved
Description
TailReadCounter.SendAsync awaits base.SendAsync() without .NoContext(). This
transport-counting handler forwards test requests without needing a synchronization context, so its
continuation captures one unnecessarily.
Code

src/KurrentDB/test/Eventuous.Tests.KurrentDB/Metrics/SharedSourceGapMetricsTests.cs[115]

+            return await base.SendAsync(request, cancellationToken);
Evidence
Rule 6 calls for .NoContext() on awaited I/O when context capture is unnecessary; the new forwarding
handler awaits an HTTP send without it.

CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext
src/KurrentDB/test/Eventuous.Tests.KurrentDB/Metrics/SharedSourceGapMetricsTests.cs[111-115]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new HTTP request-counting handler awaits I/O without the repository's NoContext convention.
## Fix Focus Areas
- src/KurrentDB/test/Eventuous.Tests.KurrentDB/Metrics/SharedSourceGapMetricsTests.cs[111-116]
## Recommended Fix
Apply .NoContext() to the awaited base.SendAsync() call.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. SQLite test imports break sort order ✓ Resolved
Description
EndOfStreamSourceKeyTests places Eventuous.Sqlite imports after Microsoft imports rather than
keeping its imports sorted with system imports first. This new file's split import groups conflict
with the repository's configured ordering and make subsequent imports easy to place inconsistently.
Code

src/Sqlite/test/Eventuous.Tests.Sqlite/Subscriptions/EndOfStreamSourceKeyTests.cs[10]

+using Eventuous.Sqlite.Projections;
Evidence
Rule 7 requires EditorConfig formatting. The repository sets csharp_sort_usings_with_system_first to
true, while this new file resumes Eventuous imports after Microsoft imports.

CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting
src/Sqlite/test/Eventuous.Tests.Sqlite/Subscriptions/EndOfStreamSourceKeyTests.cs[4-13]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new SQLite source-key test has imports out of the order specified by .editorconfig.
## Fix Focus Areas
- src/Sqlite/test/Eventuous.Tests.Sqlite/Subscriptions/EndOfStreamSourceKeyTests.cs[4-13]
## Recommended Fix
Sort and group the imports with System first, followed by Eventuous and Microsoft namespaces in order.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. PostgreSQL tail queries repeat for transient clients ✓ Resolved
Description
GetEndOfStreamSourceKey uses the NpgsqlDataSource instance as part of the key, so separate
instances targeting the same database never share a read. When AddEventuousPostgres is configured
with a transient data-source lifetime, each subscription receives a distinct instance and collection
executes the same schema-and-stream tail query once per subscription.
Code

src/Postgres/src/Eventuous.Postgresql/Subscriptions/PostgresSubscriptionBase.cs[45]

+        => (GetType(), DataSource, Schema.Name, streamName);
Evidence
The new tuple key contains DataSource, while PostgreSQL registration exposes a transient
data-source lifetime and builds a new instance for each resolution. The metrics collector caches
reads only when source keys compare equal, so distinct instances bypass sharing even when their
connection string and query target are identical.

src/Postgres/src/Eventuous.Postgresql/Subscriptions/PostgresSubscriptionBase.cs[41-48]
src/Postgres/src/Eventuous.Postgresql/Extensions/RegistrationExtensions.cs[20-36]
src/Postgres/src/Eventuous.Postgresql/Extensions/RegistrationExtensions.cs[99-118]
src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionMetrics.cs[83-93]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
PostgreSQL subscriptions using transient data sources repeat identical end-of-stream reads because their source keys contain distinct data-source instances.
## Fix Focus Areas
- src/Postgres/src/Eventuous.Postgresql/Subscriptions/PostgresSubscriptionBase.cs[43-45]
- src/Postgres/src/Eventuous.Postgresql/Extensions/RegistrationExtensions.cs[99-118]
## Recommended Fix
Provide a stable source identity when data sources are known to target the same database with equivalent read configuration. Preserve separate keys when configuration could change the query result, and add coverage for two transient data sources targeting one source.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Custom subscription gaps use the wrong ID ✓ Resolved
Description
GetEndOfStream captures the registration ID in RegisteredSubscriptionMeasure and replaces the
measured subscription's returned ID with it. When builder.Configure changes
SubscriptionOptions.SubscriptionId, checkpoint commits use the configured ID but the gap collector
looks up the registration ID, so it reports a gap without that subscription's checkpoint.
Code

src/Core/src/Eventuous.Subscriptions/Registrations/SubscriptionRegistrationExtensions.cs[81]

+                    ? new RegisteredSubscriptionMeasure(subscriptionId, key, measure).GetEndOfStream
Evidence
The builder runs the caller's configuration after setting the registration ID; the keyed wrapper
then uses the registration ID for the metric, while checkpoint metrics index commits by their
emitted ID.

src/Core/src/Eventuous.Subscriptions/Registrations/SubscriptionBuilder.cs[329-339]
src/Core/src/Eventuous.Subscriptions/Registrations/SubscriptionRegistrationExtensions.cs[75-82]
src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionMetrics.cs[90-106]
src/Core/src/Eventuous.Subscriptions/Diagnostics/CheckpointCommitMetrics.cs[12-27]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Keyed measures substitute the registration ID for the resolved subscription's configured ID, causing checkpoint lookups to miss when configuration changes `SubscriptionId`.
## Fix Focus Areas
- src/Core/src/Eventuous.Subscriptions/Registrations/SubscriptionRegistrationExtensions.cs[75-82]
- src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionMetrics.cs[90-106]
## Recommended Fix
Capture the resolved subscription's runtime ID when constructing the wrapper, and add a regression test that configures a different ID and commits a checkpoint under that ID.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Persistent subscriptions duplicate reads ✓ Resolved
Description
AllPersistentSubscription.EndOfStreamSourceKey uses Client identity, but the constructor
accepting a persistent-subscriptions client creates a new Client for each subscription. When
multiple subscriptions receive the same persistent-subscriptions client, their keys differ and each
collection makes a separate end-of-stream read.
Code

src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/AllPersistentSubscription.cs[R125-126]

+    public virtual object? EndOfStreamSourceKey
+        => GetType() == typeof(AllPersistentSubscription) ? (typeof(AllStreamSubscriptionMeasure), Client) : null;
Evidence
The persistent-client constructor allocates a fresh KurrentDBClient, the new key uses that
instance, and the collector shares only equal keys. The stream-persistent key has the same
construction path.

src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/PersistentSubscriptionBase.cs[74-88]
src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/AllPersistentSubscription.cs[37-49]
src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/AllPersistentSubscription.cs[123-133]
src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/StreamPersistentSubscription.cs[112-119]
src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionMetrics.cs[83-93]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Persistent subscriptions constructed from one persistent-subscriptions client each get a distinct read client, preventing their identical end-of-stream reads from sharing a key.
## Fix Focus Areas
- src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/PersistentSubscriptionBase.cs[74-88]
- src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/AllPersistentSubscription.cs[123-126]
- src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/StreamPersistentSubscription.cs[112-115]
## Recommended Fix
Retain the supplied client as a shared source identity for this constructor path and use it in both persistent-subscription keys, while keeping the existing read client for the measure. Test two subscriptions constructed from the same persistent-subscriptions client.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: Auto: 🧠 Deep: Cross-cutting metrics logic spans core and four provider implementations.

Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 7437033 🧠 Deep

Results up to commit N/A


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. Four gap tests omit context-free awaits ✓ Resolved
Description
TailQueryCounter.Create awaits SQLite connection opens and closes without .NoContext(), and the
new PostgreSQL, SQL Server, and KurrentDB tests likewise omit it on container or database I/O. These
awaits run during integration-test setup and collection, so their continuations capture a
synchronization context when one is present.
Code

src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/SharedSourceGapMetricsTests.cs[127]

+                await connection.OpenAsync(cancellationToken);
Evidence
Rule 5 requires applicable awaits to avoid synchronization-context capture. The cited new tests
await SQLite connection I/O, database initialization, and container startup without .NoContext().

CLAUDE.md: Use Asynchronous I/O Without Capturing Context: CLAUDE.md: Use Asynchronous I/O Without Capturing Context
src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/SharedSourceGapMetricsTests.cs[124-135]
src/Postgres/test/Eventuous.Tests.Postgres/Metrics/SharedSourceGapMetricsTests.cs[22-29]
src/SqlServer/test/Eventuous.Tests.SqlServer/Metrics/SharedSourceGapMetricsTests.cs[22-30]
src/KurrentDB/test/Eventuous.Tests.KurrentDB/Metrics/SharedSourceGapMetricsTests.cs[21-25]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New gap-metrics integration tests await asynchronous I/O without `.NoContext()`.
## Fix Focus Areas
- src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/SharedSourceGapMetricsTests.cs[20-149]
- src/Postgres/test/Eventuous.Tests.Postgres/Metrics/SharedSourceGapMetricsTests.cs[21-102]
- src/SqlServer/test/Eventuous.Tests.SqlServer/Metrics/SharedSourceGapMetricsTests.cs[21-100]
- src/KurrentDB/test/Eventuous.Tests.KurrentDB/Metrics/SharedSourceGapMetricsTests.cs[19-25]
## Recommended Fix
Apply `.NoContext()` to applicable container and database I/O awaits, including SQLite connection setup and cleanup. Leave test assertion awaits unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. A test append captures the caller context ✓ Resolved
Description
Append awaits writer.AppendToStreamAsync() without .NoContext(). Each event written by the new
KurrentDB integration test resumes its helper on the captured synchronization context, unlike the
transport helper in the same test.
Code

src/KurrentDB/test/Eventuous.Tests.KurrentDB/Metrics/SharedSourceGapMetricsTests.cs[R87-90]

+            var result = await writer.AppendToStreamAsync(
+                stream, StreamState.Any, [new EventData(Uuid.NewUuid(), "shared-gap-event", "{}"u8.ToArray())],
+                cancellationToken: cancellationToken
+            );
Evidence
Rule 9 requires applicable awaited I/O to use .NoContext(). The new Append helper directly
awaits the asynchronous client write without it.

CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext: CLAUDE.md: Keep I/O Asynchronous and Use NoContext
src/KurrentDB/test/Eventuous.Tests.KurrentDB/Metrics/SharedSourceGapMetricsTests.cs[86-92]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new KurrentDB test helper awaits a network append without `.NoContext()`.
## Fix Focus Areas
- src/KurrentDB/test/Eventuous.Tests.KurrentDB/Metrics/SharedSourceGapMetricsTests.cs[86-92]
## Recommended Fix
Apply `.NoContext()` to the `AppendToStreamAsync` operation before awaiting it.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. SQLite test setup blocks runner threads ✓ Resolved
Description
TailQueryCounter calls synchronous SqliteConnection.Open() even though OpenAsync() is
available. The counter opens a connection during test setup and again during disposal, so both paths
can block a test-runner thread while waiting for SQLite I/O.
Code

src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/SharedSourceGapMetricsTests.cs[116]

+            _connection.Open();
Evidence
Rule 6 requires asynchronous I/O where available. The new counter calls Open() during construction
and again during disposal.

CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext
src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/SharedSourceGapMetricsTests.cs[114-116]
src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/SharedSourceGapMetricsTests.cs[129-133]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new SQLite query counter opens its connection synchronously during setup and disposal.
## Fix Focus Areas
- src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/SharedSourceGapMetricsTests.cs[114-137]
## Recommended Fix
Initialize the counter through an async factory that awaits OpenAsync(). Use asynchronous cleanup for the second open while preserving the profiler removal and pool cleanup.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View action required (2)
4. The request helper omits NoContext ✓ Resolved
Description
TailReadCounter.SendAsync awaits base.SendAsync() without .NoContext(). This
transport-counting handler forwards test requests without needing a synchronization context, so its
continuation captures one unnecessarily.
Code

src/KurrentDB/test/Eventuous.Tests.KurrentDB/Metrics/SharedSourceGapMetricsTests.cs[115]

+            return await base.SendAsync(request, cancellationToken);
Evidence
Rule 6 calls for .NoContext() on awaited I/O when context capture is unnecessary; the new forwarding
handler awaits an HTTP send without it.

CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext: CLAUDE.md: Use Async I/O and NoContext
src/KurrentDB/test/Eventuous.Tests.KurrentDB/Metrics/SharedSourceGapMetricsTests.cs[111-115]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new HTTP request-counting handler awaits I/O without the repository's NoContext convention.
## Fix Focus Areas
- src/KurrentDB/test/Eventuous.Tests.KurrentDB/Metrics/SharedSourceGapMetricsTests.cs[111-116]
## Recommended Fix
Apply .NoContext() to the awaited base.SendAsync() call.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. SQLite test imports break sort order ✓ Resolved
Description
EndOfStreamSourceKeyTests places Eventuous.Sqlite imports after Microsoft imports rather than
keeping its imports sorted with system imports first. This new file's split import groups conflict
with the repository's configured ordering and make subsequent imports easy to place inconsistently.
Code

src/Sqlite/test/Eventuous.Tests.Sqlite/Subscriptions/EndOfStreamSourceKeyTests.cs[10]

+using Eventuous.Sqlite.Projections;
Evidence
Rule 7 requires EditorConfig formatting. The repository sets csharp_sort_usings_with_system_first to
true, while this new file resumes Eventuous imports after Microsoft imports.

CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting: CLAUDE.md: Follow Repository EditorConfig Formatting
src/Sqlite/test/Eventuous.Tests.Sqlite/Subscriptions/EndOfStreamSourceKeyTests.cs[4-13]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new SQLite source-key test has imports out of the order specified by .editorconfig.
## Fix Focus Areas
- src/Sqlite/test/Eventuous.Tests.Sqlite/Subscriptions/EndOfStreamSourceKeyTests.cs[4-13]
## Recommended Fix
Sort and group the imports with System first, followed by Eventuous and Microsoft namespaces in order.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
6. PostgreSQL tail queries repeat for transient clients ✓ Resolved
Description
GetEndOfStreamSourceKey uses the NpgsqlDataSource instance as part of the key, so separate
instances targeting the same database never share a read. When AddEventuousPostgres is configured
with a transient data-source lifetime, each subscription receives a distinct instance and collection
executes the same schema-and-stream tail query once per subscription.
Code

src/Postgres/src/Eventuous.Postgresql/Subscriptions/PostgresSubscriptionBase.cs[45]

+        => (GetType(), DataSource, Schema.Name, streamName);
Evidence
The new tuple key contains DataSource, while PostgreSQL registration exposes a transient
data-source lifetime and builds a new instance for each resolution. The metrics collector caches
reads only when source keys compare equal, so distinct instances bypass sharing even when their
connection string and query target are identical.

src/Postgres/src/Eventuous.Postgresql/Subscriptions/PostgresSubscriptionBase.cs[41-48]
src/Postgres/src/Eventuous.Postgresql/Extensions/RegistrationExtensions.cs[20-36]
src/Postgres/src/Eventuous.Postgresql/Extensions/RegistrationExtensions.cs[99-118]
src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionMetrics.cs[83-93]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
PostgreSQL subscriptions using transient data sources repeat identical end-of-stream reads because their source keys contain distinct data-source instances.
## Fix Focus Areas
- src/Postgres/src/Eventuous.Postgresql/Subscriptions/PostgresSubscriptionBase.cs[43-45]
- src/Postgres/src/Eventuous.Postgresql/Extensions/RegistrationExtensions.cs[99-118]
## Recommended Fix
Provide a stable source identity when data sources are known to target the same database with equivalent read configuration. Preserve separate keys when configuration could change the query result, and add coverage for two transient data sources targeting one source.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Custom subscription gaps use the wrong ID ✓ Resolved
Description
GetEndOfStream captures the registration ID in RegisteredSubscriptionMeasure and replaces the
measured subscription's returned ID with it. When builder.Configure changes
SubscriptionOptions.SubscriptionId, checkpoint commits use the configured ID but the gap collector
looks up the registration ID, so it reports a gap without that subscription's checkpoint.
Code

src/Core/src/Eventuous.Subscriptions/Registrations/SubscriptionRegistrationExtensions.cs[81]

+                    ? new RegisteredSubscriptionMeasure(subscriptionId, key, measure).GetEndOfStream
Evidence
The builder runs the caller's configuration after setting the registration ID; the keyed wrapper
then uses the registration ID for the metric, while checkpoint metrics index commits by their
emitted ID.

src/Core/src/Eventuous.Subscriptions/Registrations/SubscriptionBuilder.cs[329-339]
src/Core/src/Eventuous.Subscriptions/Registrations/SubscriptionRegistrationExtensions.cs[75-82]
src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionMetrics.cs[90-106]
src/Core/src/Eventuous.Subscriptions/Diagnostics/CheckpointCommitMetrics.cs[12-27]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Keyed measures substitute the registration ID for the resolved subscription's configured ID, causing checkpoint lookups to miss when configuration changes `SubscriptionId`.
## Fix Focus Areas
- src/Core/src/Eventuous.Subscriptions/Registrations/SubscriptionRegistrationExtensions.cs[75-82]
- src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionMetrics.cs[90-106]
## Recommended Fix
Capture the resolved subscription's runtime ID when constructing the wrapper, and add a regression test that configures a different ID and commits a checkpoint under that ID.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Persistent subscriptions duplicate reads ✓ Resolved
Description
AllPersistentSubscription.EndOfStreamSourceKey uses Client identity, but the constructor
accepting a persistent-subscriptions client creates a new Client for each subscription. When
multiple subscriptions receive the same persistent-subscriptions client, their keys differ and each
collection makes a separate end-of-stream read.
Code

src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/AllPersistentSubscription.cs[R125-126]

+    public virtual object? EndOfStreamSourceKey
+        => GetType() == typeof(AllPersistentSubscription) ? (typeof(AllStreamSubscriptionMeasure), Client) : null;
Evidence
The persistent-client constructor allocates a fresh KurrentDBClient, the new key uses that
instance, and the collector shares only equal keys. The stream-persistent key has the same
construction path.

src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/PersistentSubscriptionBase.cs[74-88]
src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/AllPersistentSubscription.cs[37-49]
src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/AllPersistentSubscription.cs[123-133]
src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/StreamPersistentSubscription.cs[112-119]
src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionMetrics.cs[83-93]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Persistent subscriptions constructed from one persistent-subscriptions client each get a distinct read client, preventing their identical end-of-stream reads from sharing a key.
## Fix Focus Areas
- src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/PersistentSubscriptionBase.cs[74-88]
- src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/AllPersistentSubscription.cs[123-126]
- src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/StreamPersistentSubscription.cs[112-115]
## Recommended Fix
Retain the supplied client as a shared source identity for this constructor path and use it in both persistent-subscription keys, while keeping the existing read client for the measure. Test two subscriptions constructed from the same persistent-subscriptions client.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/SharedSourceGapMetricsTests.cs Outdated
Comment thread src/Sqlite/test/Eventuous.Tests.Sqlite/Subscriptions/EndOfStreamSourceKeyTests.cs Outdated
Comment thread src/KurrentDB/src/Eventuous.KurrentDB/Subscriptions/AllPersistentSubscription.cs Outdated
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Test Results

   48 files  ± 0     48 suites  ±0   15m 57s ⏱️ +42s
  639 tests +26    639 ✅ +26  0 💤 ±0  0 ❌ ±0 
1 272 runs  +46  1 272 ✅ +46  0 💤 ±0  0 ❌ ±0 

Results for commit 7437033. ± Comparison against base commit ed6cfd1.

This pull request removes 9 and adds 35 tests. Note that renamed tests count towards both.
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(09/21/2026 13:40:13 +00:00)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(09/21/2026 13:40:13)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(9a8c03f6-9bdf-4072-a43a-772bdae7bb21)
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-09-21T13:35:10.6224849+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-21T13:35:10.6224849+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-09-21T13:35:10.6224849+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-09-21T13:35:10.6224849+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-21T13:35:10.6224849+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-09-21T13:35:10.6224849+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-21T13:35:10.6224849+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-09-21T13:35:10.6224849+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2026-09-21T13:35:10.6224849+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-21T13:35:10.6224849+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-09-21T13:35:13.7108596+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-21T13:35:13.7108596+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-09-21T13:35:13.7108596+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-09-21T13:35:13.7108596+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-21T13:35:13.7108596+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-09-21T13:35:13.7108596+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-21T13:35:13.7108596+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-09-21T13:35:13.7108596+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2026-09-21T13:35:13.7108596+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-21T13:35:13.7108596+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-09-21T13:35:17.3347072+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-21T13:35:17.3347072+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-09-21T13:35:17.3347072+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-09-21T13:35:17.3347072+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-21T13:35:17.3347072+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-09-21T13:35:17.3347072+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-21T13:35:17.3347072+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-09-21T13:35:17.3347072+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2026-09-21T13:35:17.3347072+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-21T13:35:17.3347072+00:00 })
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(10/09/2026 17:21:59 +00:00)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(10/09/2026 17:21:59)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(d1a9c5f3-c7a2-4932-a8c6-320fbaa25588)
Eventuous.Tests.KurrentDB.Metrics.SharedSourceGapMetricsTests ‑ All_stream_subscriptions_read_shared_tail_once_per_collection
Eventuous.Tests.KurrentDB.Subscriptions.EndOfStreamSourceKeyTests ‑ Derived_subscriptions_keep_independent_reads
Eventuous.Tests.KurrentDB.Subscriptions.EndOfStreamSourceKeyTests ‑ Keys_share_catchup_and_persistent_sources_but_separate_clients_and_streams
Eventuous.Tests.KurrentDB.Subscriptions.EndOfStreamSourceKeyTests ‑ Persistent_client_keys_share_sources_but_separate_clients_and_streams
Eventuous.Tests.Postgres.Metrics.SharedSourceGapMetricsTests ‑ All_stream_subscriptions_read_shared_tail_once_per_collection(Singleton)
Eventuous.Tests.Postgres.Metrics.SharedSourceGapMetricsTests ‑ All_stream_subscriptions_read_shared_tail_once_per_collection(Transient)
Eventuous.Tests.Postgres.Subscriptions.EndOfStreamSourceKeyTests ‑ Derived_subscriptions_keep_independent_reads
…

♻️ This comment has been updated with latest results.

@nmummau

nmummau commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

/review

Comment thread src/Postgres/src/Eventuous.Postgresql/Subscriptions/PostgresSubscriptionBase.cs Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit f5cc41f

@nmummau

nmummau commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

/review

@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 35ed1b0

Apply NoContext() to I/O and checkpoint awaits, including SQLite
setup and cleanup, while preserving assertion awaits.

Disambiguate PostgreSQL and SQLite task helpers with assembly aliases.
@nmummau

nmummau commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

/review

@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 7437033

@nmummau

nmummau commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

@alexeyzimarev this is ready for a review. I went through a few stages of fixing qodo suggestions.

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 gap metric reads the end of the same stream once for each subscription

1 participant