-
-
Notifications
You must be signed in to change notification settings - Fork 248
fix(net): end a track with its session's error when the session dies #4120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c03f2ba
3f3c281
47c4e7e
3d5ef1d
bab53ca
a3dc504
cfd3c8f
305af76
d26bb24
df2c4f4
c785711
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ import { race, Signal } from "@moq/signals"; | |
| import * as announce from "../announced.ts"; | ||
| import * as broadcast from "../broadcast.ts"; | ||
| import { BroadcastCache } from "../consume.ts"; | ||
| import { controlTimeout, error, ProtocolViolation, reason } from "../error.ts"; | ||
| import { closeError, controlTimeout, error, ProtocolViolation, reason, sessionCause } from "../error.ts"; | ||
| import * as netGroup from "../group.ts"; | ||
| import { Cost, type Route, routesEqual, UNKNOWN_HOP } from "../hop.ts"; | ||
| import { hiddenBelow, hooks, scopeCaptures, scopeHead, scopeOverlaps } from "../internal.ts"; | ||
|
|
@@ -97,6 +97,10 @@ function sees(filter: Filter, path: Path.Valid): boolean { | |
| export class Subscriber { | ||
| #session: Session; | ||
|
|
||
| // The transport, so a request cut off by the session's close ends with the session's | ||
| // error. Optional for tests that drive a bare session. | ||
| #quic?: WebTransport; | ||
|
|
||
| // The Hop IDs this session declared; see {@link Cluster}. What the peer declared is what | ||
| // says whether an advertisement carries a hop path, and ours is what a path looping back | ||
| // to us contains. | ||
|
|
@@ -140,17 +144,21 @@ export class Subscriber { | |
| */ | ||
| constructor({ | ||
| session, | ||
| quic, | ||
| cluster, | ||
| hidden = false, | ||
| }: { | ||
| /** The session abstraction for bidi streams and request IDs. */ | ||
| session: Session; | ||
| /** The transport the session runs on. */ | ||
| quic?: WebTransport; | ||
| /** The Hop IDs the SETUP exchange settled (MoQ Cluster). */ | ||
| cluster?: Cluster.Hops; | ||
| /** Whether the peer understands the HIDDEN parameter (MoQ Hidden). */ | ||
| hidden?: boolean; | ||
| }) { | ||
| this.#session = session; | ||
| this.#quic = quic; | ||
| this.#cluster = cluster; | ||
| this.#hidden = hidden; | ||
| } | ||
|
|
@@ -479,10 +487,22 @@ export class Subscriber { | |
| return consumer; | ||
| } | ||
|
|
||
| // The adapter is gone. If the transport has already closed, that close is the | ||
| // error. A still-open transport, such as a GOAWAY drain, has no peer code yet, | ||
| // so this does not wait for it. | ||
| async #closedSession(): Promise<Error> { | ||
| const quic = this.#quic; | ||
| if (!quic) return new Error("session closed"); | ||
| return Promise.race([ | ||
| closeError(quic), | ||
| new Promise<Error>((resolve) => queueMicrotask(() => resolve(new Error("session closed")))), | ||
| ]); | ||
| } | ||
|
|
||
| async #runSubscribe(broadcast: Path.Valid, request: track.Request) { | ||
| const requestId = await this.#session.nextRequestId(); | ||
| if (requestId === undefined) { | ||
| request.reject(new Error("session closed")); | ||
| request.reject(await this.#closedSession()); | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -533,7 +553,7 @@ export class Subscriber { | |
| console.debug(`subscribe ok: id=${requestId} broadcast=${broadcast} track=${request.name}`); | ||
| } catch (err) { | ||
| // A control request that timed out is not late content, so it carries its own code. | ||
| const e = err instanceof TimeoutError ? controlTimeout(err) : error(err); | ||
| const e = err instanceof TimeoutError ? controlTimeout(err) : await sessionCause(this.#quic, err); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On IETF drafts 14–16, if the control adapter closes while Useful? React with 👍 / 👎.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. When (Written by Grok 4.7) |
||
| request.reject(e); | ||
| console.warn( | ||
| `subscribe error: id=${requestId} broadcast=${broadcast} track=${request.name} error=${reason(e)}`, | ||
|
|
@@ -617,7 +637,7 @@ export class Subscriber { | |
| stream.close(); | ||
| console.debug(`subscribe close: id=${requestId} broadcast=${broadcast} track=${request.name}`); | ||
| } catch (err) { | ||
| const e = error(err); | ||
| const e = await sessionCause(this.#quic, err); | ||
| producer.close(e); | ||
| stream.abort(e); | ||
| console.warn( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ import type * as announce from "../announced.ts"; | |
| import type { Established } from "../connection/established.ts"; | ||
| import { type Probe, type Stats, transportStats } from "../connection/stats.ts"; | ||
| import { type Transport, transportOf } from "../connection/transport.ts"; | ||
| import { error, fromClose, StreamCode, StreamError } from "../error.ts"; | ||
| import { closeError, error, fromClose, StreamCode, StreamError, sessionCause } from "../error.ts"; | ||
| import { type Hop, randomHop } from "../hop.ts"; | ||
| import type { Consumer as OriginConsumer } from "../origin.ts"; | ||
| import type * as Path from "../path.ts"; | ||
|
|
@@ -161,11 +161,17 @@ export class Connection implements Established { | |
| tasks.push(this.#subscriber.runDatagrams()); | ||
| } | ||
|
|
||
| let fatal: Error | undefined; | ||
| try { | ||
| await Promise.all(tasks); | ||
| } catch (err) { | ||
| console.error("fatal error running connection", err); | ||
| // A session-sourced failure is the peer's close, not the raw transport error. | ||
| fatal = await sessionCause(this.#quic, err); | ||
| } finally { | ||
| // The session died under every track it was receiving, so they end with its | ||
| // error. A deliberate close() already ended them cleanly, which makes this a no-op. | ||
| this.#subscriber.close(fatal ?? (await closeError(this.#quic))); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the session stream or an incoming-stream accept loop is the first task to reject during shutdown, Useful? React with 👍 / 👎.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. A fatal task that is the session close now goes through (Written by Grok 4.7) |
||
| this.close(); | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.