🧪 Problem-Driven Systems Lab

🔵 Caso 15 — .NET 8#

⬅️ Caso 15 · ⚖️ Comparativa de los 7 stacks · 🔵 Perfil de .NET · 🧬 Todos los perfiles

Stack .NET del caso 15. Cola sin capacidad contra cola acotada con política explícita.

El único stack donde la política es un enum del constructor#

csharp
new BoundedChannelOptions(capacity) {
    FullMode = BoundedChannelFullMode.Wait          // backpressure
             | BoundedChannelFullMode.DropOldest    // descarta el viejo
             | BoundedChannelFullMode.DropWrite     // descarta el nuevo
}

En Python, Java o Go la política se expresa eligiendo qué método llamar en cada sitio de envío — y por lo tanto se puede elegir distinto en dos lugares del mismo sistema sin que nada lo note. Acá la decisión se toma una vez, al construir el canal, y después todos los productores la heredan.

Hay un segundo detalle que ningún otro stack tiene: el canal avisa cuando descarta.

csharp
Channel.CreateBounded<Msg>(options, dropped => Interlocked.Increment(ref droppedCount));

No hay que inferir la pérdida de un contador propio ni confiar en que alguien se acuerde de incrementarlo. El descarte silencioso —la peor variante de este caso— es difícil de escribir en .NET.

El reverso#

Channel.CreateUnbounded<T>() es igual de fácil de escribir y no lleva ninguna advertencia. Un método, cero parámetros, y el sistema se queda sin freno.

Primitivas nativas#

PrimitivaRol
Channel.CreateBounded + BoundedChannelFullModeCapacidad y política, en el constructor.
callback itemDroppedEl descarte se reporta solo.
WriteAsync / TryWriteEnvío con espera o sin ella.
ChannelReader.ReadAllAsync()El consumidor, como await foreach.

Rutas#

RutaQué muestra
/healthliveness
/produce-unbounded?messages=120&consume_ms=2queue_depth_peak = total producido; oldest_msg_age_ms_peak sin techo
/produce-bounded?...&policy=blockprofundidad acotada a capacity; producer_blocked_ms > 0, nada se pierde
/produce-bounded?...&policy=drop_oldestprofundidad acotada; dropped > 0, el productor nunca se frena
/produce-bounded?...&policy=dead_letterprofundidad acotada; dead_lettered > 0, revisable en /dlq
/queue/stateprofundidad pico, bytes ocupados y edad del mensaje más viejo
/dlq?limit=20contenido de la dead letter queue
/diagnostics/summaryacumulado por variante y política
/reset-lablimpia DLQ y contadores

Parámetros: messages (1–2000), capacity (1–1000), consume_ms (0–100, cuánto tarda el consumidor por mensaje), policy (block · drop_oldest · dead_letter).

Hub#

bash
docker compose -f compose.dotnet.yml up -d --build
curl "http://127.0.0.1:8500/15/produce-unbounded?messages=120&consume_ms=2"
curl "http://127.0.0.1:8500/15/produce-bounded?messages=120&capacity=32&policy=block&consume_ms=2"
curl "http://127.0.0.1:8500/15/queue/state"

La lección que ningún stack cambia#

Las tres políticas pagan algo distinto y ninguna es gratis:

PolíticaQué paga
blocklatencia: la lentitud viaja aguas arriba hasta el cliente
drop_oldestdatos: se pierden mensajes, y en silencio salvo que se cuenten
dead_letterdeuda operativa: alguien tiene que mirar esa cola (caso 20)

Una cola sin límite parece una cuarta opción sin costo. No lo es: solo difiere el pago hasta el OOM.

Ver esta carpeta en GitHub ↗