Skip to content

vstreamclient: framework for robust + simple usage - #17222

Open
derekperkins wants to merge 79 commits into
vitessio:mainfrom
derekperkins:vstreamclient
Open

derekperkins wants to merge 79 commits into
vitessio:mainfrom
derekperkins:vstreamclient

Conversation

@derekperkins

@derekperkins derekperkins commented Nov 13, 2024 •

Copy link
Copy Markdown
Member

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

  • "Backport to:" labels have been added if this change should be back-ported to release branches
  • If this change is to be back-ported to previous releases, a justification is included in the PR description
  • Tests were added or are not required
  • Did the new or modified tests pass consistently locally and on CI?
  • Documentation was added or is not required

Deployment Notes

Adds the public go/vt/vstreamclient package for checkpointed Vitess event consumption.

  • Checkpoints require a separate unsharded state keyspace. Construction requires readable VSchema sharding metadata and executes CREATE TABLE IF NOT EXISTS, even for a pre-provisioned table.
  • All source primaries, eligible stream tablets, and writer/replication sessions must use FULL row images throughout the retained replay range. New probes each shard's primary and configured stream tablet type; unverifiable targets fail closed unless explicitly bypassed after out-of-band verification.
  • Run claims ownership after receiving its first successful VStream response. Initial receive failures and startup timeouts leave the incumbent's owner token untouched. State-row fencing protects checkpoints; concurrent consumers still require serialized handoffs or sink-enforced fencing/version checks.
  • Explicit starting positions must be concrete and cover every configured source shard for the selected tablet type. Symbolic positions and TablePKs copy cursors are rejected.
  • The default liveness window is now ten seconds. Liveness failures cancel immediately; WithHeartbeatSeconds overrides WithFlags.HeartbeatInterval regardless of option order, including when the supplied flags leave the interval at zero.
  • Graceful shutdown can complete at a ROLLBACK boundary without waiting for a later heartbeat.
  • Construction rejects heartbeat intervals and timeout multipliers whose combined liveness window exceeds the maximum supported duration.

@vitess-bot

vitess-bot Bot commented Nov 13, 2024

Copy link
Copy Markdown
Contributor

Review Checklist

Hello reviewers! 👋 Please follow this checklist when reviewing this Pull Request.

General

  • Ensure that the Pull Request has a descriptive title.
  • Ensure there is a link to an issue (except for internal cleanup and flaky test fixes), new features should have an RFC that documents use cases and test cases.

Tests

  • Bug fixes should have at least one unit or end-to-end test, enhancement and new features should have a sufficient number of tests.

Documentation

  • Apply the release notes (needs details) label if users need to know about this change.
  • New features should be documented.
  • There should be some code comments as to why things are implemented the way they are.
  • There should be a comment at the top of each new or modified test to explain what the test does.

New flags

  • Is this flag really necessary?
  • Flag names must be clear and intuitive, use dashes (-), and have a clear help text.

If a workflow is added or modified:

  • Each item in Jobs should be named in order to mark it as required.
  • If the workflow needs to be marked as required, the maintainer team must be notified.

Backward compatibility

  • Protobuf changes should be wire-compatible.
  • Changes to _vt tables and RPCs need to be backward compatible.
  • RPC changes should be compatible with vitess-operator
  • If a flag is removed, then it should also be removed from vitess-operator and arewefastyet, if used there.
  • vtctl command output order should be stable and awk-able.

@vitess-bot vitess-bot Bot added NeedsBackportReason If backport labels have been applied to a PR, a justification is required NeedsDescriptionUpdate The description is not clear or comprehensive enough, and needs work NeedsIssue A linked issue is missing for this Pull Request NeedsWebsiteDocsUpdate What it says labels Nov 13, 2024
@github-actions github-actions Bot added this to the v22.0.0 milestone Nov 13, 2024
@derekperkins derekperkins changed the title Vstreamclient vstreamclient: framework for robust + simple usage Nov 13, 2024
@derekperkins
derekperkins marked this pull request as ready for review December 9, 2024 23:35
@derekperkins
derekperkins requested a review from deepthi as a code owner December 9, 2024 23:35

@rohit-nayak-ps rohit-nayak-ps left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TestVStreamClient for 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 name TestVStreamClient to TestVStreamClientBasic or 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 vgtid to start with the copy phase. No need to generate the per-shard vgtid done in New()
  • 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/vreplication has tests where entire clusters are created. Do those help?

@derekperkins

Copy link
Copy Markdown
Member Author

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

We can just send "" as the vgtid to start with the copy phase. No need to generate the per-shard vgtid done in New()

I'm not sure how to do that. The VGtid struct only has []*ShardGtid. It didn't accept me sending through an empty VGtid (maybe I only tested nil?), so this seemed like the only way.

go/test/endtoend/vreplication seems like it has what I need for testing at first glance. I hope to get the tests set up and PR passing this weekend.

@derekperkins derekperkins added Type: Feature Component: VReplication and removed NeedsDescriptionUpdate The description is not clear or comprehensive enough, and needs work NeedsIssue A linked issue is missing for this Pull Request NeedsBackportReason If backport labels have been applied to a PR, a justification is required labels Jan 10, 2025
@rohit-nayak-ps

Copy link
Copy Markdown
Member

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.

Understood. I see you are already working on this.

We can just send "" as the vgtid to start with the copy phase. No need to generate the per-shard vgtid done in New()
I'm not sure how to do that. The VGtid struct only has []*ShardGtid. It didn't accept me sending through an empty VGtid (maybe I only tested nil?), so this seemed like the only way.

See example in end to end test TestVStreamCurrent:

var shardGtids []*binlogdatapb.ShardGtid
	vgtid := &binlogdatapb.VGtid{}
	shardGtids = append(shardGtids, &binlogdatapb.ShardGtid{
		Keyspace: "ks",
		Shard:    "-80",
		Gtid:     "current",
	})
	shardGtids = append(shardGtids, &binlogdatapb.ShardGtid{
		Keyspace: "ks",
		Shard:    "80-",
		Gtid:     "current",
	})
	vgtid.ShardGtids = shardGtids
	filter := &binlogdatapb.Filter{
		Rules: []*binlogdatapb.Rule{{
			Match:  "t1",
			Filter: "select * from t1",
		}},
	}
	flags := &vtgatepb.VStreamFlags{}
	reader, err := gconn.VStream(ctx, topodatapb.TabletType_PRIMARY, vgtid, filter, flags)

@derekperkins

Copy link
Copy Markdown
Member Author

See example in end to end test TestVStreamCurrent:

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?

https://github.com/vitessio/vitess/blob/ecb37200d31c389883129b5d141c279ecdfcd688/go/vt/vstreamclient/state.go#L100-L106

@github-actions

Copy link
Copy Markdown
Contributor

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:

  • Push additional commits to the associated branch.
  • Remove the stale label.
  • Add a comment indicating why it is not stale.

If no action is taken within 7 days, this PR will be closed.

@github-actions github-actions Bot added the Stale Marks PRs as stale after a period of inactivity, which are then closed after a grace period. label Feb 10, 2025
@github-actions

Copy link
Copy Markdown
Contributor

This PR was closed because it has been stale for 7 days with no activity.

@github-actions github-actions Bot closed this Feb 17, 2025
derekperkins and others added 23 commits August 30, 2026 13:59
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>

Copilot AI 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.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.

@devin-ai-integration devin-ai-integration 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.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 1 new potential issue.

Devin Review

Comment on lines +49 to +50
// EventFunc is an optional callback function that can be registered for individual event types
type EventFunc func(ctx context.Context, event *binlogdatapb.VEvent) error

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.

🔍 Type declarations remain ungrouped

The new files declare types separately, despite the repository rule requiring one grouped type declaration at each file’s top.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +198 to +200
if !claimedOwnership {
err = v.takeStateOwnership(ctx)
if err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +294 to +299
// 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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>

Copilot AI 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.

Pull request overview

Copilot reviewed 24 out of 24 changed files in this pull request and generated 1 comment.

Comment on lines +219 to +224
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)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +313 to +314
> `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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 new potential issue.

Devin Review

Comment on lines +524 to +529
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)

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.

🔴 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.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

This branch has not been deployed

No deployments
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.

RFC: VStream client reference implementation

7 participants