Runtime "Sync Now" - #3326
Open
dgreer-dev wants to merge 5 commits into
Open
Conversation
williamhbaker
left a comment
Member
There was a problem hiding this comment.
Some directional comments! I didn't get into the gritty details just yet since I am hoping things can be simplified quite a bit.
dgreer-dev
marked this pull request as draft
August 10, 2026 23:37
Contributor
Author
|
Pushed a commit with the four resolved convo fixes.
|
Remove the check which rejected a materialization configuring a connector-side `syncSchedule` alongside a model-level one, and with it `connector_config_has_sync_schedule` -- which sniffed the undecrypted endpoint config for the key, treating an empty object or all-null / all empty-string values as absent so that a UI clearing the schedule didn't leave a tripwire behind. Configuring both is not something we want users to do, but it doesn't break: the two pacing mechanisms just interleave chaotically, and we plan to migrate extant configs automatically. That isn't worth the complexity, nor the guesswork of deciding what a disabled connector schedule looks like. It also stood in the way of runtime-only commit pacing, where a model-level `syncSchedule` is the sole cadence control and the connector's is left at its documented all-zero off switch. The connector parses a Go duration, which accepts "0m0s" and a bare "0" as readily as "0s", so the check would have had to enumerate zero spellings to let that through. `SyncScheduleInvalid` stays: a malformed schedule is still an error.
dgreer-dev
force-pushed
the
daveg/3281-sync-now
branch
from
August 12, 2026 15:37
731ba0e to
c71883f
Compare
dgreer-dev
marked this pull request as ready for review
August 12, 2026 15:37
Sync-now forces an immediate commit of a materialization's open transaction -- collapsing any sync-schedule hold -- and resolves once that transaction is fully acknowledged: committed and queryable in the endpoint. Its mechanism is a commit barrier built from two session messages. CloseNow, which the leader already honors as its FSM's `close_requested` input, gains a controller-assigned sequence. The new Synced answers it: the leader reports its count of fully-acknowledged transactions of this session, the transactions still pending ahead of that count, and the highest CloseNow sequence received, broadcasting whenever any of the three change. Shards relay Synced to their controllers unmodified, exactly as CloseNow flows the other way. A controller sends CloseNow with sequence S, waits for a Synced echoing `close_request_seq >= S`, and then awaits `acknowledged_count` reaching that same message's acknowledged + pending. The echo is what makes the barrier exact rather than best-effort: counts travel leader -> shard -> controller, so counts a controller already holds can predate a transaction which has since opened, while the echoing report was taken after the request landed and covers everything the leader then held. The leader keeps no waiters, no timers, and no notion of a caller: repeated CloseNow is idempotent, and a session which exits simply stops reporting. Counts restart from zero with the next session, so a controller must discard a target recorded under a session which ended. Also declares the user-facing TaskControl service and its SyncNow request/response messages. The reactor front door serves them by driving this barrier, in the following commit. The sync_now_e2e test drives the barrier against a real leader session -- a real tonic server behind an armed LEAD interceptor, with real journal IO gating Tail::Done -- playing both the shard and controller sides itself. Covered: a schedule-held transaction collapsing on CloseNow and resolving only on the released acknowledgement; repeated CloseNow yielding one commit; a barrier taken in the post-commit drain window resolving at that same acknowledgement; the min-duration floor bypass; and the pipelined case of an open Head transaction behind a draining Tail, which is what pending_count's arithmetic exists for.
…in the CLI The reactor front door serves TaskControl.SyncNow by driving the CloseNow / Synced commit barrier of the prior commit. materializeAppV2 tracks the leader's Synced counts per session, and its session loop sends CloseNow when woken. syncNow claims a sequence, wakes the loop, waits for the leader's echo, takes acknowledged + pending from the echoing report as its target, and awaits acknowledged_count reaching it. Concurrent callers coalesce onto one CloseNow bearing the highest claimed sequence. A session which ends fences its counts and fails parked callers with Unavailable; sync-now is idempotent, and re-invoking re-establishes the contract. The front door resolves the task's shard zero and answers where it is primary, taking the shard's Store as a syncNower. A non-primary front door forwards the RPC to the one which is, gazette mayProxy style, with the proxied Header riding gRPC metadata (SyncNowRequest is user-facing and deliberately has no Header field) and the caller's Authorization forwarded verbatim for independent verification. Captures and derivations hold no open transaction and are answered immediately from the shard's type; a materialization whose Store is no syncNower runs the V1 runtime, which has no leader to ask: NOT_FOUND. Users reach the service as HTTP/NDJSON (task_control_http.go), sharing the CORS treatment of gazette's grpc-gateway /v1/ mux; gRPC carries only the front-door-to-front-door proxy hop. Each response line is flushed as it's sent, and 15s heartbeats hold an hour-long wait open through load-balancer idle timeouts. This endpoint is the UI contract of the dashboard's future "Sync Now" button. flowctl raw sync-now calls the HTTP endpoint bearing the user's task authorization, printing the ack and done JSON lines and exiting zero on completion. Attempts are retried while failures are retryable -- a broken stream, a 5xx, or the leader's own Unavailable -- re-reading the front door address and token each time, as an hour-long wait may outlive both. TestSyncNowBarrier covers the barrier arithmetic over a bare materializeAppV2: ack withheld until the leader's echo, the echo catching a transaction opened behind stale counts, session fencing before and after the ack, and coalescing of concurrent callers. TestTaskControlSyncNow drives both transports over real consumertest members: per-message NDJSON flushing, proxying from a non-primary front door, error passthrough, task resolution, claim scoping, and V1 / capture / derivation answers.
- Resolve the task's type from the shard spec's task-type label rather than re-parsing the shard ID prefix, with taskShardZero returning the spec it already decoded. - Encode the Begin->Done stopping-shortcut exclusion of acknowledged_count as an exhaustive Tail::resolves_on_done, and pin it with a unit test. - Give the flowctl HTTP client a per-read timeout, so a stalled stream fails as a retryable Transport error instead of trusting heartbeats nothing enforced. - Document that the Synced barrier also awaits trigger delivery. - Move flowctl's stub-server sync-now tests to crates/flowctl/tests/, leaving the pure retry-classification table in-crate. - Inline the single-caller reactor_front_door helper and drop redundant #[serde(default)] attributes.
dgreer-dev
force-pushed
the
daveg/3281-sync-now
branch
from
August 24, 2026 14:53
c71883f to
d5ca710
Compare
williamhbaker
left a comment
Member
There was a problem hiding this comment.
Looking pretty good with the updates. I had a couple of pending comments that I think are still relevant, which will hopefully show up below this...
… TLS client - taskShardZero now checks the caller's claims and locates shard zero's primary under its existing keyspace lock, returning a one-member Route. syncNow forwards to a peer primary via that Route, or calls Resolve (MayProxy: false) only to obtain the local Store once recovered. The gRPC-metadata proxy Header and its helpers go away. - flowctl no longer honors SSL_CERT_FILE with a per-command reqwest client; it keeps only the per-read timeout the shared client lacks.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
flowctl raw sync-now --task X(andPOST /v1/task-control/sync-now) forces animmediate commit of a V2 materialization's open transaction and returns once that
transaction is committed and queryable in the destination. Closes #3281.
The workflow we want to enable is a user wanting to run analytics on the freshest
data available, regardless of any sync schedule (or other transaction-holding
reason). Essentially we want to allow
flowctl raw sync-now ... && ./my-analyitcs.pyto be a safe and useful command. See below for specifics of the new command.
Design points worth your attention
it's queryable", whatever is holding the transaction (schedule, min-duration
floor, or just size). Worst case is a smaller transaction. "Nothing to do" is
never an error: an idle task Acks
IDLE, a capture or derivationNOT_APPLICABLE, both exit 0.IDLE/NOT_APPLICABLEresolves at thetarget transaction's
Tail::Done, so N concurrent callers park on one commit.zero with the caller's claims, proxies to the primary's peer via
mayProxy,and relays 1:1 to the co-located sidecar.
/v1/task-control/sync-nowexists as a secondway to force the sync. Calls the same code as flowctl, just an HTTP different path.
connector
syncSchedulecounts as unconfigured, so runtime-only pacing isexpressible on every connector image already shipped; and a model-level
syncSchedulewithoutenable-runtime-v2is now rejected rather thansilently inert.
Verification
Rust e2e tests ensure transaction control works as advertised. Go tests stand up
two consumer test members against a stub sidecar, covering both transports
including the peer-proxied path. flowctl's response handling is covered against a
stub server over a real channel.
Also verified on a local stack against a schedule-held V2 materialization
(transcripts available). Highlights: three parallel
flowctlpokes plus onecurl -Nstream against one held transaction all exited 0 reporting identicalcommitted stats (one
HELD_COLLAPSED, threeALREADY_CLOSINGwaiters);destination row count equalled the pre-poke source count exactly on every run.
Example Usage
Ack is immediate and carries the outcome plus a snapshot of the awaited
transaction. Done arrives when that transaction is committed and queryable in
the destination, with its committed stats.
durationMillisis thetransaction's total open age, not the commit duration. Progress heartbeats
(~15s) are consumed for liveness but not printed unless
--progress, since anhour of them is a lot of lines unasked.
Any completed outcome exits 0,
IDLEandNOT_APPLICABLEincluded - "nothingto do" is success. Exit is non-zero for
NOT_FOUND(not running here, or noton the V2 runtime) and for
--timeoutelapsing.Flags:
--progressprints the heartbeats;--no-waitprints the Ack and hangsup, which is harmless because the commit has already been forced;
--timeout 10mbounds the whole invocation, which otherwise waits indefinitely and retriesleader restarts.
The same call over the REST transport, which is what the dashboard button will
use:
Commits