fix(gtc): prevent CDC data loss on poison records, streamed transactions, and reconnects - #557
Conversation
…transactions, and reconnects - Dead-lettered records no longer halt the pipeline: after exhausted retries, a record successfully parked in the DLQ is skipped and the checkpoint advances. The runtime still fails when no DLQ is configured or the DLQ write itself fails, and replay-dlq surfaces sink failures instead of silently re-parking entries. - Disable pgoutput in-progress transaction streaming. The decoder never handled StreamCommit/StreamAbort, so streamed transactions were silently dropped (or leaked into the next transaction on abort); the consumer buffers until commit anyway, so streaming gave no benefit. The decoder now rejects stream messages loudly if re-enabled. - Resume replication from the advanced client LSN on reconnect instead of rewinding to the original start LSN, and reset decoder state on each new replication session. - Record sink error/retry metrics and add a dead-letter counter. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015houJkqb8SuqPW4YpLoWCq
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
trenova | daa5c10 | Aug 30 2026, 07:58 PM |
|
Warning Review limit reachedNext included review available in 46 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change disables streamed WAL transactions, resets decoder state on reconnect, resumes replication from the latest client LSN, and adds dead-letter handling and metrics for exhausted projection retries. ChangesWAL recovery and streaming control
Dead-letter projection handling
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: 🔵 Low · up to The PR improves CDC recovery and dead-letter handling, but a partial dead-letter replay failure can repeat events already written to append-only sinks. The change is mergeable with owner awareness and a follow-up to ensure replay consumers or sinks provide idempotency. Sequence Diagram(s)sequenceDiagram
participant Runtime
participant Sink
participant DeadLetterQueue
participant Metrics
Runtime->>Sink: Write projection
Sink-->>Runtime: Return failure
Runtime->>DeadLetterQueue: Write dead-letter record
Runtime->>Metrics: Increment dead-letter counter
DeadLetterQueue-->>Runtime: Confirm write
Runtime->>Runtime: Continue processing
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment |
…auto-rating migration Bun's migrator keys migrations by their numeric prefix, so 20260928000000_shipment_auto_rating and 20260928000000_widen_rate_agreement_versions collided: the widen migration's Up/Down overwrote the auto-rating one during Discover, and the auto_rated/auto_rated_at columns were never created. Every Test Go run on master has been red since the two landed together. Rename shipment_auto_rating to 20260928100000 (postgres + sqlite). Databases that already recorded 20260928000000 actually ran the widen SQL, so history stays truthful; the renamed migration runs as new and its DDL is idempotent. Also regenerate pkg/reportcatalog/catalog_gen.go, which was stale for the same feature's autoRated/autoRatedAt fields and failing the Codegen Checks job. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015houJkqb8SuqPW4YpLoWCq
|
The two failing checks on the first push (Codegen Checks, Integration Tests) are pre-existing base-branch failures, not caused by this PR's GTC-only diff — Migration name collision (Integration Tests). Stale report catalog (Codegen Checks). The Cloudflare Workers deployment failure looks unrelated to this diff (no client changes here) and its logs are only visible in the Cloudflare dashboard — worth a look there if it persists on this PR's latest commit. Generated by Claude Code |
Description
Fixes three silent data-loss / availability bugs in the GTC CDC pipeline:
writeProjectionstill returned the error, which propagated up and killed the entire runtime — one permanently-bad record halted all replication. The retry loop is now split from the dead-letter decision: a record successfully parked in the DLQ is skipped (logged at error level and counted in a newgtc_dead_lettered_records_totalmetric) and the checkpoint advances past it. The runtime still fails hard when no DLQ is configured, when the DLQ write itself fails, or during shutdown (context cancellation never dead-letters).replay-dlquses the raw write path so a failed replay surfaces to the operator instead of silently re-parking the entry and reporting success.streaming 'true'but the decoder never handledStreamCommit/StreamAbort, so any large transaction Postgres streamed before commit was silently dropped (and on abort, its records leaked into the next transaction). Streaming is now off (streaming 'false'): the consumer buffers all records until commit anyway, so streaming provided no consumer-side benefit, and pgoutput protocol v2 doesn't carry per-change subtransaction info, making correct subtransaction-abort handling impossible at this protocol version. The decoder now rejects stream messages with a loud error if the option is ever re-enabled, instead of corrupting silently.Start(), so any reconnect after hours of streaming replayed everything from the original position. It now resumes from the advanced client LSN (falling back to the start LSN only on first connect), and decoder state is reset on each new replication session so a half-received transaction from a broken connection can't bleed into the resumed stream.Also wires the previously declared-but-never-recorded
SinkErrorsandRetryAttemptsmetrics in the sink retry loop.Related Issue or Discussion
Found during a codebase completeness review; maintainer-driven session.
Type of Change
Scope
services/gtc/internal/core/services/runtime.go(+ tests)services/gtc/internal/adapters/primary/wal/decoder.go,reader.go(+ tests)services/gtc/internal/infrastructure/metrics/metrics.goValidation
cd services/gtc && go build ./... && go vet ./... && go test ./...— all packages pass;gofmt -lclean on touched packages.golangci-lintcould not run in this environment (binary built with Go 1.25, repo targets Go 1.26).cd services/tms && task test— not run; no TMS changes.cd services/tms && task lint— not run; no TMS changes.cd client && pnpm build— not run; no client changes.cd client && pnpm lint— not run; no client changes.New test coverage: poison record → DLQ + checkpoint advance; halt when no DLQ configured; halt when DLQ write fails; replay surfaces sink failures without re-parking; decoder rejects stream messages; decoder reset clears transaction state; reconnect resume-LSN selection.
Deployment Notes
gtc_dead_lettered_records_totaland sink health; replay withreplay-dlqonce the sink recovers.streaming 'false'takes effect on the next replication session; no migration needed. No config or schema changes.Checklist
AGENTS.md,CLAUDE.md, and existing repository patterns.🤖 Generated with Claude Code
https://claude.ai/code/session_015houJkqb8SuqPW4YpLoWCq
Generated by Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests