Skip to content

[DB-2199] Fix write timeout regressions introduced with multi-stream appends - #5699

Merged
shaan1337 merged 1 commit into
masterfrom
shaan1337/fix-write-timeout-msa-bugs
Aug 5, 2026
Merged

[DB-2199] Fix write timeout regressions introduced with multi-stream appends#5699
shaan1337 merged 1 commit into
masterfrom
shaan1337/fix-write-timeout-msa-bugs

Conversation

@shaan1337

Copy link
Copy Markdown
Member

No description provided.

The failure constructor of `WriteEventsCompleted` leaves `FirstEventNumbers`
and `LastEventNumbers` empty, so `LowAllocReadOnlyMemory<T>.Single` throws
when the write result is not `Success`. Two write completion callbacks read
them unconditionally, and the throw happens before the failure handling runs.

- `CoreProjectionCheckpointWriter`: a failed checkpoint write is neither
  retried nor reported as a projection failure. The projection stops
  processing events and just sits at `Writing (N)` until the node or
  the projection subsystem is restarted.

- `PersistentSubscriptionMessageParker`: a failed park write is not retried
  and the event is silently lost - neither delivered, nor retried, nor
  written to the parked stream. The consumer's in-flight slot is leaked too,
  so enough failed parks and the connection stops receiving messages.

Guard both reads on `OperationResult.Success` and add a test for each.
@linear-code

linear-code Bot commented Aug 5, 2026

Copy link
Copy Markdown

DB-2199

@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Fix write-timeout handling for multi-stream appends (checkpoint + parked writes)

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Guard event-number reads on successful writes to avoid exceptions on timeouts.
• Ensure checkpoint writes retry properly and fail projections after retry exhaustion.
• Ensure park-message timeouts complete callbacks without updating parked-message stats.
Diagram

graph TD
  PSMP["PersistentSubscriptionMessageParker"] -->|"writes parked event"| IOD["IODispatcher"] --> WEC["WriteEventsCompleted"] -->|"callback + result"| PSMP
  CPCW["CoreProjectionCheckpointWriter"] -->|"write checkpoint"| IOD --> WEC -->|"result + event#"| CPCW
  CPCW -->|"schedule retry"| TIMER["TimerService"] --> CPCW
  CPCW -->|"on exhaustion"| PROJ["Projection failure"]
  subgraph Legend
    direction LR
    _cmp([Component]) ~~~ _msg["Message"]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Populate First/LastEventNumbers on failures in WriteEventsCompleted
  • ➕ Prevents this entire class of bugs across all call sites
  • ➕ Simplifies downstream callback code (no success-guard needed)
  • ➖ Risky semantic change: failure completions may not have meaningful event numbers
  • ➖ Broader blast radius; may mask assumptions elsewhere
2. Introduce safe accessors (e.g., TryGetSingleEventNumber)
  • ➕ Centralizes the invariant and avoids repeated result checks
  • ➕ Makes failure-path handling explicit and self-documenting
  • ➖ Requires API changes or extensions across multiple projects
  • ➖ Still needs callers to decide fallback behavior
3. Always pass EventNumber.Invalid from dispatcher on non-success
  • ➕ Keeps message shape unchanged while standardizing failure behavior
  • ➕ Localizes change to the dispatch boundary
  • ➖ May hide cases where event numbers are unexpectedly missing on success
  • ➖ Still requires downstream to interpret Invalid correctly

Recommendation: The PR’s approach (guard reads on OperationResult.Success and add regression tests) is the safest, lowest-blast-radius fix for the regression. Consider a follow-up to add a standardized safe accessor or dispatcher-level normalization to reduce future repeated footguns, but avoid changing WriteEventsCompleted failure semantics unless you can verify all downstream assumptions.

Files changed (4) +95 / -4

Bug fix (2) +9 / -4
PersistentSubscriptionMessageParker.csOnly update parked-message state on successful writes +5/-3

Only update parked-message state on successful writes

• Guards reads of LastEventNumbers.Single behind OperationResult.Success to avoid exceptions when write metadata is absent on failures. Ensures failures still invoke the completion callback without mutating parked-message counters/timestamps.

src/KurrentDB.Core/Services/PersistentSubscription/PersistentSubscriptionMessageParker.cs

CoreProjectionCheckpointWriter.csAvoid reading FirstEventNumbers on failed checkpoint writes +4/-1

Avoid reading FirstEventNumbers on failed checkpoint writes

• Updates the write completion callback to only read FirstEventNumbers.Single on success, otherwise passing EventNumber.Invalid. Prevents failure-path exceptions that previously bypassed retry/failure handling.

src/KurrentDB.Projections.V1/Services/Processing/Checkpointing/CoreProjectionCheckpointWriter.cs

Tests (2) +86 / -0
PersistentSubscriptionMessageParkerTests.csAdd regression test for park write commit-timeout behavior +27/-0

Add regression test for park write commit-timeout behavior

• Adds a fixture that forces parked-message writes to time out and asserts the completion callback runs with the failure result. Verifies no parked-message stats are updated when the write fails.

src/KurrentDB.Core.Tests/Services/PersistentSubscription/PersistentSubscriptionMessageParkerTests.cs

when_the_checkpoint_write_times_out.csAdd regression test for checkpoint write timeouts and retry exhaustion +59/-0

Add regression test for checkpoint write timeouts and retry exhaustion

• Introduces a test that forces checkpoint writes to return CommitTimeout repeatedly. Confirms retries are attempted until the scheduler stops and the projection is failed after exhausting retry limits.

src/KurrentDB.Projections.V1.Tests/Services/core_projection/checkpoint_manager/when_the_checkpoint_write_times_out.cs

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

@shaan1337
shaan1337 merged commit 8620c94 into master Aug 5, 2026
106 checks passed
@shaan1337
shaan1337 deleted the shaan1337/fix-write-timeout-msa-bugs branch August 5, 2026 10:03

@github-actions github-actions 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.

🚨 @shaan1337 Failed to create cherry Pick PR due to error:

RequestError [HttpError]: Merge conflict
   at /home/runner/work/_actions/kurrent-io/Automations/master/cherry-pick-pr-for-label/node_modules/@octokit/request/dist-node/index.js:66:23
   at process.processTicksAndRejections (node:internal/process/task_queues:104:5) {
 status: '409',
 headers: {
   'access-control-allow-origin': '*',
   'access-control-expose-headers': 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset, Warning',
   'content-length': '127',
   'content-security-policy': "default-src 'none'",
   'content-type': 'application/json; charset=utf-8',
   date: 'Wed, 05 Aug 2026 10:03:31 GMT',
   'referrer-policy': 'origin-when-cross-origin, strict-origin-when-cross-origin',
   server: 'github.com',
   'strict-transport-security': 'max-age=31536000; includeSubdomains; preload',
   vary: 'Accept-Encoding, Accept, X-Requested-With',
   'x-accepted-github-permissions': 'contents=write',
   'x-content-type-options': 'nosniff',
   'x-frame-options': 'deny',
   'x-github-api-version-selected': '2022-11-28',
   'x-github-edge-region': 'iad',
   'x-github-media-type': 'github.v3; format=json',
   'x-github-request-id': 'E7C1:2F4E58:1601F3C:49FD2FD:6A730A73',
   'x-ratelimit-limit': '5000',
   'x-ratelimit-remaining': '4995',
   'x-ratelimit-reset': '1785927808',
   'x-ratelimit-resource': 'core',
   'x-ratelimit-used': '5',
   'x-xss-protection': '0'
 },
 request: {
   method: 'POST',
   url: 'https://api.github.com/repos/kurrent-io/KurrentDB/merges',
   headers: {
     accept: 'application/vnd.github.v3+json',
     'user-agent': 'octokit-core.js/3.3.2 Node.js/24',
     authorization: 'bearer [REDACTED]',
     'content-type': 'application/json; charset=utf-8'
   },
   body: '{"base":"cherry-pick-cherry-pick/5699/shaan1337/fix-write-timeout-msa-bugs-release/v26.0-087cd9af-0a53-419c-9436-3dfa5dc0b62c","commit_message":"Merge d6c2f3b00a8906686d84f9b5eb313edb02436d27 into cherry-pick-cherry-pick/5699/shaan1337/fix-write-timeout-msa-bugs-release/v26.0-087cd9af-0a53-419c-9436-3dfa5dc0b62c [skip ci]\\n\\n\\nskip-checks: true\\n","head":"d6c2f3b00a8906686d84f9b5eb313edb02436d27"}',
   request: { agent: [Agent], hook: [Function: bound bound register] }
 },
 documentation_url: 'https://docs.github.com/rest/branches/branches#merge-a-branch'
}

🚨👉 Check https://github.com/kurrent-io/KurrentDB/actions/runs/30995845342

@github-actions github-actions 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.

@shaan1337 👉 Created pull request targeting release/v26.1: #5700

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants