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
3 changes: 3 additions & 0 deletions cpp/obs/src/moq-dock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ QString ExplainFailure(int code, const std::string &reason)
return QStringLiteral("Connect failed · the relay did not accept the session");
case MoQError::Kind::Offline:
return {};
case MoQError::Kind::TooManyRequests:
return QStringLiteral(
"Relay request limit reached · reduce tracks or ask the operator to raise the limit");
case MoQError::Kind::Other:
return QString::fromStdString(reason);
}
Expand Down
6 changes: 4 additions & 2 deletions cpp/obs/src/moq-error.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@

namespace MoQError {

enum class Kind { Unauthorized, Forbidden, Certificate, Timeout, Network, Connect, Offline, Other };
enum class Kind { Unauthorized, Forbidden, Certificate, Timeout, Network, Connect, Offline, Other, TooManyRequests };

// What the output recorded a failure as, beside its text. None means no failure.
enum Code : int { None = 0, Unauthorized, Forbidden, Connect, Offline, Other };
enum Code : int { None = 0, Unauthorized, Forbidden, Connect, Offline, Other, TooManyRequests };

inline Kind Classify(int code, std::string reason)
{
if (code == Code::TooManyRequests)
return Kind::TooManyRequests;
for (char &c : reason) {
if (c >= 'A' && c <= 'Z')
c = static_cast<char>(c - 'A' + 'a');
Expand Down
2 changes: 2 additions & 0 deletions cpp/obs/src/moq-output.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ MoQError::Code FailureCode(const moq::Error &error)
if (std::holds_alternative<moq::Error::kConnect>(variant))
return MoQError::Code::Connect;
if (const auto *protocol = std::get_if<moq::Error::kProtocol>(&variant)) {
if (protocol->details.kind == moq::ProtocolKind::kTooManyRequests)
return MoQError::Code::TooManyRequests;
if (protocol->details.kind == moq::ProtocolKind::kUnauthorized)
return MoQError::Code::Unauthorized;
}
Expand Down
3 changes: 2 additions & 1 deletion cpp/obs/test/moq-error-test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ int main()
Kind::Certificate ||
Classify(MoQError::Unauthorized, "reconnect timed out after 10s") != Kind::Unauthorized ||
Classify(MoQError::Forbidden, "forbidden") != Kind::Forbidden ||
Classify(MoQError::Offline, "offline") != Kind::Offline) {
Classify(MoQError::Offline, "offline") != Kind::Offline ||
Classify(MoQError::TooManyRequests, "reconnect timed out") != Kind::TooManyRequests) {
std::fprintf(stderr, "FAIL: connection failure classification\n");
return 1;
}
Expand Down
3 changes: 3 additions & 0 deletions dart/moq_ffi/lib/src/moq.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4249,6 +4249,7 @@ enum MoqProtocolKind {
timestampMismatch,
app,
unknown,
tooManyRequests,
}

class FfiConverterMoqProtocolKind {
Expand Down Expand Up @@ -4299,6 +4300,8 @@ class FfiConverterMoqProtocolKind {
return LiftRetVal(MoqProtocolKind.app, 4);
case 22:
return LiftRetVal(MoqProtocolKind.unknown, 4);
case 23:
return LiftRetVal(MoqProtocolKind.tooManyRequests, 4);
default:
throw UniffiInternalError(
UniffiInternalError.unexpectedEnumCase,
Expand Down
5 changes: 5 additions & 0 deletions doc/bin/obs.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,8 @@ stream with the reason in the log rather than silently using a default.

The plugin is C++ over the [generated C++ bindings](/lib/cpp/) and ships with
every moq-cpp release.

If the relay closes the session for too many requests, reduce the broadcasts
or subscribed tracks, or have the operator adjust its
[session limits](/bin/relay/config#session-and-peer). Reconnecting with the same
workload does not resolve a request cap.
25 changes: 25 additions & 0 deletions doc/bin/relay/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,31 @@ how much unread data one peer can make the relay buffer. Keep
`stream_receive_window` well under it so one slow group cannot starve the
connection.

## \[session] and \[peer]

Request caps 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.

```toml
[session]
announces = 1000 # Simultaneous broadcasts or namespaces.
subscriptions = 1000 # Simultaneously held subscriptions.

[peer]
announces = 100000
subscriptions = 10000
```

These are the defaults. For example, `--peer-subscriptions=50000` raises the
subscription cap on cluster links. Size peer caps for the combined traffic
they carry, and use session peak counters to check 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.

## \[runtime]

By default one work-stealing runtime serves every connection off one UDP
Expand Down
9 changes: 5 additions & 4 deletions doc/concept/stats.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,11 @@ on the new.

`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 per-session limits: a session that goes past one is closed with
`TOO_MANY_REQUESTS`. On `sessions.json` a peak never goes down while the root
has a session; on the totals and presence tracks it never goes down while the
track lives. Summing nodes or roots takes the largest.
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
`sessions.json` a peak never goes down while the root has a session; on the
totals and presence tracks it never goes down while the track lives. Summing
nodes or roots takes the largest.

### Counters

Expand Down
2 changes: 1 addition & 1 deletion doc/lib/c/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ here, from media publish and consume to raw pixels and PCM with the codec
inside. The header is the reference: each function carries a doc comment.

- **Handles and callbacks.** Every object is an integer handle, and every async result arrives on a `moq_status_callback` with your `void *user_data`. A status `> 0` is a live result, `0` a clean close, and `< 0` an error. The last two are terminal: moq-c never touches `user_data` again, so free it there. `*_close` and `*_cancel` only request shutdown; the terminal callback still fires.
- **Errors.** Negative return codes are named by `MOQ_ERROR_*`, and `moq_error()` gives the reason on the calling thread. Auth rejections have their own codes so you don't retry them. For the peer's protocol code use `moq_error_protocol()`; do not parse `moq_error()`.
- **Errors.** Negative return codes are named by `MOQ_ERROR_*`, and `moq_error()` gives the reason on the calling thread. Auth rejections have their own codes so you don't retry them. For the peer's protocol code use `moq_error_protocol()`; do not parse `moq_error()`. A request-cap closure has kind `MOQ_PROTOCOL_KIND_TOO_MANY_REQUESTS`; reduce the workload or adjust the relay's [session limits](/bin/relay/config#session-and-peer) before reconnecting.
- **Threading.** Call any function from any thread. Raw publish calls block until the codec takes the frame, which paces a publisher.
- **Zeroed config is the default.** A zeroed `moq_client_config` means the default for every field, so new fields don't break callers. A field whose default isn't zero has a `has_*` flag: `backoff_timeout_us = 0` means "retry forever" only with `has_backoff_timeout = true`.
- **Audio needs cuts.** Video groups at its keyframes, but audio forms a group only where you call `moq_publish_media_cut`: after every frame, or at a segment cadence to align with video.
Expand Down
6 changes: 6 additions & 0 deletions doc/lib/cpp/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,12 @@ gives the message Rust's `Display` does, for logs; branch on the case, not the
text. A Rust panic or a misused future (`get()` twice) aborts with a message on
stderr instead of throwing.

A session closed for exceeding the relay's request caps reports the protocol
kind `ProtocolKind::kTooManyRequests`. Reduce
the simultaneous announcements or subscriptions, or ask the relay operator
to adjust the [session limits](/bin/relay/config#session-and-peer), before
reconnecting with the same workload.

Check a result before reading it. With exceptions off below C++23, reading the
value of an error is undefined, through `*result` and `value()` alike, since
the bundled `tl::expected` has nothing to throw.
Expand Down
6 changes: 6 additions & 0 deletions doc/lib/dart/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ host still poll in place; independent native wake callbacks can run concurrently
The generated bindings deliver native wakeups to the Dart isolate. Keep UI work
on its owning isolate.

A session closed for exceeding the relay's request caps reports the protocol
kind `ProtocolKind.tooManyRequests`. Reduce
the simultaneous announcements or subscriptions, or ask the relay operator
to adjust the [session limits](/bin/relay/config#session-and-peer), before
reconnecting with the same workload.

## Reference

- API reference: [pub.dev/documentation/moq](https://pub.dev/documentation/moq/latest/)
Expand Down
6 changes: 6 additions & 0 deletions doc/lib/go/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ host still poll in place; independent native wake callbacks can run concurrently
The generated bridge synchronizes wakeups before resuming the awaiting goroutine.
Application goroutines remain responsible for serializing their own shared state.

A session closed for exceeding the relay's request caps reports the protocol
kind `ProtocolKindTooManyRequests`. Reduce
the simultaneous announcements or subscriptions, or ask the relay operator
to adjust the [session limits](/bin/relay/config#session-and-peer), before
reconnecting with the same workload.

## Reference

- API reference: [pkg.go.dev/moq.dev/moq](https://pkg.go.dev/moq.dev/moq)
Expand Down
6 changes: 6 additions & 0 deletions doc/lib/kt/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ host still poll in place; independent native wake callbacks can run concurrently
The generated bindings resume Kotlin coroutines through their dispatcher. Use
the UI dispatcher when updating UI state.

A session closed for exceeding the relay's request caps reports the protocol
kind `ProtocolKind.TOO_MANY_REQUESTS`. Reduce
the simultaneous announcements or subscriptions, or ask the relay operator
to adjust the [session limits](/bin/relay/config#session-and-peer), before
reconnecting with the same workload.

## Reference

- API reference: [javadoc.io/doc/dev.moq/moq](https://javadoc.io/doc/dev.moq/moq)
Expand Down
6 changes: 6 additions & 0 deletions doc/lib/py/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ host still poll in place; independent native wake callbacks can run concurrently
The generated bindings wake Python futures through the asyncio event loop. The
package exit hook joins native workers before interpreter finalization.

A session closed for exceeding the relay's request caps reports the protocol
kind `ProtocolKind.TOO_MANY_REQUESTS`. Reduce
the simultaneous announcements or subscriptions, or ask the relay operator
to adjust the [session limits](/bin/relay/config#session-and-peer), before
reconnecting with the same workload.

## Reference

- API reference: [moq-rs.readthedocs.io](https://moq-rs.readthedocs.io)
Expand Down
6 changes: 6 additions & 0 deletions doc/lib/swift/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ host still poll in place; independent native wake callbacks can run concurrently
The generated bindings resume Swift continuations. Use the appropriate actor
when updating UI state.

A session closed for exceeding the relay's request caps reports the protocol
kind `ProtocolKind.tooManyRequests`. Reduce
the simultaneous announcements or subscriptions, or ask the relay operator
to adjust the [session limits](/bin/relay/config#session-and-peer), before
reconnecting with the same workload.

## Reference

- API reference: [Swift Package Index (DocC)](https://swiftpackageindex.com/moq-dev/moq-swift/documentation/moq)
Expand Down
10 changes: 10 additions & 0 deletions go/wrapper/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,13 @@ func TestErrorSentinels(t *testing.T) {
})
}
}

func TestSessionLimitProtocolKind(t *testing.T) {
err := ffi.NewMoqErrorProtocol(ffi.MoqProtocolError{
Scope: moq.ErrorScopeSession, Code: 7, Kind: moq.ProtocolKindTooManyRequests, Message: "request limit",
})
details, ok := moq.ProtocolError(err)
if !ok || details.Kind != moq.ProtocolKindTooManyRequests || moq.IsAuthError(err) {
t.Fatalf("session limit lost its kind: %+v, %v", details, err)
}
}
2 changes: 2 additions & 0 deletions go/wrapper/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ const (
ProtocolKindApp = ffi.MoqProtocolKindApp
// ProtocolKindUnknown identifies unknown.
ProtocolKindUnknown = ffi.MoqProtocolKindUnknown
// ProtocolKindTooManyRequests identifies an exceeded session request cap.
ProtocolKindTooManyRequests = ffi.MoqProtocolKindTooManyRequests
)

// AudioSampleFormat values: the raw PCM sample layout fed to or returned from the
Expand Down
1 change: 1 addition & 0 deletions py/moq-rs/tests/test_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ async def reject():
def test_protocol_error_helper_covers_known_app_and_unknown():
cases = [
(moq.ErrorScope.SESSION, 0x2, moq.ProtocolKind.UNAUTHORIZED, True),
(moq.ErrorScope.SESSION, 0x7, moq.ProtocolKind.TOO_MANY_REQUESTS, False),
(moq.ErrorScope.SESSION, 64 + 404, moq.ProtocolKind.APP, False),
(moq.ErrorScope.SESSION, 0x1F, moq.ProtocolKind.UNKNOWN, False),
(moq.ErrorScope.STREAM, 64 + 7, moq.ProtocolKind.APP, False),
Expand Down
1 change: 0 additions & 1 deletion quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ demand (#5214), each ranked beside the work it shares code with.
- [IETF fill timeout](/quest/m1/ietf-fill-timeout.md) - native FILL_TIMEOUT bounds upstream waiting on drafts 18-22, alongside local content-age enforcement
- [IETF group ingest refuses loudly](/quest/m1/ietf-ingest-refusal.md) - a subgroup other than 0 or a datagram past object 0 aborts the track, and a peer's malformed group is refused rather than dropped at debug
- [Draft 17-18 withdraw by reset](/quest/m1/ietf-withdraw-reset.md) - withdrawing a PUBLISH_NAMESPACE on drafts 17 and 18 resets the request instead of FIN-ing it
- [Relay session limits](/quest/m1/relay-session-limits.md) - moq-relay sets per-session request limits, tighter for clients than peers, and the bindings name a refused request
- [Catalog track identity](/quest/m1/catalog-tracks.md) - a track's codec and description never change for its name; resolution changes in band below ceilings fixed at creation, and anything else mints a new rendition or epoch
- [Archive](/quest/m1/archive/README.md) - record selected tracks to any object_store and replay them over FETCH or derived HLS; the catalog entry and format may break in place, since no archives exist
- [In-band auth](/quest/m1/auth/README.md) - a session tells its peer what it may publish and subscribe to, unions tokens presented in band, and fails loud on an out-of-scope publish
Expand Down
10 changes: 5 additions & 5 deletions quest/m1/quic/peer-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ setters provide this seam; the transport integration waits for
[the fork switch](/quest/m1/quic/fork/README.md), so moq-tokio and moq-uring
run on `moq-quic` instead of `moq-noq`.

Decided 2026-10-08: [relay session limits](/quest/m1/relay-session-limits.md)
lands first and introduces the relay's peer config surface (classifying a
session as a cluster peer, and a peer table); this quest extends that
surface with the QUIC values rather than adding its own.
Decided 2026-10-08: the relay's peer config surface already exists. A
session is a cluster peer when its admitted grant is marked `peer`, and the
`[peer]` table (beside `[session]` for ordinary clients) holds its request
caps; this quest extends that table with the QUIC values rather than adding
its own.

- moq-net's transport trait gains `set_limits(Limits)` on the session,
`Limits` carrying the three values, implemented by the moq-tokio and
Expand All @@ -47,6 +48,5 @@ viewer session does not; the io_uring path applies the same values; a

## Required

- [Relay session limits](/quest/m1/relay-session-limits.md) - introduces the peer classification and config table this extends
- [Hard fork](/quest/m1/quic/fork/README.md) - moq-tokio and moq-uring run on `moq-quic`, whose setters this calls

37 changes: 0 additions & 37 deletions quest/m1/relay-session-limits.md

This file was deleted.

11 changes: 11 additions & 0 deletions quest/m1/test-flakes-2/interop-close-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,15 @@ observation had the same cause. The server HTTP/3 control-stream lifetime is
still another candidate. Preserve this quest until a failing browser trace
paired with relay logs establishes that cause and its regression.

Seen again 2026-10-11 on #5310's Interop run
[38121507310](https://github.com/moq-dev/moq/actions/runs/38121507310), whose
`interop-failure` artifact keeps the run directory and browser traces. The
relay log pins the refusal at admission, before the relay answered SETUP:
connection 64 closed with "the auth server refused the session"
(07:50:07.583), through `Connection::run`'s reject path, and the harness
reported "Connection lost" by 07:50:07.79. Pair this relay log with the
artifact's browser trace to tell whether the close capsule raced the
server's HTTP/3 control stream or connection teardown. #5310's next push
passed the same check.

Public API: none expected. Wire: none expected.
17 changes: 17 additions & 0 deletions rs/moq-c/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ pub enum moq_protocol_kind {
MOQ_PROTOCOL_KIND_APP = 23,
/// Unknown.
MOQ_PROTOCOL_KIND_UNKNOWN = 24,
/// The session exceeded its announcement or subscription cap.
MOQ_PROTOCOL_KIND_TOO_MANY_REQUESTS = 25,
}

/// A protocol failure a peer sent: scope, verbatim wire code, and recognized kind.
Expand Down Expand Up @@ -377,6 +379,7 @@ fn session_kind(err: &moq_net::SessionError) -> moq_protocol_kind {
moq_net::SessionError::GoawayTimeout => MOQ_PROTOCOL_KIND_GOAWAY_TIMEOUT,
moq_net::SessionError::Timeout => MOQ_PROTOCOL_KIND_TIMEOUT,
moq_net::SessionError::Version => MOQ_PROTOCOL_KIND_VERSION,
moq_net::SessionError::TooManyRequests => MOQ_PROTOCOL_KIND_TOO_MANY_REQUESTS,
moq_net::SessionError::App(_) => MOQ_PROTOCOL_KIND_APP,
moq_net::SessionError::Unknown(_) => MOQ_PROTOCOL_KIND_UNKNOWN,
_ => MOQ_PROTOCOL_KIND_UNKNOWN,
Expand Down Expand Up @@ -455,3 +458,17 @@ impl ffi::ReturnCode for Error {
}
}
}

#[cfg(test)]
mod tests {
#[test]
fn session_limit_has_a_known_kind() {
let error = super::from_session(&moq_net::SessionError::TooManyRequests);
assert_eq!(error.scope, super::moq_error_scope::MOQ_ERROR_SCOPE_SESSION as u32);
assert_eq!(error.code, moq_net::SessionError::TooManyRequests.to_code());
assert_eq!(
error.kind,
super::moq_protocol_kind::MOQ_PROTOCOL_KIND_TOO_MANY_REQUESTS as u32
);
}
}
Loading
Loading