adapter: serve SUBSCRIBE from the persist shard without a dataflow - #38789
Draft
jubrad wants to merge 1 commit into
Draft
adapter: serve SUBSCRIBE from the persist shard without a dataflow#38789jubrad wants to merge 1 commit into
jubrad wants to merge 1 commit into
Conversation
A SUBSCRIBE on a table, source, or materialized view installs a dataflow on a cluster whose only job is to read that collection's persist shard and ship it back. Thousands of clients reconnecting after a network blip therefore create thousands of dataflows, each with its own persist reader, and the cluster spends its time installing them rather than serving anyone. Behind `enable_subscribe_persist_fast_path`, a subscribe whose source is a bare reference to a storage collection is served by reading the shard from environmentd instead. `StorageCollections::subscribe` returns the collection's snapshot at the subscribe's timestamp followed by every later update, hiding persist and txn-wal behind one seam. A `SharedTail` per collection owns a single listen reader and a single snapshot reader and fans decoded batches out to per-subscriber queues, because registering a persist reader is a compare-and-set on the shard's state and a storm of private readers serializes on it. Formatting moves out of `ActiveSubscribe` into a `SubscribeFormatter` so it can run where the rows are needed, which for this path is the session: `PersistTailStream` is the row stream the session polls during FETCH, and it reads, batches, and formats only when polled. The coordinator keeps an `ActiveSubscribe` for cancellation, dependency drops, and mz_subscriptions, and reaches the client through a control channel. The snapshot is read through persist's consolidating cursor and shipped in pieces as the client fetches, so a large snapshot is never held whole; an output that orders or groups a timestamp still buffers it. A client that stops fetching costs only its queue of decoded events, counted against the existing subscribe_max_buffered_bytes budget. Over budget the tail drops the queue rather than the client, which resumes below its delivered frontier from the retained window or from the shard, so the collection's RETAIN HISTORY is what bounds how far behind a client may fall. Nothing new holds back compaction. The client-visible protocol is unchanged: the same statements, rows, progress messages, cancellation, and bookkeeping. test/testdrive/ subscribe-persist-fast-path.td runs the same statements under both flag settings and expects identical output, github-6335.td pins the flag off where it asserts on dataflows, and persist_tail.rs carries unit tests for the batcher and the stream. The feature benchmark gains a first-fetch scenario for a large snapshot, and the parallel subscribe scenarios gain dataflow-pinned counterparts; those counterparts no longer inherit from the scenarios they mirror, which had made the originals non-leaf and dropped them from the run. Design: doc/developer/design/20260910_subscribe_persist_fast_path.md Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Motivation
A
SUBSCRIBEon a table, source, or materialized view installs a dataflow on a cluster whose only job is to read that collection's persist shard and ship it back. Thousands of browser or distributed clients reconnecting after a network blip therefore create thousands of dataflows, each with its own persist reader, and the cluster spends its time installing them rather than serving anyone. Materialized views already have a shard in persist with a stream pushing to it; a subscribe on one should be able to read that directly.This is a proof of concept, behind
enable_subscribe_persist_fast_path, which defaults off in production and on in the test configuration. It is meant to be measured and argued about, not shipped as is.What it does
A subscribe whose source is a bare reference to an existing storage collection is served by reading the shard from
environmentd. A query, a view, an index, or a log source builds the same dataflow as before.StorageCollections::subscribe(id, as_of, with_snapshot, max_buffered_bytes)returns the collection's snapshot at the subscribe's timestamp followed by every later update and strictly advancing progress. It hides persist and txn-wal behind one seam: for txn-wal backed tables the shard's physical upper only moves when a write is applied, so the stream combines a persist listen with the txns shard's remap entries the waytxns_progressdoes, driven from a task instead of a dataflow.SharedTailper collection owns one listen reader and one snapshot reader and fans decoded batches out to per-subscriber queues. Registering a persist reader is a compare-and-set on the shard's state, so a storm of private readers serializes on it; sharing also decodes each part once instead of once per subscriber. The tail retains a short window of past batches so a subscriber whoseas_oftrails the shared listen still gets a complete stream.ActiveSubscribeinto aSubscribeFormatterso it can run where the rows are needed. For this path that is the session:PersistTailStreamis the row stream the session polls duringFETCH, and it reads, batches, and formats only when polled. The coordinator's loop is off the data path.FETCH 10does not wait for it. Persist merges the shard's runs in key order, so each piece is final and can ship without the rest. An output that orders or groups a timestamp, meaningWITHIN TIMESTAMP ORDER BYand the envelopes, still buffers the whole timestamp.subscribe_max_buffered_bytesbudget. Over budget the tail drops the queue rather than the client, which resumes below its delivered frontier from the retained window or from the shard itself. The collection'sRETAIN HISTORYis therefore what bounds how far behind a client may fall. Nothing new holds back compaction.What is unchanged for clients
Same statements, same rows, same progress messages, same cancellation, dependency drops,
mz_subscriptions, and statement logging.SUBSCRIBE (SELECT ...)is untouched. A materialized view withoutRETAIN HISTORYbehaves as today, is more tolerant of short stalls, and produces the same error and SQLSTATE for long ones.Measurements
Local
bin/environmentd --optimizedon a laptop, onequickstartreplica, a materialized view over 10,000 rows of 64 bytes,Nclients connecting at once over eight load-generator processes. Median of two runs, no errors at any size.Total CPU falls 41% at 2048 clients and the cluster does nothing, but
environmentdrises, because decode and consolidate move there and are still paid per subscriber. Sharing that work between subscribers at the same timestamp is the obvious next step and is listed in the design doc.For one client on a materialized view of 1,000,000 rows, with one subscribe run first on each path so neither pays for the other's cold cache:
FETCH 10from a new cursorCOPYTests
test/testdrive/subscribe-persist-fast-path.tdruns tables, materialized views, envelopes,WITHIN TIMESTAMP ORDER BY,SNAPSHOT false,AS OF,UP TO, a dropped dependency, a stalled client that resumes, a snapshot spanning several chunks, and a snapshot needing consolidation, then repeats the statements with the flag off and expects identical output.test/testdrive/github-6335.tdpins the flag off, since it asserts on subscribe dataflows.src/adapter/src/coord/persist_tail.rscarries unit tests for the batcher's frontier, snapshot,UP TOand error semantics, for chunked snapshot delivery, and for the stream's detach, resume, and termination paths.SubscribeFirstFetchLargeSnapshot, and the parallel subscribe scenarios gain dataflow-pinned counterparts. Those counterparts no longer inherit from the scenarios they mirror, which had made the originals non-leaf and silently dropped them from the run.Reviewing
This is larger than one reviewable change and is a draft for that reason. It splits along these lines if that is easier: the persist and txn-wal visibility changes; the storage seam and
SharedTail; the formatter extraction out ofActiveSubscribe; the sequencing, flag, andPersistTailStream; chunked snapshot delivery; and the tests and benchmark scenarios.Release notes
None. The change is behind a feature flag that defaults off, and no user-visible behavior changes with it off.
🤖 Generated with Claude Code