GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

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

@blp

@blp blp commented Nov 4, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

In a multihost pipeline, the integral of each materialized view was gathered onto the one host that runs the view's output connectors. That host held the whole view and did all of its work. This PR spreads view integrals over all hosts, builds send_snapshot snapshots from the parts on every host, and adds distributed output connectors, which run a part on every host.

This PR builds on #7942, which records the output assignment in each checkpoint. Review and merge #7942 first. Until then, this PR's base is that branch, so the diff shows only this PR's commits.

Commits

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.

Behavior changes

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

Notes for review

  • Calls between hosts use HTTPS. Without client certificates, each host trusts the root CA of its own pipeline certificate chain, as the coordinator does.
  • SerBatchReaderHandle::deserialize_chunk is unsafe: it reads rkyv archives from another host without validation, as dbsp's exchange already does with its batches.
  • The leader protocol for distributed Delta Lake output: 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 all parts' files in one Delta commit. Output buffering is refused for distributed Delta Lake output for now.

Rollout

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.

Measurement

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.

Changes since the first push

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

Testing

  • Rust: new in-process tests on 2 and 3 hosts for snapshots, leases, restarts from checkpoints, distributed Null and Delta Lake output, commit failures, staggered connector starts, and the leader's report handling. Each new test fails with its fix reverted.
  • Python platform tests: 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.

Follow-ups

  • Output buffering for distributed Delta Lake output: the leader could choose the flush points for all parts.
  • 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.
  • A test that stops the commit leader's host in the middle of a commit, and checks recovery.
  • Placement in the coordinator could count the integrals of a pipeline that keeps them on one host.

