-
Notifications
You must be signed in to change notification settings - Fork 957
mssqlserver_cdc: ack-gate snapshot checkpoint and fix transaction-boundary resume (CON-504) #4677
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
squiidz
wants to merge
27
commits into
main
Choose a base branch
from
con-504-mssqlserver-ack-gate
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
9908f83
mssqlserver_cdc: track in-flight snapshot batch acks in the publisher
squiidz c5fdc32
mssqlserver_cdc: make batch tracking atomic with batch flushing
squiidz 2539c00
mssqlserver_cdc: checkpoint only fully-published transaction boundaries
squiidz 0c9ee6f
mssqlserver_cdc: gate post-snapshot checkpoint on downstream acks
squiidz c17934a
mssqlserver_cdc: adversarial crash tests for snapshot barrier and spl…
squiidz 53aef1f
mssqlserver_cdc: address review - fail the snapshot gate on nack, dro…
squiidz 2fdb39b
mssqlserver_cdc: checkpoint the exact end position of each drained po…
squiidz 5c04730
mssqlserver_cdc: reset the snapshot gate per attempt
squiidz 4e7ea3d
mssqlserver_cdc: log downstream batch rejections
squiidz 90d6ac3
mssqlserver_cdc: address review - terminal nacks restart with a fresh…
squiidz c3f2ff9
mssqlserver_cdc: nacks resolve checkpoints (auto_replay_nacks off is …
squiidz 16e7f8f
mssqlserver_cdc: serialize checkpoint persistence to prevent cache re…
squiidz 2ad7f4c
mssqlserver_cdc: unblock buffering under backpressure and rebuild the…
squiidz c9deaa8
mssqlserver_cdc: barrier the snapshot handoff behind parked flushers
squiidz ef971a5
mssqlserver_cdc: shut the publisher down from Close so the ticket cha…
squiidz 991bcc9
mssqlserver_cdc: log handoff flush cancellation at info
squiidz 81022d4
mssqlserver_cdc: cancellable ticket admission, batcher teardown under…
squiidz a3f52af
mssqlserver_cdc: seal the flush queue when an abandoned ticket drops …
squiidz eff7a9b
mssqlserver_cdc: log drops and poisoning, make the publisher pointer …
squiidz 143e303
mssqlserver_cdc: seal the queue when Track fails after admission
squiidz 20c9fc5
mssqlserver_cdc: seal the queue on a failed Flush in every path
squiidz a5a40c8
mssqlserver_cdc: cover the monotonic guard and poisoned rebuild with …
squiidz e2c8ad0
mssqlserver_cdc: make abandon-seal atomic and seal Flush errors under…
squiidz ee2f11b
mssqlserver_cdc: surface the real flush error in flushCurrent
squiidz 64b9fe6
mssqlserver_cdc: document that the Flush-error seals are contract-def…
squiidz ece7cd3
mssqlserver_cdc: log undelivered-at-shutdown batches at debug
squiidz 61364b5
mssqlserver_cdc: set the stopping flag before shutdown cancellation p…
squiidz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
138 changes: 138 additions & 0 deletions
138
internal/impl/mssqlserver/input_mssqlserver_cdc_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| // Copyright 2026 Redpanda Data, Inc. | ||
| // | ||
| // Licensed as a Redpanda Enterprise file under the Redpanda Community | ||
| // License (the "License"); you may not use this file except in compliance with | ||
| // the License. You may obtain a copy of the License at | ||
| // | ||
| // https://github.com/redpanda-data/connect/blob/main/licenses/rcl.md | ||
|
|
||
| package mssqlserver | ||
|
|
||
| import ( | ||
| "context" | ||
| "log/slog" | ||
| "sync" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/Jeffail/checkpoint" | ||
| "github.com/Jeffail/shutdown" | ||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/redpanda-data/benthos/v4/public/service" | ||
|
|
||
| "github.com/redpanda-data/connect/v4/internal/impl/mssqlserver/replication" | ||
| ) | ||
|
|
||
| // recordingCache is a minimal service.Cache capturing Set calls. | ||
| type recordingCache struct { | ||
| service.Cache | ||
| mu sync.Mutex | ||
| sets [][]byte | ||
| } | ||
|
|
||
| func (c *recordingCache) Set(_ context.Context, _ string, value []byte, _ *time.Duration) error { | ||
| c.mu.Lock() | ||
| defer c.mu.Unlock() | ||
| c.sets = append(c.sets, append([]byte(nil), value...)) | ||
| return nil | ||
| } | ||
|
|
||
| func (c *recordingCache) recorded() [][]byte { | ||
| c.mu.Lock() | ||
| defer c.mu.Unlock() | ||
| return append([][]byte(nil), c.sets...) | ||
| } | ||
|
|
||
| func newTestInput(t *testing.T) (*sqlServerCDCInput, *recordingCache) { | ||
| t.Helper() | ||
| cache := &recordingCache{} | ||
| i := &sqlServerCDCInput{ | ||
| cfg: &config{lsnCacheKey: "lsn"}, | ||
| res: service.MockResources(), | ||
| log: service.NewLoggerFromSlog(slog.Default()), | ||
| stopSig: shutdown.NewSignaller(), | ||
| cpCache: cache, | ||
| batching: service.BatchPolicy{Count: 1}, | ||
| checkpointLimit: 8, | ||
| } | ||
| batcher, err := i.batching.NewBatcher(i.res) | ||
| require.NoError(t, err) | ||
| pub := newBatchPublisher(batcher, checkpoint.NewCapped[replication.LSN](8), i.log) | ||
| pub.cacheLSN = i.cacheLSN | ||
| i.publisher.Store(pub) | ||
| t.Cleanup(func() { i.publisher.Load().shutSig.TriggerSoftStop() }) | ||
| return i, cache | ||
| } | ||
|
|
||
| // TestCacheLSNMonotonicGuard locks in the persist guard: advancing writes | ||
| // land, equal and regressing writes are silently skipped - a stale ack from | ||
| // an abandoned publisher generation must never move the durable resume | ||
| // position backwards. | ||
| func TestCacheLSNMonotonicGuard(t *testing.T) { | ||
| i, cache := newTestInput(t) | ||
| ctx := t.Context() | ||
|
|
||
| require.NoError(t, i.cacheLSN(ctx, replication.LSN("00000010"))) | ||
| require.NoError(t, i.cacheLSN(ctx, replication.LSN("00000020")), "an advancing LSN must persist") | ||
| require.NoError(t, i.cacheLSN(ctx, replication.LSN("00000020")), "an equal LSN is a no-op, not an error") | ||
| require.NoError(t, i.cacheLSN(ctx, replication.LSN("00000015")), "a regressing LSN is a no-op, not an error") | ||
|
|
||
| got := cache.recorded() | ||
| require.Len(t, got, 2, "only the two advancing writes may reach the cache") | ||
| require.Equal(t, "00000010", string(got[0])) | ||
| require.Equal(t, "00000020", string(got[1])) | ||
|
|
||
| require.Error(t, i.cacheLSN(ctx, nil), "an empty LSN is rejected") | ||
| } | ||
|
|
||
| // TestRebuildPublisherIfPoisoned proves the rebuild actually swaps | ||
| // generations: the old publisher is closed, the new one is a distinct | ||
| // publisher with a fresh tracker wired to cacheLSN, and a late ack from the | ||
| // OLD generation cannot regress the durable position past the guard. | ||
| func TestRebuildPublisherIfPoisoned(t *testing.T) { | ||
| i, cache := newTestInput(t) | ||
| ctx := t.Context() | ||
|
|
||
| old := i.publisher.Load() | ||
|
|
||
| // Not poisoned: same generation back. | ||
| same, err := i.rebuildPublisherIfPoisoned() | ||
| require.NoError(t, err) | ||
| require.Same(t, old, same) | ||
|
|
||
| // Deliver a batch on the old generation but hold its ack (late ack). The | ||
| // flushing Publish blocks on the unbuffered channel until consumed. | ||
| oldPublished := make(chan error, 1) | ||
| go func() { oldPublished <- old.Publish(ctx, streamingEvent("00000010", "00000010")) }() | ||
| oldMsg := <-old.msgs() | ||
| require.NoError(t, <-oldPublished) | ||
|
|
||
| // Poison and rebuild. | ||
| old.poisoned.Store(true) | ||
| rebuilt, err := i.rebuildPublisherIfPoisoned() | ||
| require.NoError(t, err) | ||
| require.NotSame(t, old, rebuilt, "a poisoned publisher must be replaced") | ||
| require.Same(t, rebuilt, i.publisher.Load(), "the stored pointer must be the new generation") | ||
| select { | ||
| case <-old.shutSig.HasStoppedChan(): | ||
| default: | ||
| t.Fatal("the old generation's flush loop must be stopped by the rebuild") | ||
| } | ||
|
|
||
| // The new generation persists progress normally. | ||
| newPublished := make(chan error, 1) | ||
| go func() { newPublished <- rebuilt.Publish(ctx, streamingEvent("00000030", "00000030")) }() | ||
| newMsg := <-rebuilt.msgs() | ||
| require.NoError(t, <-newPublished) | ||
| require.NoError(t, newMsg.ackFn(ctx, nil)) | ||
|
|
||
| // The old generation's late ack resolves into its abandoned tracker and | ||
| // must be a no-op on the durable position (monotonic guard). | ||
| require.NoError(t, oldMsg.ackFn(ctx, nil)) | ||
| got := cache.recorded() | ||
| require.Equal(t, "00000030", string(got[len(got)-1]), "a late ack from the abandoned generation must not regress the cache") | ||
| for _, v := range got { | ||
| require.NotEqual(t, "00000010", string(v), "the stale LSN must never have been persisted after the newer one") | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.