vstreamclient: framework for robust + simple usage - #17222
derekperkins wants to merge 79 commits into
Conversation
Review ChecklistHello reviewers! 👋 Please follow this checklist when reviewing this Pull Request. General
Tests
Documentation
New flags
If a workflow is added or modified:
Backward compatibility
|
8fa88ec to
829c980
Compare
829c980 to
378cd92
Compare
378cd92 to
97a18c6
Compare
rohit-nayak-ps
left a comment
There was a problem hiding this comment.
Overall it looks very good. I have mentioned some issues below. Once we address those, and whatever you feel are needed as part of this, we should merge and address the several TODOs noted in the future.
I am still undecided about where to locate it. I will let others weigh in here.
I had some issues running the unit tests:
- I had to drop the state table before running
TestVStreamClientfor the second time. - I could only run one of the tests at a time. Looks like the first test doesn't clean up the state and hence the second fails, if I just run
go test. Also it will be good to nameTestVStreamClienttoTestVStreamClientBasicor some such to make it easier to run each test separately - I think it might be easier not to require the state table by default (or provide some mechanism to clean it up either by default or by setting a flag separately). We could store the state locally for the duration of the test in-memory. Maybe if we fix the previous issues this may not be necessary though ...
- It is difficult to debug this in an IDE because of different short timeouts. While trying to figure out some local failures various contexts kept expiring even after setting a few of the values to high numbers. Not sure what the best solution for this is: maybe move all timeout constants to a common place so anyone developing/debugging can modify it there. Maybe this is just needed for the initial phase, but others who fork this might also run into it while testing their changes.
Also:
- We can just send "" as the
vgtidto start with the copy phase. No need to generate the per-shard vgtid done inNew() - We should also get the failing tests working. Maybe it just needs a rebase. If not, let us know if you need help with any eventual failing tests.
- Not sure if you had a look at the e2e tests in
go/test/endtoend/vreplication. Those allow creating a cluster if you wanted to create an e2e test for this. - Can you explain the specific tests for which you wanted to use non-exported functions from vtgate/vttablet.
go/test/endtoend/vreplicationhas tests where entire clusters are created. Do those help?
|
@rohit-nayak-ps thanks for the review! Yes, the unit tests weren't meant to be true unit tests, just a way to test functionality, with me truncating and restarting regularly. Whatever the new test harness turns out to be will be easily testable. I just wanted to make sure I had an API that people thought was good and that I didn't need to do any major refactoring before spinning it up.
I'm not sure how to do that. The
|
97a18c6 to
f9d7402
Compare
Understood. I see you are already working on this.
See example in end to end test |
That vstream test code seems to be initializing each shard. I was patterning my solution on that and other test code I saw, except I am handling generic keyspaces / shards. Am I missing something? |
|
This PR is being marked as stale because it has been open for 30 days with no activity. To rectify, you may do any of the following:
If no action is taken within 7 days, this PR will be closed. |
|
This PR was closed because it has been stale for 7 days with no activity. |
Three related fixes to how New interacts with the state row:
Validation now runs before any state mutation, and the ownership claim
is the last step, so a constructor that fails (bad stored JSON, table
config mismatch, invalid explicit position) can never fence a healthy
running client that it will not replace.
The bootstrap/restart upserts are replaced by a plain insert (a
concurrent initializer surfaces as a duplicate-key conflict mapped to
ErrFenced) and an owner-token-predicated update (a stale constructor
that lost ownership after reading state cannot steal it back). The
update also explicitly resets copy_completed when restarting a copy,
so a previous completed flag can no longer survive alongside a fresh
copy position and mask a partial copy after a crash.
Persisted table_config is now unmarshalled into value types, so
malformed-but-valid JSON like {"ks.t":null} produces a structured
validation error on New instead of a nil-pointer panic on every
startup attempt.
Addresses PR review feedback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Derek Perkins <derek@nozzle.io>
Five related fixes from PR review: The shutdown cause (ErrHeartbeatTimeout / ErrStartupTimeout) is now recorded before the monitor waits for the graceful flush, and Run surfaces it even when the final flush succeeds, so applications that restart only on errors can't silently stop consuming. The monitor starts before the VStream is opened, so a blocked stream setup is covered by the startup deadline instead of hanging outside it. The effective startup timeout is floored at the heartbeat liveness window, since an idle stream at a concrete position may not deliver its first event until the first heartbeat fires. The monitor re-reads the last-event timestamp after observing that processing is idle, closing the semantic race where a stale timestamp paired with a fresh idle flag could trigger a false timeout. endRun closes the graceful-shutdown flush channel, so a GracefulShutdown caller no longer blocks for its entire wait duration when Run exits through an error path where no future flush can occur. The shutdown-requested check is also exported as ShutdownRequested so flush functions can detect a pending shutdown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
A heartbeat is not a transaction boundary: with TransactionChunkSize enabled, VTGate can deliver BEGIN and row chunks in separate batches while its aggregate heartbeat ticker runs independently. Flushing on a heartbeat between chunks would expose uncommitted rows (which could still roll back) and, since the checkpoint remains at the previous committed position, duplicate the flushed prefix on replay. Track BEGIN/terminal state and skip heartbeat-triggered flushes until the transaction terminates. Addresses PR review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
Delete events carry only a before-image, and RowChange.DataColumns only describes after-images, so under NOBLOB or MINIMAL row images an omitted delete value is indistinguishable from SQL NULL for both the default decoder and custom scanners, silently corrupting nullable fields downstream. New now probes @@global.binlog_row_image for every source keyspace and rejects anything other than FULL; if the setting cannot be queried, it logs a warning and continues. Addresses PR review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
No runtime path read the field, so setting it promised transactional sink behavior and delivered none. Remove it while the API is unreleased; it can return together with an implementation. Addresses PR review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
Four honesty fixes from PR review: the shutdown-channel tests now require a clean graceful exit before the outer run deadline (a client that ignored the channel would only end via the deadline and fail), the threshold test records FlushMeta reasons and requires a MaxRowsPerFlush-triggered flush, the multi-table test no longer cancels the run right after GracefulShutdown (the shutdown alone must end the run, and the persisted checkpoint is verified), and the slow-flush replay case waits for ShutdownRequested before releasing the flush gate instead of sleeping, so it deterministically proves shutdown-during-flush semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
The heartbeat test now proves the flush boundary was a heartbeat: it records the FlushReason and whether a heartbeat event arrived after the row was buffered, with a 5s min flush duration so the row's own COMMIT cannot satisfy the threshold. The transaction-boundary test now enables TransactionChunkSize (so VTGate delivers the transaction in separate chunks), holds the transaction open across observed heartbeats while asserting nothing is delivered before COMMIT, and requires both rows to arrive in exactly one flush. The deterministic mid-transaction heartbeat interleaving is pinned by the package unit test TestHandleEvents_HeartbeatMidTransactionDefersFlush; nothing streams from the binlog until the source transaction commits, so that ordering cannot be forced end to end. Addresses PR review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
…queries The same-table-name routing test previously used no-op FlushFns and only inspected raw event names from hooks, so misrouting between the two TableConfigs would still pass; each table now has its own collector and the test asserts every row reached only its intended FlushFn. The background exec helpers also move from context.Background() to WithoutCancel plus a generous timeout, so a stuck VTGate can no longer hang the package indefinitely during cleanup. Addresses PR review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
…owner token The full e2e suite caught a false fence in the two-statement flow: an interrupted copy leaves the row holding exactly the values a restart re-persists, so the owner-predicated update changed nothing and MySQL's zero changed-rows count was misread as a fence. Fold the claim and the persist into a single update that compares against the owner token observed when state was read and rotates it to this client's token: the CAS makes racing constructors lose deterministically with ErrFenced, and the token rotation guarantees the row always changes when the CAS matches, so identical values can never be mistaken for a fence. NULL-safe comparison keeps rows without a token claimable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
New previously fenced the incumbent consumer as its final step, so a caller that constructed a client and then delayed, abandoned, or crashed before Run — or whose stream setup failed — left no active consumer. New now only reads and validates state and prepares the takeover; Run executes the compare-and-swap once the VStream is established, so ownership is only ever taken by a client that is actually about to consume. A takeover that loses the CAS surfaces from Run as an error wrapping ErrFenced. Addresses PR review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
New constructor-level tests prove a takeover whose compare-and-swap affects zero rows fails with ErrFenced (both the claim-only and persist paths), a concurrent bootstrap insert's duplicate key maps to ErrFenced, and of two constructors that observed the same owner token, exactly one can win the takeover. Addresses PR review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
With the watchdog now covering stream establishment, a blocked VStream call is canceled with ErrStartupTimeout, but the error path wrapped the generic transport error, breaking the documented errors.Is contract. Surface the cancel cause like the Recv path does, with a blocked-VStream regression test that also proves a Run that never established its stream takes no ownership. Addresses PR review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
VTGate treats ROLLBACK as terminal, but the transaction tracking left inTransaction set, so after BEGIN/ROLLBACK an idle stream suppressed every later heartbeat flush, including shutdown completion and checkpointing of previously buffered committed rows. Clear the state on ROLLBACK (rows from rolled-back transactions are never delivered, so there is nothing to discard) and stop describing heartbeats as unconditional safe boundaries in the README. Addresses PR review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
The single timestamp re-read still allowed an incoherent pair: a batch could start after the idle observation and store a fresh timestamp mid-comparison, or on the startup path finish between the zero timestamp read and the idle check. The liveness check now requires the timestamp to be unchanged across the idle observation (handleEvents stores the timestamp before clearing the processing flag, so any moved value means a batch completed and the window reset), and the startup check observes idle before reading the timestamp for the same reason. Addresses PR review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
The Default* package variables are documented as safely modifiable, but New copied them unvalidated: a zero or negative startup timeout, heartbeat multiplier, min flush duration, or graceful shutdown wait produced immediate cancellations at runtime instead of a precondition error. New now validates the effective values, and the monitor's last-resort floors use fixed constants instead of rereading the same possibly-invalid globals. Addresses PR review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
An explicit position becomes the durable restart point with copy_completed=true, so a typo could poison state and fence the incumbent before VTGate ever rejected it. WithStartingVGtid now parses every gtid (rejecting values like 'garbage'), rejects duplicates, requires every (keyspace, shard) to be a configured source shard (rejecting foreign and state keyspaces), and requires the position to cover every shard of every configured source keyspace, since VTGate only opens streams for the listed shards and omitted shards would silently never be consumed. Addresses PR review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
… closed An untargeted probe only observed one arbitrary shard, and probe failures were logged and ignored, so a sharded keyspace with another primary running NOBLOB (or a failed probe) proceeded straight into the silent delete corruption the check exists to prevent. Probe each shard via a keyspace:shard target and fail closed on both non-FULL values and unverifiable shards, with WithSkipRowImageCheck as an explicit opt-out for environments where the probe cannot run. The README now documents FULL as a prerequisite for the entire retained replay range and drops the unsafe suggestion that NOBLOB/MINIMAL can be handled in FlushFn: delete before-images carry no presence bitmap, so omitted values are simply not on the wire. Addresses PR review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
The shard-name heuristic cannot distinguish a sharded keyspace whose single shard happens to be named '0' from an unsharded keyspace. New now reads SHOW VSCHEMA KEYSPACES and WithStateTable rejects keyspaces whose vschema marks them sharded, keeping the shard-name heuristic only as a backstop for clusters where the vschema cannot be read. Addresses PR review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
…sets The threshold-shutdown test now gates the threshold flush, requests shutdown while that flush is provably in progress, releases it, and requires Run to exit on that very flush with no later boundary needed. The multi-table shutdown test restarts with a sentinel row and proves the priming rows do not replay, so the graceful shutdown checkpoint is verified by behavior rather than by the mere presence of shard gtids. The README's same-table-name section now documents the identical rule-set restriction with an example. Addresses PR review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
The full-suite run caught the new no-replay assertion firing on a legitimate replay: the priming run only waited for first row events, so the graceful shutdown could land mid-copy, leaving copy_completed false and correctly restarting the copy on the next client. Wait for the copy checkpoint before shutting down, so the no-replay proof tests the resume path it is meant to test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
Signed-off-by: Derek Perkins <derek@nozzle.io>
Signed-off-by: Derek Perkins <derek@nozzle.io>
Signed-off-by: Derek Perkins <derek@nozzle.io>
| // EventFunc is an optional callback function that can be registered for individual event types | ||
| type EventFunc func(ctx context.Context, event *binlogdatapb.VEvent) error |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0704e1be49
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // effect for the entire retained replay range. | ||
| func (v *VStreamClient) validateBinlogRowImage(ctx context.Context) error { | ||
| if v.cfg.skipRowImageCheck { | ||
| log.Warn("vstreamclient: binlog_row_image verification skipped; delete events are silently corrupted unless every source shard uses FULL") |
There was a problem hiding this comment.
Include the stream name in the skipped-check warning
When a process constructs multiple VStream clients, this warning does not identify which stream disabled the row-image safety check, leaving operators unable to trace a potential corruption risk back to the responsible workflow. Add the configured stream name as a structured field, as the other liveness warnings in this package already do.
AGENTS.md reference: AGENTS.md:L116-L117
Useful? React with 👍 / 👎.
| if !claimedOwnership { | ||
| err = v.takeStateOwnership(ctx) | ||
| if err != nil { |
There was a problem hiding this comment.
Mark the first response before claiming ownership
When the first Recv succeeds near the startup deadline but the state-row claim is slow, the watchdog still sees lastEventProcessedAtUnixNano == 0 and isProcessingEvents == false because that guard is not set until handleEvents, after this ownership call. It can therefore cancel a stream that already received a response; if the claim then completes successfully, this run exits while leaving its owner token persisted, fencing the healthy incumbent without an active replacement. Mark the response or ownership phase as active before issuing the state write.
AGENTS.md reference: AGENTS.md:L119-L124
Useful? React with 👍 / 👎.
| // call the user-defined event function if it exists | ||
| fn, ok := v.cfg.eventFuncs[ev.Type] | ||
| if ok { | ||
| err = fn(ctx, ev) | ||
| if err != nil { | ||
| return fmt.Errorf("vstreamclient: user error processing %s event: %w", ev.Type.String(), err) |
There was a problem hiding this comment.
Flush old-schema rows before invoking DDL hooks
When a DDL hook applies the schema change to a downstream sink, it runs here before the DDL branch calls flush. Rows from earlier committed transactions may still be buffered because they did not meet the duration or row threshold, so the client then sends old-schema rows to a sink whose schema has already changed, which can reject or misinterpret them. Flush the pre-DDL batch before invoking a hook intended to apply the DDL, or provide a post-flush DDL callback.
AGENTS.md reference: AGENTS.md:L119-L124
Useful? React with 👍 / 👎.
copyRowToStructInLocation re-derived per-type constants on every row. tryScanSpecialField called structField.Addr().Type().Implements twice per field per row to probe for sql.Scanner and encoding.TextUnmarshaler, and the field mappings were iterated as a map. Neither answer can change between rows of the same shard. Cache isTime and both interface verdicts on fieldMapping when the shard's mappings are built, and return them as a slice ordered by row index so decoding drops the map iterator and reads in wire order. Addr is now taken only when a cached verdict says the interface is satisfied. BenchmarkCopyRowToStruct, benchstat n=10: -48.43% sec/op (559.6n -> 288.6n), allocations unchanged. Behavior is unchanged except that multi-field errors now report in row order instead of map order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Derek Perkins <derek@nozzle.io>
| if flags.StreamKeyspaceHeartbeats { | ||
| return vterrors.New(vtrpcpb.Code_FAILED_PRECONDITION, "vstreamclient: StreamKeyspaceHeartbeats is not supported: it streams internal sidecar heartbeat table events that have no TableConfig") | ||
| } | ||
|
|
||
| // clone so later caller mutations can't change stream behavior or bypass the validation above | ||
| v.cfg.flags = proto.Clone(flags).(*vtgatepb.VStreamFlags) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c904050432
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| > `MaxRowsPerFlush` means up to that many rows can stay buffered in memory until a flush runs, so make sure the process | ||
| > has enough memory for the table shape and batch size you choose. It can also help to align this number with the |
There was a problem hiding this comment.
Document that MaxRowsPerFlush is not a hard buffer cap
This promises that at most MaxRowsPerFlush rows remain buffered, but handleRowEvent appends an entire row event and flushing occurs only at safe transaction boundaries. With TransactionChunkSize enabled, a transaction can span many responses and grow currentBatch far beyond this value before COMMIT; a single large response can exceed it as well. Operators relying on this statement for memory sizing can therefore still encounter unbounded transaction-sized memory use, so the documentation should describe the setting as a flush trigger and output chunk limit rather than a maximum buffer size.
Useful? React with 👍 / 👎.
| row = sqltypes.MakeRowTrusted(shard.fields, rc.After) | ||
| vstreamStats.RowInsertCount++ | ||
| table.stats.RowInsertCount++ | ||
|
|
||
| case rc.Before != nil: // update event | ||
| row = sqltypes.MakeRowTrusted(shard.fields, rc.After) |
There was a problem hiding this comment.
🔴 Partial updates mislabel column values
When partial updates omit leading columns, MakeRowTrusted assigns remaining values the wrong column types. Custom scanners can reject or corrupt valid updates.
Prompt for agents
In go/vt/vstreamclient/table.go, TableConfig.handleRowEvent converts compact partial after-images with sqltypes.MakeRowTrusted(shard.fields, rc.After). That helper pairs row lengths with fields positionally, but RowChange.DataColumns identifies which full-schema columns are actually present. If an omitted column precedes a present one, the resulting sqltypes.Value gets the omitted column's type. Build the row for VStreamScanner implementations using DataColumns to select the corresponding field types while preserving the compact value order. Full images and delete before-images must retain their current behavior. Add a scanner test where a partial after-image omits the first field and includes a later field of a different SQL type.
Was this helpful? React with 👍 or 👎 to provide feedback.
Description
Implementation for #17221. I did a walkthrough in the March community meeting, starting at 7:18.
The actual code was 95% written by me, tests were 95% written by AI
Related Issue(s)
Checklist
Deployment Notes
Adds the public
go/vt/vstreamclientpackage for checkpointed Vitess event consumption.