Vista generada de dec-0122: MoQT VOD: backlog acotado por suscriptor y corte EXCESSIVE_LOAD del lento (enmienda dec-0104)
docs/decisions/dec-0122-moqt-vod-backlog-por-suscriptor.mdVista generada desde
docs/decisions/dec-0122-moqt-vod-backlog-por-suscriptor.md. No se edita a mano:bun run docs:genla regenera ybun run docs:checkfalla si difiere. El estado aquí es el del model: si discrepa con otra página, manda el model.
| Campo | Valor |
|---|---|
| Estado | LOCKED |
| Fecha | 2026-10-01 |
| Fichero | docs/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.zigStreamSink,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)
Leído de docs/decisions/dec-0122-moqt-vod-backlog-por-suscriptor.md, el fichero canónico.
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.dec-0104 §1 (dónde se aplica la contrapresión VOD).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).docs/reviews/2026-10-01-appsec-final.md),
arreglo en w6/appsec-zig-transports-2 (9e44083), integrado en c7d7277.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.
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).pump, en orden, a medida que los ACK abren hueco.StreamSink y relay, warn).FETCH_INDEX_CAP, recibe FETCH_ERROR «out of window», como fija dec-0104.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.
| Quién | Qué recibe |
|---|---|
| Suscriptor que lee a ritmo | Todos 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 pista | No esperan al lento |
| Otras pistas y el resto del daemon | No esperan al lento; la cola compartida no se llena por él |
| El lento tras el corte | FETCH 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.
PENDING_QUEUE_CAP (256) ni de FETCH_INDEX_CAP (8192)..live, que sigue sin runtime (r39 §3).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.