Skip to content

fix(common): bound the channels Context::map opens, not just its concurrency - #449

Open
mpzFixApplier wants to merge 1 commit into
ethereum:devfrom
mpzFixApplier:fix/map-channel-bound
Open

fix(common): bound the channels Context::map opens, not just its concurrency#449
mpzFixApplier wants to merge 1 commit into
ethereum:devfrom
mpzFixApplier:fix/map-channel-bound

Conversation

@mpzFixApplier

@mpzFixApplier mpzFixApplier commented Aug 1, 2026

Copy link
Copy Markdown

Fixes the channel exhaustion described in #448

The problem

Context::map gives every item its own child context, and therefore its own multiplexer channel. buffered(concurrency_limit) bounds how many items run at once, but the channel IDs are still per item, so a map over N items opens N channels over its lifetime.

That is enough to exhaust a mux which tracks channels rather than in-flight work. A mux frees a channel when its stream is dropped, but that release is processed by the connection task and lags behind the rate at which the sliding window opens new ones, so the live-channel count keeps climbing. Against tlsn's mux (512 streams) an MPC-TLS session with a ~7 KB max_sent_data budget fails this way in roughly half of all runs, surfacing as context mux error whose hidden source is maximum number of streams reached.

The change

Distribute items round-robin over at most concurrency_limit lanes, each lane owning one child context and processing its items sequentially.

  • Channels ever opened: min(items.len(), concurrency_limit), independent of workload size.
  • That is still the concurrency bound too, so concurrency_limit keeps its meaning — and its default of 32 is unchanged here.
  • Lane assignment is index % lanes, derived from the item index alone, so both parties agree on the channel layout and per-channel message order with no extra coordination.
  • Results are still returned in input order.

Measurements

tlsn alpha.15 driving loopback MPC-TLS at a given max_sent_data, everything else equal:

buffered window round-robin lanes
MAX_SENT=7168 7/15 ok 12/12 ok
MAX_SENT=8192 mostly failing 12/12 ok
MAX_SENT=16384 mostly failing 10/10 ok

Tests

cargo test --workspace passes. The existing test_map_respects_concurrency_limit still holds unchanged (lanes bound concurrency as well), and a two-party recording test is added that maps far more items than the limit and pins the opened-channel count to the lane count while checking both sides stay in lockstep and results come back in input order.

Changelog

Added under ## [Unreleased] -> ### Fixed.

…ncurrency

`Context::map` gives every item its own child context, and therefore its own
multiplexer channel. `buffered(concurrency_limit)` bounds how many items run at
once, but the channel IDs are still per item, so a `map` over N items opens N
channels over its lifetime.

That is enough to exhaust a multiplexer which tracks channels rather than
in-flight work. A mux frees a channel when its stream is dropped, but that
release is processed by the connection task and lags behind the rate at which
the sliding window opens new ones, so the live-channel count keeps climbing on a
large enough workload. Against tlsn's mux (512 streams, `crates/tlsn/src/session.rs`)
an MPC-TLS session with a ~7KB `max_sent_data` budget fails this way in roughly
half of all runs, with `context mux error` — whose source, `maximum number of
streams reached`, is hidden by `ContextError`'s `Display`.

Distribute the items round-robin over at most `concurrency_limit` lanes instead,
each lane owning one child context and processing its items sequentially. The
number of channels a `map` ever opens becomes `min(items.len(), concurrency_limit)`,
independent of the workload, and that is also still the concurrency bound, so
`concurrency_limit` keeps its meaning and its default.

Both parties derive lanes from the item index alone (`index % lanes`), so they
agree on the channel layout and on the per-channel message order without any
extra coordination. Results are still returned in input order.

Measured with tlsn alpha.15 driving loopback MPC-TLS at a given `max_sent_data`,
everything else equal:

  buffered window, MAX_SENT=7168:  7/15 runs ok
  round-robin lanes, MAX_SENT=7168: 12/12 ok
                     MAX_SENT=8192: 12/12 ok
                     MAX_SENT=16384: 10/10 ok

`cargo test --workspace` passes; the existing `test_map_respects_concurrency_limit`
still holds (lanes bound concurrency too), and a two-party recording test pins the
channel count to the lane count.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant