Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions doc/bin/relay/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,21 +109,19 @@ read, whether or not their message is complete.

## \[session] and \[peer]

Request caps apply separately to each session on every transport. The relay
Session limits apply separately to each session on every transport. The relay
uses `[peer]` for an admitted grant marked `peer` or `upstream`, including
verified LAN membership, and for links it dials through the cluster. A client
certificate, route hop, or cluster-looking URL alone does not select peer caps.
certificate, route hop, or cluster-looking URL alone does not select peer limits.

```toml
[session]
announces = 1000 # Simultaneous broadcasts or namespaces.
subscriptions = 1000 # Simultaneously held subscriptions.
fetches = 10000 # Independent simultaneous FETCH requests.
requests = 10000 # Other simultaneous moq-transport requests.

[peer]
announces = 100000
subscriptions = 10000
fetches = 10000
requests = 20000

[peer.quic]
max_streams = 100000 # Concurrent QUIC streams per direction.
Expand All @@ -139,15 +137,17 @@ not throttled by `[quic]` limits sized for one viewer, for example
`--peer-quic-max-streams=200000`. Peers over WebSocket, TCP, or iroh keep
their transport's own limits.

These are the defaults. For example, `--peer-subscriptions=50000` raises the
subscription cap on cluster links. `--session-fetches` and `--peer-fetches`
set the independent FETCH caps; concurrent callers of the same group each
hold a slot. Size peer caps for the combined traffic they carry, and use
session peak counters to check announcement and subscription headroom.
Exceeding a cap closes the session with `TOO_MANY_REQUESTS`; reconnecting with
the same load will exceed it again. Zero admits no entries of that kind, not unlimited
entries. On moq-transport drafts 14 to 16 these caps also size the request
window, so very small values can prevent other requests from progressing.
These are the defaults. Each subscription and FETCH holds a stream on moq-lite
and on moq-transport draft 17 and later, so `[quic]` or `[peer.quic]`
`max_streams` bounds them there, and a peer past it waits rather than fails.
On drafts 14 to 16, `announces` and `requests` together size the request
window instead, which every request counts against, so very small values can
prevent other requests from progressing. On later drafts `requests` bounds the
PUBLISHes a peer's track listings hold; the rest are skipped. Size peer limits
for the combined traffic they carry, and use session peak counters to check
announcement and subscription headroom. Exceeding `announces` closes the session
with `TOO_MANY_REQUESTS`; reconnecting with the same load will exceed it again.
Zero admits no entries of that kind, not unlimited entries.

## \[runtime]

Expand Down
7 changes: 4 additions & 3 deletions doc/concept/stats.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,9 +368,10 @@ reads it as zero when absent. A request moved to another tier stays counted wher
it was admitted; later traffic follows the new tier.

`announces_peak` and `subscriptions_peak` are the most broadcasts announced to
the relay, and subscriptions held on it, by any one session. Compare them with
the relay's [client or peer session limits](/bin/relay/config#session-and-peer):
a session that goes past one is closed with `TOO_MANY_REQUESTS`. On
the relay, and subscriptions held on it, by any one session. Compare announces
with the relay's [client or peer session limits](/bin/relay/config#session-and-peer),
past which a session is closed with `TOO_MANY_REQUESTS`, and subscriptions with
its QUIC `max_streams`, which bounds them. On
`sessions.json` a peak never goes down while the label has a session; on the
totals and presence tracks it never goes down while the track lives. Summing
nodes or labels takes the largest.
Expand Down
2 changes: 1 addition & 1 deletion doc/lib/rs/moq-net.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ What you use it for, beyond what the concept page already describes:
- **Publish only while someone is watching.** `demand()` on a track, group, or broadcast says whether a subscriber is attached, which is how capture and transcode skip work nobody asked for. Holding a broadcast or track consumer is not demand: a track consumer requests nothing until it queries, subscribes, or fetches, and those count until dropped. A shared fetch stays up until its last reader leaves.
- **Use the model without a runtime.** Disable default features on `moq-net` to use its `poll_*` APIs and origin driver with caller-supplied time. The default `async` feature adds async helpers, `time::run`, and both wire sessions.
- **You drive the session, or `moq-tokio` does.** `connect` and `accept` return a session plus a driver that never reads the clock itself. `moq_net::time::run` polls it on tokio or in the browser. `moq-tokio` and `moq-wasm` hide that. A custom transport implements `moq_net::transport::poll`, and `moq_net::transport::memory::pair` links two sessions in one process with no sockets, for tests or in-process links.
- **A session caps what its peer can hold.** `session::Limits` (via `Client::with_limits` and `Server::with_limits`) bounds announces, subscriptions, and FETCH requests independently per session; the defaults suit a relay mesh, so lower them for untrusted peers. Past a cap, the session closes with `TOO_MANY_REQUESTS`. On moq-transport drafts 14 to 16 they also size the request-ID window, which every request counts against, so very low limits can starve it. Peer-declared lengths are capped before they are buffered.
- **A session bounds what its peer can hold.** Each moq-lite request, and each moq-transport request from draft 17 on, holds a stream, so the transport's stream credit bounds subscriptions and FETCHes. `session::Limits` (via `Client::with_limits` and `Server::with_limits`) bounds the rest per session: `announces`, and `requests`, which with `announces` sizes the request-ID window on moq-transport drafts 14 to 16. The defaults suit a relay mesh, so lower them for untrusted peers. Past a limit, the session closes with `TOO_MANY_REQUESTS`; very low limits can starve the window, which every request counts against. Peer-declared lengths are capped before they are buffered.
- **Authorization is in band.** On moq-lite 07 (`moq-lite-07-wip`, opt-in) and on moq-transport draft-17+ with the [MoQ Auth extension](/draft/moq-auth), `session.auth()` presents tokens without reconnecting and watches the grant the peer sent back. A server verifies tokens itself by taking `handshake.auth().requests()` before `ok()`. `authorize(&grant)` narrows or widens a live session on any version: what falls outside resets with `Unauthorized` and the session stays up. A client that publishes outside its grant closes the session, naming the path.
- **A graceful close waits.** `session.close().await` withdraws announcements and drains for up to one second. Application delivery requires explicit `moq-lite-07-wip`; older Lite versions (including default lite-06) and IETF wait only for transport ACKs. `abort` ends immediately. IETF drafts 14 through 16 send withdrawals without waiting.

Expand Down
4 changes: 2 additions & 2 deletions js/net/src/ietf/setup_options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ const KNOWN_SETUP_OPTIONS: readonly bigint[] = Object.values(SetupOption);
/**
* Requests a draft-14 to -16 peer may hold open at once.
*
* Sized like moq-net's window: twice its default 100,000 announce and 10,000
* subscription and FETCH caps. Draft-17 dropped MAX_REQUEST_ID. Grants are batched per
* Sized like moq-net's window: twice its default 100,000 announces and 20,000
* other requests. Draft-17 dropped MAX_REQUEST_ID. Grants are batched per
* half window, so only about half of it is guaranteed to a peer holding requests open.
*/
export const REQUEST_WINDOW = 240_000n;
Expand Down
8 changes: 6 additions & 2 deletions js/net/src/util/limits.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Session, SessionCode } from "../error.ts";

/** How many live requests a peer may hold on one session, as moq-net's `session::Limits`. @internal */
/**
* How many live requests a peer may hold on one session. Unlike moq-net's `session::Limits`, it
* still caps subscriptions and FETCHes: `@moq/qmux` returns stream credit on accept, not on close,
* so credit alone does not bound them here. @internal
*/
export interface Limits {
/** Broadcasts (moq-lite) or namespaces (moq-transport) the peer may have announced to us. */
announces: number;
Expand All @@ -10,7 +14,7 @@ export interface Limits {
fetches: number;
}

/** moq-net's default limits. @internal */
/** The default limits, matching moq-net's announces. @internal */
export const LIMITS: Readonly<Limits> = Object.freeze({ announces: 100_000, subscriptions: 10_000, fetches: 10_000 });

/** A session's count of live peer requests, which closes the session past its cap. @internal */
Expand Down
1 change: 0 additions & 1 deletion quest/m1/api-audit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ Rejected in planning:
- [Binding connection state](/quest/m1/api-audit/binding-state.md) - one `state()` snapshot and `state_changed()` replace `status`, `connects`, and both `failure` polls
- [moq-net exports with a consumer](/quest/m1/api-audit/net-exports.md) - Rust and JS model exports without a consumer go private, wrong ones are deleted, and JS names mirror Rust
- [One clean close](/quest/m1/api-audit/clean-close.md) - a clean session close has one spelling and failures are recorded inside moq-net
- [Session limits only where needed](/quest/m1/api-audit/session-limits.md) - `session::Limits` keeps only bounds that nothing else enforces, and its documented example compiles
- [Ranges refuse what the wire drops](/quest/m1/api-audit/ranges-wire.md) - relative and time anchors fail loud where a wire cannot carry them, `floor()` honors the subscription, and bindings take an optional max delay
- [Derived origins borrow](/quest/m1/api-audit/derived-origin-close.md) - closing a JS `scope()` or `withPreference()` handle releases only that handle
- [Wrapper config names](/quest/m1/api-audit/wrapper-config.md) - Python, Go, and Dart client options use the FFI's names and nesting
Expand Down
30 changes: 0 additions & 30 deletions quest/m1/api-audit/session-limits.md

This file was deleted.

6 changes: 1 addition & 5 deletions quest/m1/js-qmux-credit.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,8 @@ mirror Rust.
`maybeReplenishStreamCredit` in `js/qmux/src/session.ts` fires once a stream
is accepted and its receive half is terminal. A bidirectional subscription
whose receive half ends while its send half stays open frees credit early.
Because of this, [Session limits](/quest/m1/api-audit/session-limits.md)
Because of this, [moq-dev/moq#6467](https://github.com/moq-dev/moq/pull/6467)
kept the `@moq/net` caps. Fix the credit, then remove those caps, with a test
that a half-closed stream still holds its credit.

Public API: `@moq/net` loses its subscription and FETCH caps. Wire: none.

## Related

- [Session limits](/quest/m1/api-audit/session-limits.md) - kept the JS caps because of this
6 changes: 1 addition & 5 deletions quest/m1/listing-credit.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ In `rs/moq-net/src/ietf/publisher/tracks.rs`, every FORWARD=1 or unfittable
track goes into `request.skips` as the listing is enumerated, and
`poll_write_skips` paces only the writes. A peer that stops reading makes the
session hold state proportional to the whole catalog, multiplied across
peers. After [Session limits](/quest/m1/api-audit/session-limits.md), the
peers. After [moq-dev/moq#6467](https://github.com/moq-dev/moq/pull/6467), the
listing PUBLISHes are bounded only by `Limits::requests`, a runtime cap.

Decided 2026-10-23 (maintainer): replace the cap with flow control. Couple
Expand All @@ -22,7 +22,3 @@ it, and drop the listing's use of `Limits::requests`. Test with a peer that
withholds credit and reads nothing.

Public API: none expected. Wire: none.

## Required

- [Session limits](/quest/m1/api-audit/session-limits.md) - rewrites the same listing code
16 changes: 9 additions & 7 deletions quest/m2/ietf-join-after-finish.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,18 @@ alive, so retaining it costs memory a peer controls. Candidate bounds:
stream; keep the entry until the subscriber closes its side, with our
subscriber holding it open until its joining FETCH resolves. Drafts 14 to 16
share the control stream and have no such signal.
- **The subscription limit.** A finished entry stays charged to the peer's
`session::Limits::subscriptions`, released when its FETCH takes it, the peer
cancels, or a new subscription needs the room (oldest finished entry first).
This works on every draft, and a peer that never sends the FETCH costs no
more than an open subscription.
- **The request window.** On drafts 14 to 16 a finished entry keeps its
SUBSCRIBE's request permit, so it counts against the window that
`session::Limits` sizes, released when its FETCH takes it, the peer
cancels, or a new request needs the room (oldest finished entry first). A
peer that never sends the FETCH costs no more than an open subscription.
There is no per-session subscription limit to charge instead: stream credit
bounds subscriptions on later drafts.
- **The session alone.** Grows with every subscription a long-lived relay
session ever makes, so probably not bounded enough.

Recommendation: the subscription limit, plus the request close where the
draft has one. Keep only entries that can be answered (`Joined::Group`); an
Recommendation: the request close where the draft has one, and the request
window on drafts 14 to 16. Keep only entries that can be answered (`Joined::Group`); an
unsubscribed or refused subscription still drops its entry at once. The
entry must not outlive what authorized the subscription: when its request
token's grant expires or is revoked, or the session grant no longer covers
Expand Down
7 changes: 5 additions & 2 deletions rs/moq-net/MIGRATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,11 +434,14 @@ Treat both like `NotFound` if you match on errors.

### Sessions cap peer requests (#4820)

A peer past the session's announce, subscription, or fetch bound loses the session with `TooManyRequests`.
A peer past the session's announce bound loses the session with `TooManyRequests`.
Stream credit bounds its subscriptions and FETCHes, except on moq-transport drafts 14 to 16, where the request window sized by `announces` and `requests` does.
Raise the defaults for a large relay mesh.

```rust
let client = Client::new().with_limits(session::Limits { subscriptions: 100_000, ..Default::default() });
let mut limits = session::Limits::default();
limits.announces = 1_000_000;
let client = Client::new().with_limits(limits);
```

### A path is one broadcast (#4741)
Expand Down
6 changes: 3 additions & 3 deletions rs/moq-net/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1113,9 +1113,9 @@ impl Issue {
}
}

/// A request's slot against the session's cap, shared with the application's verification
/// of its token so work still in flight stays counted after the request ends. Opaque here:
/// auth only keeps it alive.
/// A request's slot in the session's count, shared with the application's verification of its
/// token so work still in flight stays counted after the request ends. Opaque here: auth only
/// keeps it alive.
pub(crate) type Hold = std::sync::Arc<dyn std::any::Any + Send + Sync>;

/// The serving task's hold on one peer token's [`Issue`].
Expand Down
2 changes: 1 addition & 1 deletion rs/moq-net/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ impl Client {
// The server's requests, admitted up to our limits and granted back as they close.
parameters.set_varint(
ietf::ParameterVarInt::MaxRequestId,
ietf::initial_max_request_id(self.limits.requests(), false),
ietf::initial_max_request_id(self.limits.window(), false),
);
parameters.set_bytes(ietf::ParameterBytes::Implementation, b"moq-lite-rs".to_vec());
// Advertise the request path in-band (draft 14-16), same as the lite-05 SETUP.
Expand Down
10 changes: 5 additions & 5 deletions rs/moq-net/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ pub enum SessionError {
#[error("key-value formatting error")]
KeyValueFormatting,

/// The peer went past what the session allows: a request ID at or past the
/// MAX_REQUEST_ID we advertised (moq-transport drafts 14 to 16), or more announcements,
/// subscriptions, or FETCH requests than its `crate::session::Limits`.
/// The peer went past what the session allows: more announcements than its
/// `crate::session::Limits`, or on moq-transport drafts 14 to 16, a request ID at or past
/// the MAX_REQUEST_ID we advertised or more live requests than the window.
#[error("too many requests")]
TooManyRequests,

Expand Down Expand Up @@ -451,8 +451,8 @@ pub enum Error {
#[error("too many parameters")]
TooManyParameters,

/// The peer already holds as many FETCH requests, announcements, or subscriptions as this
/// session allows (see `crate::session::Limits`).
/// The peer went past this session's `crate::session::Limits`: too many announcements, or
/// too many requests on moq-transport drafts 14 to 16.
#[error("too many requests")]
TooManyRequests,

Expand Down
4 changes: 2 additions & 2 deletions rs/moq-net/src/ietf/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ impl Control {
incoming: None,
// The peer takes the other parity: a client's IDs are even, a server's odd.
modern: Spent::new(u64::from(client)),
history: crate::session::Limits::default().requests(),
history: crate::session::Limits::default().window(),
going_away: false,
}),
}
Expand Down Expand Up @@ -484,7 +484,7 @@ mod tests {
/// Skipping to the top of a production-sized window records one run, not every ID.
#[test]
fn draft16_a_jump_is_one_run_split_only_by_requests_inside_it() {
let window = crate::session::Limits::default().requests();
let window = crate::session::Limits::default().window();
let control = Control::new(None, false).with_window(window, false, Version::Draft16);
let top = 2 * (window - 1);
let runs = |control: &Control| {
Expand Down
Loading
Loading