The coordinator changes are in feldera/cloud#2177, which needs this PR.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@blp blp added documentation Improvements or additions to documentation performance connectors Issues related to the adapters/connectors crate User-facing For PRs that lead to Feldera-user visible changes rust Pull requests that update Rust code python Pull requests that update python code enterprise Issue related to Feldera Enterprise features. multihost Related to multihost or distributed pipelines labels Nov 4, 2026
@blp blp self-assigned this Nov 4, 2026
@blp
blp requested a review from ryzhyk November 4, 2026 22:06
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Nov 4, 2026
View reviewed changes
Comment thread crates/static-connectors/src/catalog.rs
for part in &readers.partitions {
part.serialize_chunks(256, &mut |chunk| {
n_chunks += 1;
let batch = unsafe { handle.deserialize_chunk(chunk) };

@mythical-fred-oss mythical-fred-oss Bot Nov 4, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for 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.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Nov 4, 2026
View reviewed changes
Comment thread crates/connectors/src/controller.rs

let leases = TOKIO.block_on(async {
let mut leases = Vec::new();
for host in (0..hosts.len()).filter(|&host| host != local_host) {

@mythical-fred-oss mythical-fred-oss Bot Nov 4, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for 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.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Nov 4, 2026
View reviewed changes
Comment thread crates/connectors/src/controller/snapshot_parts.rs
report.step
);
}
Ok(response) => {

@mythical-fred-oss mythical-fred-oss Bot Nov 4, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for 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.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Nov 4, 2026
View reviewed changes
Comment thread crates/connectors/src/controller/distribution.rs
/// 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(

@mythical-fred-oss mythical-fred-oss Bot Nov 4, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for 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.

  • Before this PR, 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.
  • A single-host Delta output with 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.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@mythical-fred-oss

mythical-fred-oss Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

What I ran

Check Result
cargo check --locked -p dbsp_connectors --tests ✅
cargo test -p dbsp_connectors --lib (output_leader, snapshot_parts, distribution, part_file) ✅ 15/15
cargo test -p static-connectors --test connector_suites -- multihost snapshot_lease, run 5 times ✅ 15/15 every run, no flakes
cargo test -p static-connectors --lib (spreads / round_trips) ✅
pre-commit run --files <changed> ✅
QA: a throwaway test where the leader always answers 503 ❌ report_output never returns (see inline comment)

Gates and higher-level issues

  1. Gate, high: unsafe without a SAFETY comment. The unsafe block in the test integral_round_trips_through_chunks has no // SAFETY: comment (see inline comment).
  2. Gate, high: SQL type coverage. The new rkyv chunk path between hosts and the distributed Delta output are tested only with BIGINT, BOOLEAN and VARCHAR, in Rust and in the Python tests. Please add an all-types schema such as DeltaTestStruct, for both the snapshot round trip and distributed Delta output.
  3. Pitfall, high: no feature flag, and a costly upgrade. Spreading view integrals is now unconditional for every multihost pipeline.
    • After the upgrade, a pipeline restarting from an older checkpoint needs a bootstrap, and by default that bootstrap waits for approval.
    • Pipelines whose views read non-materialized tables cannot restart from their checkpoint at all, so they lose their state.
    • Please gate the new layout behind a dev_tweaks flag, or keep the OneHost layout when restoring an older checkpoint, until the new layout has had some soak time.
  4. Pitfall, medium: the runtime needs a new coordinator first. Multihost send_snapshot, including HTTP egress with a snapshot, is now refused unless the coordinator sends host_urls. A runtime that ships before that coordinator change regresses those connectors, so the rollout order matters.
  5. Missing, medium: untested failure cases.
    • A follower part, or the leader part, disconnects at runtime while the other parts keep reporting. Today this hangs (see inline comment).
    • The leader's host starts the connector later than a follower; the staggered test only has a follower start first.
    • A distributed Delta output resumes after the leader crashes between the Delta commit and its answer to the parts.
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@blp
blp force-pushed the distributed-outputs branch from fe29ca7 to 02eb986 Compare November 5, 2026 16:12
ryzhyk
ryzhyk reviewed Nov 7, 2026
View reviewed changes

@ryzhyk ryzhyk left a comment

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for 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.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread crates/static-connectors/src/catalog.rs
///
/// | Materialized relation | Registered by | Integral on multihost | Integral shard key |
/// |---|---|---|---|
/// | Table | [register_materialized_input_zset] | Each worker's own input, not sharded | |

@ryzhyk ryzhyk Nov 7, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for 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.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread crates/dbsp/src/operator/output.rs Outdated
/// 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>(

@ryzhyk ryzhyk Nov 7, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for 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.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@blp blp Nov 12, 2026

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for 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.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@ryzhyk

ryzhyk commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

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.

Pipelines::new recomputes output_assignment from scratch on every start, and nothing records the previous assignment. So any change to the computed placement between two runs from the same checkpoint does the same damage.

What can move streams: anything that feeds assign_output_streams, which sorts streams by cost and breaks ties by name.
- Adding or removing a plain output connector on any stream, or turning distributed on or off for one. Distributed connectors don't count toward cost.
- Adding, removing or renaming a table or view, which changes the set of streams and their name order.
- A query change that makes the coordinator estimate a view as small (aggregated) instead of large, or the reverse.
- A coordinator release that changes the cost model or the tie-breaking. This is the upgrade case.
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ryzhyk

ryzhyk commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

Issue 2. This one I haven't tried to understand in detail, and just passing on as is

A distributed Delta part can silently drop its snapshot or a transaction's changes (6; verified)
- Why: parts commit only in steps_processed, which runs only for queue entries that carry a step count. But DeltaTableWriter::batch_start clears pending_actions on every batch.
- Effect: a snapshot queued during an open transaction carries no count, so the commit's own change batch wipes the snapshot's files, or the reverse. No error is raised, and snapshot_sent is still checkpointed, so the snapshot is never resent.
- Fix: keep the current batch's files and the current step's files in separate lists.

I asked for a step-by-step scenario:

Setup

- Pipeline: multihost, 2 hosts, dev_tweaks.view_integrals: all_hosts, which is the default for a new pipeline. So each host holds part of every view.
- View: materialized view v = SELECT * FROM t. It already holds 300 committed rows, about 150 on each host.
- Output sink: delta_table_output with mode: append, update_mode: cdc, distributed: true, send_snapshot: true, paused: true.
- Leader: host 1, the host assigned to v. Host 0's part is a follower.

Sequence

Each step shows host 0; host 1 does the same with its own share.

1. While sink is paused: each step, CircuitThread::push_output gives it an empty entry. The snapshot stays pending until the connector starts.
2. POST /start_transaction:
   - From now on, CircuitThread::processed_records returns None after every step, because a transaction is open.
   - update_snapshot doesn't run during a transaction, so the newest stored snapshot is still rows 0..300.
3. The client inserts rows 300..310 into t inside the transaction.
4. POST /output_endpoints/sink/start: the coordinator relays it to both hosts.
5. The next step, still inside the transaction:
   - paused_outputs no longer contains sink, and its snapshot is pending.
   - Because the endpoint is distributed, push_output passes the step's own count along, which is None.
   - enqueue_latest_snapshot takes host 0's part of the stored snapshot (about 150 rows) and calls mark_snapshot_delivered.
   - It queues {snapshot, 150 rows, no step count}.
6. Host 0's output thread:
   - push_batch_to_encoder calls DeltaTableWriter::batch_start, which finds nothing pending.
   - encode writes Parquet files A for the 150 rows, so pending_actions = [A].
   - batch_end returns early, because a part keeps its files for the leader.
   - The entry has no step count, so steps_processed isn't called. Files A sit in the table directory, uncommitted.
7. POST /commit_transaction: at the step where the commit completes, processed_records becomes Some(C). The transaction's change stream is published; host 0's share is, say, 5 of the 10 new rows.
8. push_output: the snapshot is no longer pending, so the normal path queues {changes, 5 rows, step C}.
9. Host 0's output thread:
   - batch_start runs pending_actions.clear(), and files A are dropped.
   - encode writes files D0, so pending_actions = [D0], and batch_end returns early.
   - steps_processed(C) takes [D0], and OutputPart::commit reports only D0 to the leader.
10. Host 1, the leader:
    - Its own snapshot files B were dropped the same way when its change batch arrived.
    - CommitLeader::gather(C) receives D0, adds the leader's own D1, checks the paths, and commits D0 and D1 in one Delta commit.

Result

- The table holds only rows 300..310. The 300 snapshot rows never reach it.
- Files A and B remain in the table directory as orphans; VACUUM eventually removes them.
- No error is raised, and the connector stays healthy.
- The loss is permanent. Step 5 set the delivered flag. The next checkpoint stores snapshot_sent, so after a restart ControllerInner::add_output_endpoint treats the snapshot as already sent. Only changing the connector's definition clears the flag.

When it doesn't happen

- Host with no new rows in the commit: if a host has no new rows of its own, its change entry carries no data. batch_start never runs, and steps_processed(C) commits its snapshot files. With few new rows, only some hosts lose their share, so the table ends up with part of the snapshot.
- No transaction open at the start: the snapshot carries the step's count and is committed at once. I haven't traced whether a commit that spans several steps opens the same window without an API transaction.
- Plain Delta connectors: not affected, because their batch_end commits each batch immediately.

Test to reproduce it: two in-process hosts with this connector. Ingest 300 rows, start a transaction, insert 10 more, start the connector, step, then commit. Expect 310 rows in the table; I expect it to hold about 10.

Possible second bug (unverified): a start that lands on the step where a host publishes its share of a multi-step commit may make the snapshot skip that share. The pending-snapshot branch drops the step's changes, but the stored snapshot predates the transaction. If real, this would also affect plain connectors on main.

Next: waiting for your command.
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ryzhyk

ryzhyk commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

Issue 3. A distributed input plus a distributed Delta output to a new table can never start

H3. A distributed input plus a distributed Delta output to a new table can never start (5–6; verified)
- Why: when a distributed Delta Lake or Iceberg input needs host 0 to choose a value first, the coordinator activates host 0 alone. Host 0 publishes its choices only after all its connectors, outputs included, have initialized.
- Effect: if the output's leader is on another host, host 0's part waits for a table that only the leader creates. It gives up after about 10 attempts, and every restart repeats this.
- Fix: publish choices once the inputs have initialized, or let the other parts open the table at their first commit.
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ryzhyk

ryzhyk commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

Issue 4. Multihost snapshots are encoded one piece at a time (3; verified)

H4. Multihost snapshots are encoded one piece at a time (3; verified)
- Why: encode_snapshot_parts calls encode once per local worker part and once per ~1 MiB chunk from another host.
- Effect on Delta Lake:
  - Each call writes its own files, so thousands of small files (inferred).
  - With update_mode: merge, each call is its own commit, so readers see the snapshot half-applied (traced).
  - An encode error clears all earlier parts' files while the snapshot carries on, so the commit holds only the later parts.
- Fix: encode the local parts as one reader (whole), merge remote chunks into larger readers, and make Delta's error path drop only the failing call's files.
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ryzhyk

ryzhyk commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

Medium importance issues (some looks pretty high to me):

Medium

- M5. Leases expire mid-snapshot (verified)
  - Why: all leases are taken up front, a lease's idle time resets only after a scan, hosts are read in order after the local parts, and the idle limit is 60 minutes.
  - Effect: if the earlier parts take over an hour to drain into the sink, the later hosts fail. This also happens under one_host, where those hosts' parts are empty, so it is a regression for large snapshots that used to be purely local.
- M6. A part's report larger than 2 MiB stalls the connector forever (verified)
  - Why: /coordination/output/commit uses actix's default 2 MiB JSON body limit, and PeerClient::report_output retries the resulting 413 error forever.
  - When: about 1–2k file entries per host in one step reaches the limit, for example the snapshot of a large view (size inferred).
- M7. send_snapshot on a distributed connector under one_host duplicates or loses rows (verified; both reviewers found it)
  - Why: only host H's part has a snapshot, taken at its own start step. The other parts send changes from their own start steps, and the coordinator starts each host independently.
  - Tables are not affected: a table's integral uses the same partition function (shard_batch) as the distributed changes. I discarded one reviewer's claim that tables are affected.
  - Fix: refuse this combination unless the layout is all_hosts, or have the leader pick one snapshot step for all parts.
- M8. A failed snapshot looks complete, or disappears
  - HTTP egress ends normally with partial data; the PR's own test asserts exactly this (verified).
  - A failed configured connector vanishes from /stats (traced).
  - A checkpoint taken mid-snapshot can store snapshot_sent for a snapshot that then fails, so it is never resent (traced).
- M9. A distributed output's pause state is never made consistent across hosts (traced)
  - The coordinator does this only for inputs.
  - After a restart some parts can stay paused and drop their share, while the merged status still says running.
- M10. Switching layouts under concurrent bootstrap serves empty views (runtime traced; I didn't check whether multihost pipelines use concurrent bootstrap)
  - Why: the switch marks no relation as affected.
  - Effect: ad hoc queries and new send_snapshot connectors see empty views until cutover.
- M11. HTTP/2 flow control shared between scans and leases (inferred)
  - When: over HTTPS without client certificates.
  - Effect: unread scan data can block a lease reply. The lease runs on the circuit thread and is retried every step, up to 30 s each time.
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ryzhyk

ryzhyk commented Nov 7, 2026

Copy link
Copy Markdown
Contributor
Low

- Version skew:
  - Old coordinator with a new runtime: host H's leader waits forever for reports that never come, because only the follower checks for host URLs (verified).
  - New coordinator with an old runtime: every host runs a full writer, and they race to create the table (traced).
- Background fetch task:
  - tokio::join!(fetch, forward) hangs when the reader goes away, which pins the other host's lease and data until restart.
  - An egress client that disconnects doesn't stop the fetch.
  - A closed channel counts as success, so a panic commits a partial snapshot.
- Leasing: leases are taken one host at a time on the circuit thread, up to 30 s × (hosts − 1) per step. A failed lease waits for the next step, which an idle pipeline may never take.
- Commit leader:
  - A part that skips a step deadlocks instead of getting an error.
  - CommitLeader::gather ignores close().
  - The 60 s "unhealthy" warning fires on large but healthy steps.
- Endpoint access:
  - The new host-to-host endpoints accept any caller the pipeline accepts, even on Solo.
  - The leader trusts report.host, so a forged report can complete a step.
  - Leases are unbounded.
  - Main's /coordination/* endpoints have the same exposure.
- Unused index names: the index names in the distributed set are never used. A distributed connector on view V gives V and all its indexes a distributed change stream.
- Clock skew: __feldera_ts now comes from several hosts' clocks, which can break the documented MERGE watermark when one host's clock lags.
- Chunk size: chunks have no upper bound for indexes with few distinct keys.
- Statistics: queued_batches drifts, and some merged counters mislead.
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@blp
blp force-pushed the distributed-outputs branch from 02eb986 to 4fb7ade Compare November 11, 2026 22:51
@blp blp mentioned this pull request Nov 11, 2026
[adapters] Record the output assignment in each checkpoint. #7942
Open
@blp
blp changed the base branch from main to record-output-assignment November 11, 2026 22:52
@blp

blp commented Nov 11, 2026

Copy link
Copy Markdown
Member Author

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.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@blp
blp marked this pull request as draft November 11, 2026 23:00
@blp
blp force-pushed the distributed-outputs branch 2 times, most recently from 3fdef33 to b0f6c7b Compare November 12, 2026 00:56
blp added 3 commits November 11, 2026 17:25
@blp
[adapters] Document which workers hold each output stream and integral.
33355a4
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]>
@blp
[adapters] Spread materialized view integrals with distributed outputs.
7ff9b2d
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]>
@blp
[adapters] Build multihost snapshots from the parts on every host.
e6a6e49
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]>
@blp
blp force-pushed the record-output-assignment branch from 8b16ed1 to 06b85c6 Compare November 12, 2026 04:50
@blp
blp force-pushed the distributed-outputs branch from b0f6c7b to 80bdb96 Compare November 12, 2026 04:50
@blp
[adapters] Add distributed output connectors.
cfd79b2
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]>
@blp
blp force-pushed the distributed-outputs branch from 80bdb96 to cfd79b2 Compare November 12, 2026 17:10
@blp

blp commented Nov 12, 2026

Copy link
Copy Markdown
Member Author

@ryzhyk Issue 3 (#7843 (comment)) is fixed in cfd79b2, with both of your suggested fixes:

  • A part of a distributed Delta Lake output that is not the leader no longer waits for the table at startup. It opens the table before it writes its first files, in a step, and the first step runs only after the coordinator has activated every host. The OutputEndpoint and InputEndpoint docs now state the rule: a connector must not wait for another host while it starts.
  • A host of a multihost pipeline now starts its input connectors, publishes their choices, and only then starts its output connectors, as a safeguard against a connector that breaks that rule.

New tests: multihost_input_choices_before_outputs_initialize activates host 0 alone, as the coordinator does, and follower_part_opens_the_table_after_initialization starts a follower before its table exists. Each fails with its fix reverted.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@blp
[adapters] Encode multihost snapshots in parts of about a file.
fd809c7
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]>
@blp

blp commented Nov 12, 2026

Copy link
Copy Markdown
Member Author

@ryzhyk Issue 4 (#7843 (comment)) is fixed in the new last commit, fd809c7, along the lines you suggested:

  • This host's share of the snapshot goes to the encoder as one part, the whole reader.
  • The other hosts' chunks are combined into parts of about the size the encoder wants (Encoder::preferred_encode_bytes; for Delta Lake, a 100 MiB file per write thread). A builder for each remote worker appends that worker's chunks in order and spills to storage past dbsp's usual threshold, so this host holds only about one part of other hosts' data at a time, and storage stays balanced across the hosts.
  • A Delta Lake part that fails to encode now drops only its own files.

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: multihost_delta_snapshot_writes_few_files (200,000 rows on 2 hosts: 2 files, against 7 before) and a_failed_encode_drops_only_its_own_files. Each fails with its fix reverted.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@blp blp mentioned this pull request Nov 12, 2026
Commit a multihost snapshot to a Delta Lake output in merge mode as one commit #7952
Open
@blp
[adapters] Keep snapshot leases alive, and take none under one_host.
382066d
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]>
@blp

blp commented Nov 12, 2026

Copy link
Copy Markdown
Member Author

@ryzhyk M5 (leases expire mid-snapshot, from #7843 (comment)) is fixed in the new last commit, 382066d:

  • While a snapshot is in progress, the connector's host touches each of its leases every minute through a new /coordination/snapshot/touch endpoint, so a lease that waits behind a slow sink keeps its idle time short. A host still drops the leases of a holder that crashed, after an hour.
  • Under one_host, a snapshot to a connector that is not distributed takes the single-host path again, with no leases or scans, so a large one_host snapshot is purely local, as before this PR.

Tests: multihost_one_host_snapshot_needs_no_other_host (the other host refuses all leases and scans), touched_leases_do_not_expire, and the_holder_keeps_its_leases_alive. Each fails with its fix reverted.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

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

Reviewers

@ryzhyk ryzhyk ryzhyk left review comments
@mythical-fred-oss mythical-fred-oss[bot] mythical-fred-oss[bot] left review comments

Uh oh!

There was an error while loading. Please reload this page.

Assignees

@blp blp

Labels

connectors Issues related to the adapters/connectors crate documentation Improvements or additions to documentation enterprise Issue related to Feldera Enterprise features. multihost Related to multihost or distributed pipelines performance python Pull requests that update python code rust Pull requests that update Rust code User-facing For PRs that lead to Feldera-user visible changes

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@blp @ryzhyk
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.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.