dec-0122

Vista generada de dec-0122: MoQT VOD: backlog acotado por suscriptor y corte EXCESSIVE_LOAD del lento (enmienda dec-0104)

ImplementadoSin versión del tren todavía· generada desde docs/decisions/dec-0122-moqt-vod-backlog-por-suscriptor.md
track/docsdec-0124track/docs:DC10

Vista generada desde docs/decisions/dec-0122-moqt-vod-backlog-por-suscriptor.md. No se edita a mano: bun run docs:gen la regenera y bun run docs:check falla si difiere. El estado aquí es el del model: si discrepa con otra página, manda el model.

CampoValor
EstadoLOCKED
Fecha2026-10-01
Ficherodocs/decisions/dec-0122-moqt-vod-backlog-por-suscriptor.md

Enmienda a: dec-0104

Por qué importa (del frontmatter del ADR):

Gobierna la contrapresión de la entrega SUBSCRIBE del daemon MoQT en VOD (native/zig/media-daemon/moqt/moqt_subscriber.zig StreamSink, moqt_relay.zig, moqt_pending_delivery.zig). Sin este ADR, un executor que leyera sólo dec-0104 («VOD nunca pierde; backpressure al productor») volvería a pausar la pista entera cuando un suscriptor no lee, que es el DoS head-of-line ZT2-P2-02: un espectador sin ACK congelaba a todos los demás de la pista y llenaba la cola compartida del daemon.

Nodos del roadmap que lo citan en refs: track/byte-runtime/m4

Páginas de la documentación que lo citan: Seguridad del data plane (implementado), Sesiones, BFF y autorización (implementado), Byte runtime (implementado)

Texto del ADR

Leído de docs/decisions/dec-0122-moqt-vod-backlog-por-suscriptor.md, el fichero canónico.

dec-0122 — MoQT VOD: backlog acotado por suscriptor y corte EXCESSIVE_LOAD del lento (enmienda dec-0104)

  • Fecha: 2026-10-01
  • Estado: LOCKED
  • Decisor: waxin, vía AskUserQuestion (2026-10-01, AppSec ZT2-P2-02): en VOD cada suscriptor tiene un backlog acotado y al lento se le corta con EXCESSIVE_LOAD; los que siguen el ritmo no pierden nada; el lento retoma con FETCH.
  • Enmienda: dec-0104 §1 (dónde se aplica la contrapresión VOD).
  • Refrenda: dec-0104 §2 y §3 (índice FETCH acotado con .out_of_window, contadores por pista), dec-0117 («los recursos están acotados»), r39 (VOD fiable, ordenado y completo).
  • Origen: hallazgo ZT2-P2-02 de la AppSec final (docs/reviews/2026-10-01-appsec-final.md), arreglo en w6/appsec-zig-transports-2 (9e44083), integrado en c7d7277.

Contexto

dec-0104 decidió que VOD no pierde objetos: con la cola PendingDeliveryQueue llena, el auto-publisher espera (pushBlocking) y el PUBLISH entrante se rechaza contado (push). Para que la cola no se vaciara sobre un suscriptor que no podía escribir, el relay retenía la entrega de la pista mientras cualquier suscriptor de esa pista tuviera su backlog de envío lleno.

Eso convierte al suscriptor más lento en el reloj de todos. La AppSec lo reprodujo por el camino de producción (SUBSCRIBE → pending_queue.push → onPollComplete): un atacante que suscribe y no envía ACK dejaba al otro espectador de la pista con 4 096 KiB de 75 MiB, la cola compartida llegaba a su tope (256) y un objeto de otra pista se rechazaba. Un solo cliente con una SCT de lectura válida paraba la entrega VOD del daemon entero (P2, DoS head-of-line).

La garantía de r39 es para el espectador que lee: que reciba la pista completa y en orden. No obliga a que el servidor espere sin límite a quien no lee.

Qué se decide

1. La contrapresión VOD es por suscriptor, no por pista

  • El relay entrega cada objeto a todos los suscriptores de la pista sin esperar a ninguno. Cada StreamSink escribe lo que le deja el ledger de envío (send_budget, por stream, conexión, sesión MoQT, actor y daemon) y guarda el resto en su propio backlog acotado: referencias a los buffers compartidos, sin copias, hasta SINK_BACKLOG_BYTES (8 MiB) y SINK_BACKLOG_ITEMS (64 objetos). Un objeto entra siempre en un backlog vacío (MAX_OBJECT_LEN lo acota).
  • El backlog se vacía desde pump, en orden, a medida que los ACK abren hueco.
  • Un suscriptor que no sigue el ritmo es el que pasaría de esos topes. A ése, y sólo a ése, se le corta la suscripción con EXCESSIVE_LOAD: se resetean su stream de datos y el stream de petición del SUBSCRIBE con ese código, se libera lo que su backlog referenciaba y el relay quita la suscripción. Queda en el log (StreamSink y relay, warn).
  • Los suscriptores que siguen el ritmo reciben todos los objetos, en orden: para ellos no cambia nada respecto a dec-0104.
  • El lento retoma con FETCH del rango que le falta, desde el índice de dec-0104 §2. Si el rango ya salió de FETCH_INDEX_CAP, recibe FETCH_ERROR «out of window», como fija dec-0104.

2. La cola compartida no se retiene por un suscriptor

PendingDeliveryQueue deja de retener la pista porque un suscriptor tenga el backlog lleno: se vacía cada ciclo de poll. dec-0104 §1 sigue igual en todo lo demás: .live con drop-oldest; .vod con pushBlocking acotado a 2 s para el auto-publisher y push no bloqueante con rechazo contado para el hilo del loop.

Se añade una cuota por pista en push: PER_TRACK_SHARE = PENDING_QUEUE_CAP / 4 (64). Un publicador que inunda su pista entre dos drenados deja tres cuartos de la cola a las demás; lo que pasa de su cuota se rechaza y se cuenta, como cualquier push en el tope.

3. Qué garantiza VOD desde ahora

QuiénQué recibe
Suscriptor que lee a ritmoTodos los objetos, en orden, sin pérdida (sin cambio)
Suscriptor que no lee (o no envía ACK)Su backlog hasta el tope; después, fin de la suscripción con EXCESSIVE_LOAD
Los demás suscriptores de la pistaNo esperan al lento
Otras pistas y el resto del daemonNo esperan al lento; la cola compartida no se llena por él
El lento tras el corteFETCH del rango que le falta; fuera de la ventana del índice, FETCH_ERROR «out of window»

«Sin pérdida silenciosa» se mantiene: el lento no pierde objetos sin saberlo, se le dice con un código de protocolo (EXCESSIVE_LOAD) y puede pedirlos con FETCH.

Lo que este ADR NO decide

  • No cambia los valores de PENDING_QUEUE_CAP (256) ni de FETCH_INDEX_CAP (8192).
  • No declara en el wire la ventana de FETCH (sigue diferido por dec-0104).
  • No reanuda la suscripción automáticamente tras el corte: retomar (FETCH y un SUBSCRIBE nuevo) es del cliente.
  • No toca el perfil .live, que sigue sin runtime (r39 §3).

Efecto sobre los artefactos

  • native/zig/media-daemon/moqt/moqt_subscriber.zig: StreamSink con backlog propio (SINK_BACKLOG_BYTES, SINK_BACKLOG_ITEMS), escritura por el ledger (moqt_send_limits) y corte lagging con EXCESSIVE_LOAD.
  • native/zig/media-daemon/moqt/moqt_relay.zig: la entrega no espera a ningún suscriptor y quita las suscripciones cortadas.
  • native/zig/media-daemon/moqt/moqt_pending_delivery.zig: PER_TRACK_SHARE y Counters.queued.
  • native/zig/media-daemon/moqt/moqt_handler_test.zig: test de producción (el espectador recibe los 300 objetos, la cola queda vacía, otra pista entra y el lento termina con EXCESSIVE_LOAD), con mutantes (ignorar el hueco, no descartar al rezagado, backlog sin tope de bytes, streams fuera del ledger, sin cuota por pista) en rojo.
  • docs/decisions/dec-0104-moqt-colas-vod-sin-perdida.md: banner y sección de enmienda.