GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — Spread view integrals over all hosts, build multihost snapshots, and add distributed output connectors by blp · Pull Request #7843 · feldera/feldera Conversation
|
| Commit | What it does |
|---|---|
| Document which workers hold each output stream and integral | Documents the layout before this PR, in the Catalog docs, so that the next commits show what changes |
| Spread materialized view integrals with distributed outputs | With an output connector that sets distributed: true, view integrals are sharded over all workers of all hosts. This adds a restriction: send_snapshot on such a view is refused. |
| Build multihost snapshots from the parts on every host | Lifts that restriction. The host that runs a send_snapshot connector leases the relation's snapshot at one step on each other host and reads each host's part as dbsp batches through /coordination/adhoc/scan. Then a new multihost pipeline spreads its view integrals. A checkpoint records where its view integrals are, and a restart keeps that layout. Adds dev_tweaks.view_integrals. |
| Add distributed output connectors | distributed: true on the Null and Delta Lake (mode: append, update_mode: cdc) output connectors. A distributed Delta Lake output commits each transaction once, through a leader. On other output connectors, distributed: true still has no effect, and the pipeline logs a warning. |
| Encode multihost snapshots in parts of about a file | A connector that is not distributed gets this host's share as one part, and the other hosts' chunks combined, through builders that spill to storage, into parts of about Encoder::preferred_encode_bytes (for Delta Lake, a target-size file per write thread). A Delta Lake part that fails to encode drops only its own files. |
| Keep snapshot leases alive, and take none under one_host | The connector's host touches its leases on the other hosts every minute, through the new /coordination/snapshot/touch, so that a lease that waits its turn behind a slow sink does not expire. Under one_host, a snapshot takes the single-host path, with no leases. |
Each commit builds and passes the Rust suites on its own.
Multihost: a new pipeline spreads the integrals of its materialized views over all hosts. A pipeline that restarts from a checkpoint keeps the checkpoint's layout, so the upgrade moves no state. The changelog has an entry.
dev_tweaks.view_integrals overrides the layout:
| Value | Layout |
|---|---|
auto (default) |
The checkpoint's layout. All hosts for a new pipeline. One host for a checkpoint from before this PR. |
one_host |
One host, as before this PR. For use if the new layout has a bug. |
all_hosts |
All hosts. Moves an existing pipeline to the new layout. |
A move in either direction rebuilds the integrals as a bootstrap. With the default bootstrap policy, await_approval, it waits for approval, and the pipeline diff says why.
Multihost send_snapshot connectors, including HTTP egress, get their snapshot from all hosts.
SerBatchReaderHandle::deserialize_chunk is unsafe: it reads rkyv archives from another host without validation, as dbsp's exchange already does with its batches.The runtime needs the coordinator from feldera/cloud#2177, which sends each host the URLs of the other hosts. Without them, a multihost pipeline that spreads its view integrals refuses send_snapshot on a connector that is not distributed. Release both together.
A 4-host QA workload (8 workers per host) joins a 1.07B-row CDC table with a 64M-row table into a materialized view, then computes a 90-day rolling aggregate. It loads 40 days of input, one transaction per day. Two runs on this PR's head differ only in dev_tweaks.view_integrals:
one_host |
all_hosts (new default) |
View not materialized | |
|---|---|---|---|
| Pipeline uptime | 4789 s | 4031 s | 3701 s |
| Seconds per input day, days 31-40 | 160 | 104 | 92 |
| Storage per host at the end, hosts 0/1/2/3 | 96 / 220 / 98 / 98 GB | 111 / 110 / 110 / 111 GB | |
| Fullest host against the mean, at the end | 1.72 | 1.005 | |
| Peak storage, all hosts | 515 GB | 449 GB | 347 GB |
The spread layout removes about 70% of the cost of the materialized view. The last column is an earlier run on an older runtime, for reference. The coordinator in these runs predates feldera/cloud#2177, so placement still charged the integral to one host.
| Finding | Change |
|---|---|
| A snapshot fetch could hang on a host that does not answer | Connect timeout of 10 s and lease timeout of 30 s on calls between hosts |
| A lease could ask for a step that the other host had not stored yet | The lease handler waits up to 10 s for that step. Found as a rare test failure under load. |
| The commit leader could wait on a part forever without a sign | A wait of more than a minute makes the connector unhealthy and logs a warning |
| A stopped leader could leave the parts waiting | Disconnecting the leader's connector closes it, so the parts get an error |
distributed: true on an unsupported output was refused |
It runs on one host with a warning, as before this PR, so existing configs still work |
| An upgrade moved every view integral | The layout follows the checkpoint, with the view_integrals override above |
| Chunk round trip covered few types | A test sends every SQL type through chunks |
| A distributed Delta Lake part lost the files of a snapshot that ended inside a transaction | batch_start keeps a part's files until steps_processed hands them to the leader. New test: multihost_distributed_delta_snapshot_in_transaction. |
| A restart could move a view to another host | Fixed separately in #7942 and feldera/cloud#2210, which this PR now builds on |
Turning distributed on kept the view from being sent again, through a second output that followed the first one's checkpoint |
Removed, as suggested in review: it was too complex for a rare case. Turning distributed on now starts a bootstrap, which sends the whole view to the connector again. |
send_snapshot on a distributed connector under one_host duplicated or lost rows (review M7) |
Refused at startup, with an error that says to set dev_tweaks.view_integrals: all_hosts |
| A part's report larger than 2 MiB stalled the connector forever (review M6) | The leader takes reports up to 256 MiB, and a part no longer retries a report that the leader refuses |
| A distributed input plus a distributed Delta Lake output to a new table could never start (review issue 3) | A part that is not the leader opens the table at its first write, not at startup, and a multihost host publishes its input choices before its output connectors start. The connector traits now state the rule: a connector must not wait for another host while it starts. |
| A multihost snapshot reached the encoder in one piece per worker and per 1 MiB chunk, which made many small Delta Lake files, a commit per piece in merge mode, and an encode error that dropped earlier pieces' files (review issue 4) | The new last commit. In merge mode, a large snapshot is still several commits, which the Delta Lake docs now say. |
A snapshot lease could expire while it waited behind earlier parts of the same snapshot, which also failed large one_host snapshots that need no other host (review M5) |
The new last commit |
test_multihost_snapshot.py and test_delta_output_distributed.py, which CI's python-multihost suite runs on 2 hosts. On a local Kubernetes cluster they found two bugs that the in-process tests could not: the certificate trust above, and a deadlock when hosts start a connector at different steps.Catalog::gather_output_to_host could return a struct instead of a 4-tuple.serialize_chunks could size each chunk's serializer from the previous chunk.The coordinator changes are in feldera/cloud#2177, which needs this PR.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| for part in &readers.partitions { | ||
| part.serialize_chunks(256, &mut |chunk| { | ||
| n_chunks += 1; | ||
| let batch = unsafe { handle.deserialize_chunk(chunk) }; |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
gate (high): missing SAFETY comment. Every unsafe block needs a // SAFETY: comment, test code included. Something like: // SAFETY: chunkwas just written byserialize_chunks of this same relation's integral.
missing (high): SQL type coverage. This is a new wire encoding (rkyv chunks sent between hosts), but the round trip only covers BIGINT, BOOLEAN and VARCHAR. Please run the same round trip over DeltaTestStruct or a schema that has every type: decimals, timestamps, intervals, binary, arrays, maps, structs, variant, nullables.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
|
||
| let leases = TOKIO.block_on(async { | ||
| let mut leases = Vec::new(); | ||
| for host in (0..hosts.len()).filter(|&host| host != local_host) { |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
pitfall (medium): the circuit thread blocks on HTTP with no timeout. These lease calls run on the circuit thread, and PeerClient's reqwest client sets no connect_timeout and no timeout. If one peer's HTTP server accepts the connection but never answers, every step on this host stalls with no log. Please set a timeout on the client, or at least on lease. A slow lease then just means "try again at the next step", as it does today on errors.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| report.step | ||
| ); | ||
| } | ||
| Ok(response) => { |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
bug (medium): a part can retry forever, and the connector status never shows it. Every non-409 reply is retried with no limit. Here is one way it happens: the leader's endpoint is disconnected, so disconnect_output removes it from output_leaders. Every later report then gets 503 "no leader on this host yet". The follower's output thread spins on that for good, its queue fills, and backpressure stalls the pipeline. The only sign is a warn! at power-of-two attempts. The reverse case has the same shape: CommitLeader::gather waits forever for a follower part that died.
I checked this with a throwaway test: a server that always answers 503, and report_output was still retrying after 15 s.
Suggestion: once the leader has existed, treat "no leader" as fatal. Or bound the retries, and also put the stall into the connector's health/status, not only the log.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| /// Returns an error if output connector `config` sets `distributed` but its | ||
| /// transport does not support it (see | ||
| /// `TransportConfig::supports_output_distribution`). | ||
| pub fn validate_output_distribution( |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
breaking (medium): this validation now applies to every pipeline, including single-host ones.
distributed on an output was documented as "Valid for input connectors only" and was silently ignored. A pipeline that sets it on a Kafka, file, or other output now fails to start after the upgrade.distributed: true plus enable_output_buffer is also refused. That contradicts the docs, which say "In a single-host pipeline, this setting has no effect."Please either skip these checks when the pipeline is not multihost, or list this change in the changelog as a behaviour change.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
What I ran
Gates and higher-level issues
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
fe29ca7 to
02eb986
Compare
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Still working on it. See a couple of suggestions I have so far inline.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| /// | ||
| /// | Materialized relation | Registered by | Integral on multihost | Integral shard key | | ||
| /// |---|---|---|---| | ||
| /// | Table | [register_materialized_input_zset] | Each worker's own input, not sharded | | |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
More precisely it assumes the input is already sharded, which is the case for input streams produced by add_input_zset.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| /// rebuild it, and give the relation a second time to readers that | ||
| /// already have it. | ||
| #[track_caller] | ||
| pub fn output_accumulated_stream_following_with_gid<B>( |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I think this is an overkill. This new mechanism makes sure that setting the distributed attribute doesn't lead to bootstrapping. I don't think it's an important case to cover and it's not worth the added complexity. I suggest we remove this mechanism and accept that (un)setting the distributed attribute can trigger bootstrapping.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Done: removed in 3fdef33. Turning distributed on now starts a bootstrap, which sends the whole view to the connector again.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
By examining individual features in this PR, claude found several serious issue. I will summarize them here, once comment per issue. Issue1: Any change that changes cost estimation causes bootstrapping. In addition, upgrading to the new runtime with this PR will cause bootstrapping, even though docs say it shouldn't; however I don't think that's an issue. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Issue 2. This one I haven't tried to understand in detail, and just passing on as is I asked for a step-by-step scenario: |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Issue 3. A distributed input plus a distributed Delta output to a new table can never start |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Issue 4. Multihost snapshots are encoded one piece at a time (3; verified) |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Medium importance issues (some looks pretty high to me): |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
02eb986 to
4fb7ade
Compare
|
This PR has changed since review; it will change again before it is ready for re-review. @ryzhyk, the new bugfix PR #7942 that this is now based on is, however, ready for review now. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
3fdef33 to
b0f6c7b
Compare
Multihost pipelines gather output delta streams, and the integrals of materialized views, to the host that the coordinator assigns to each stream, but tables spread their integrals over all workers. Add a table to the documentation of Catalog that shows each case and explains why, so that later changes to output placement can update it. Signed-off-by: Ben Pfaff <[email protected]>
In a multihost pipeline, the integral of each materialized view was gathered onto the host that the coordinator assigned to its stream. That host held the whole view, and its workers did all of the view's work. With this change, if any output connector of the pipeline sets `distributed: true`, each materialized view's integral is sharded over all of the workers of all of the hosts instead. The new integral has a new persistent id, so a checkpoint from before the change restores nothing into it and dbsp rebuilds it. This adds a restriction: a `send_snapshot` connector, including HTTP egress, needs the whole view on one host, so the controller refuses it on a materialized view whose integral is spread. The next commit lifts the restriction, by building snapshots from the parts on every host. Signed-off-by: Ben Pfaff <[email protected]>
A connector with `send_snapshot`, including HTTP egress, now gets its snapshot from all of the hosts: the host that runs the connector leases the relation's snapshot at one step on each other host, reads each host's part through `/coordination/adhoc/scan` in a new format that carries dbsp batches, and streams the parts into the connector's queue in chunks. A lease for a step that a host has not stored yet waits briefly for it, because another host can finish the step first. This lifts the restriction on `send_snapshot` that the previous commit added for spread integrals. With that, a new multihost pipeline spreads the integrals of its materialized views over all hosts. A checkpoint now records where the view integrals are, and a pipeline that restarts from a checkpoint keeps that layout, so that an upgrade does not move its state. A checkpoint from before this change keeps them on one host. The new `dev_tweaks.view_integrals` setting overrides the choice: `one_host` keeps the old layout, in case the new one has a bug, and `all_hosts` moves a pipeline to the new one. A move in either direction rebuilds the integrals as a bootstrap, which by default waits for approval, and the pipeline diff says why. The hosts call each other over HTTPS. When the pipeline does not require client certificates, each host trusts the root CA of its own pipeline certificate chain, as the coordinator does. With the integrals on all hosts, a view's state does not depend on its host, so a change of output assignment moves no state. The in-process multihost test harness now runs 3 hosts and passes each host the other hosts' URLs. The Python platform tests check `/egress` and Delta Lake snapshots on every host count. Signed-off-by: Ben Pfaff <[email protected]>
8b16ed1 to
06b85c6
Compare
b0f6c7b to
80bdb96
Compare
An output connector with `distributed: true` now runs on every host of a multihost pipeline, and each host's part writes the output of that host's workers. The null connector and the Delta Lake connector, with `mode: append` and `update_mode: cdc`, support it. On other transports, it has no effect, as before: the connector runs on one host, and the pipeline logs a warning. A distributed Delta Lake output still commits each transaction once. Each part writes its own data files and reports them, for each committed step, to the part on the stream's assigned host, which commits every part's files in one Delta commit and then answers the parts. Only that part creates the table. It commits only files inside the table. The other parts wait for the table to exist. A part's snapshot goes with the next commit, because the hosts can start the connector at different steps. Output buffering is refused for now. So is `send_snapshot` while the pipeline keeps each view's integral on one host: each part sends its own host's part of the snapshot, and then its host's changes, so with the integral on one host, the rows that change between the steps at which the hosts start the connector would arrive twice or never. A part's report lists every file that it wrote in a step, so the leader takes reports of up to 256 MiB, instead of actix's default 2 MiB limit for JSON. A part retries a report while the leader is busy, but not one that the leader refuses, for example as too large, because the leader would refuse it again. A part of a distributed Delta Lake output that is not the leader waits, as it initializes, for the leader to create the table. When an input connector needs host 0's choice, the coordinator activates host 0 alone and activates the other hosts only after host 0 publishes its choices. Host 0 published them only after all of its connectors initialized, so with the leader on another host, host 0's part waited for a table that nothing could create, and the pipeline could never start. Two changes fix this: - A part that is not the leader no longer waits for the table while it initializes. It opens the table before it writes its first files, in a step, and the pipeline runs its first step only after the coordinator has activated every host. The connector traits now state this rule: a connector must not wait for another host while it initializes. - A host of a multihost pipeline initializes its input connectors, publishes their choices, and only then initializes its output connectors, so that an output connector that breaks the rule cannot hold back the other hosts. The test harness can activate one host at a time, as the coordinator does. The connector's status combines the statuses of its parts. The distributed output of a relation is an output operator of its own, so a restart that turns `distributed` on for a connector bootstraps that operator, which sends the whole view to the connector again. The docs describe distributed output connectors, and the Python platform tests check a distributed Delta Lake output's deltas, snapshot, and resume. Signed-off-by: Ben Pfaff <[email protected]>
80bdb96 to
cfd79b2
Compare
|
@ryzhyk Issue 3 (#7843 (comment)) is fixed in cfd79b2, with both of your suggested fixes:
New tests: |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
A connector that is not distributed, in a multihost pipeline that spreads view integrals over all hosts, gets its snapshot from every host. The encoder got it in many small pieces: one for each local worker, and one for each chunk of about 1 MiB from each other host. The Delta Lake encoder writes at least one file for each piece and each write thread, so a large snapshot made thousands of small files, and in merge mode each piece was a commit of its own. Also, when one piece failed to encode, the Delta Lake encoder dropped the files of every earlier piece too, so the commit held only the later pieces, without a sign. Now: - This host's share is one piece, the whole reader, as a single-host snapshot is. - The other hosts' chunks go to the output thread as they are. There, a builder for each worker appends that worker's chunks in order. The builder spills to storage past dbsp's usual threshold, so it uses little memory. The output thread encodes the built batches together once they hold about `Encoder::preferred_encode_bytes`: for Delta Lake, a file of the target size for each write thread. So this host holds at most about one piece of other hosts' data at a time, and the files are about the size that the encoder aims for. Without storage, the builders would hold everything in memory, so each chunk is still a piece of its own. - A Delta Lake piece that fails to encode drops only its own files. In merge mode, each piece is still a commit of its own, so a reader can see a large snapshot partly applied. The Delta Lake docs say so. Signed-off-by: Ben Pfaff <[email protected]>
|
@ryzhyk Issue 4 (#7843 (comment)) is fixed in the new last commit, fd809c7, along the lines you suggested:
In merge mode, a large snapshot is still several commits, so a reader can see it partly applied. The Delta Lake docs say so. One commit per snapshot would need the whole snapshot on one host's memory or storage, or merging the parts' deletion vectors within one commit. Tests: |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
For a multihost snapshot, the connector's host leases the snapshot's step on every other host at the start, and then reads the hosts' parts one after another, as fast as the output connector takes them. A host drops a lease that goes unused for an hour, to collect the leases of a holder that crashed, and a lease that waits its turn is unused. So a snapshot whose earlier parts took more than an hour to drain into a slow sink failed when it reached a host whose lease was gone. The holder now touches each of its leases every minute, through the new `/coordination/snapshot/touch` endpoint, until the snapshot ends. A host still drops the leases of a holder that crashed. With the integrals of the views on one host, the connector's host has the whole snapshot, but it still leased and scanned the other hosts, whose parts were empty. That made a large snapshot fail as above, where before the integrals could spread it was purely local. Such a snapshot now takes the single-host path, with no leases or scans, and `send_snapshot` no longer needs the host URLs from the coordinator then. Signed-off-by: Ben Pfaff <[email protected]>
|
@ryzhyk M5 (leases expire mid-snapshot, from #7843 (comment)) is fixed in the new last commit, 382066d:
Tests: |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.