🧪 Problem-Driven Systems Lab

🐘 Caso 15 — PHP 8.3#

⬅️ Caso 15 · ⚖️ Comparativa de los 7 stacks · 🐘 Perfil de PHP · 🧬 Todos los perfiles

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

La diferencia que aporta este stack: no tiene la primitiva#

PHP no tiene cola en proceso. No hay queue.Queue, ni chan, ni BlockingQueue, ni Channel. Un array dentro de una request desaparece cuando la request termina, así que no puede haber un productor y un consumidor compartiéndolo.

Consecuencia directa: en PHP el backpressure no vive en el lenguaje, vive en el transporte. Las tres políticas existen igual en producción, pero están en otra capa:

PolíticaDónde vive en PHP
bloquearlisten.backlog de PHP-FPM y el accept queue del kernel. Cuando se llena, el kernel deja de aceptar conexiones y el cliente ve la espera
descartarpm.max_children alcanzado: FPM devuelve 502 y el request se pierde
dead letterla DLQ del broker real (SQS, RabbitMQ, Redis Streams), porque la cola de verdad nunca estuvo en PHP

Y hay algo que PHP enseña mejor que nadie justamente por no tener la primitiva: el backpressure no es una propiedad de la cola, es una propiedad del sistema entero. Si el freno no está en tu proceso, está en el kernel, en el broker o en el balanceador — pero está en algún lado, y conviene saber cuál antes de que te lo muestre un incidente.

Primitivas nativas#

PrimitivaRol
array + array_shift()La cola. Sin capacidad por defecto: «sin límite» es lo que sale si no se escribe nada.
usleep()El tiempo de consumo por mensaje.
memory_limitEl único techo real de la versión sin límite — y cuando se alcanza, el proceso muere.

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.root.yml up -d --build
curl "http://127.0.0.1:8100/15/produce-unbounded?messages=120&consume_ms=2"
curl "http://127.0.0.1:8100/15/produce-bounded?messages=120&capacity=32&policy=block&consume_ms=2"
curl "http://127.0.0.1:8100/15/queue/state"

Nota de fidelidad#

El productor y el consumidor son pasos del mismo bucle porque PHP no tiene concurrencia dentro del proceso: cada 3 mensajes producidos se drena 1. Las métricas de profundidad, edad y pérdida son comparables con los otros stacks; producer_blocked_ms no lo es, porque acá mide el drenaje intercalado y no una espera real.

Dashboard#

Con Accept: text/html, la raíz devuelve un panel para lanzar ambas variantes:

bash
docker compose -f cases/15-message-queue-backpressure/php/compose.yml up -d --build
# abrir http://localhost:8115/

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 ↗