Files
nats/notebooks/04_jetstream_benchmark.ipynb
T
fn-registry agent 9656aa632a chore: initial sync
2026-06-04 23:44:32 +02:00

712 lines
75 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"cells": [
{
"cell_type": "markdown",
"id": "72277c66",
"metadata": {},
"source": [
"# NATS pub/sub — 04 · JetStream a fondo y simulador de rendimiento\n",
"\n",
"Este notebook tiene dos partes:\n",
"\n",
"1. **JetStream a fondo** — más allá del replay básico del notebook 02: anatomía de un stream (almacenamiento, políticas de retención, límites), tipos de consumer, *acks*, deduplicación y políticas de entrega.\n",
"2. **Simulador de rendimiento interactivo** — un botón que, al pulsarlo, lanza un publisher que envía **miles de mensajes** a varios subscribers, con una **gráfica en movimiento** que muestra el throughput en tiempo real.\n",
"\n",
"> Requiere el broker `nats_demo` (arrancado por la primera celda) y `ipywidgets` (incluido en el venv del análisis)."
]
},
{
"cell_type": "markdown",
"id": "530eac60",
"metadata": {},
"source": [
"## Parte A · JetStream a fondo\n",
"\n",
"### Setup\n",
"\n",
"JetStream es la capa de persistencia de NATS. Mientras el core es *fire-and-forget*, JetStream **almacena** los mensajes en un *stream* y permite leerlos con *consumers* que controlan el ritmo, confirman (*ack*) cada mensaje y pueden reproducir el historial."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "847ca130",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Broker: already-running\n",
"JetStream context listo. account info:\n",
" streams=1 consumers=1 memory=0 storage=260\n"
]
}
],
"source": [
"import subprocess, time, json\n",
"\n",
"NATS_CONTAINER = \"nats_demo\"\n",
"NATS_PORT = 4222\n",
"NATS_URL = f\"nats://127.0.0.1:{NATS_PORT}\"\n",
"\n",
"def _docker(*args, check=True):\n",
" return subprocess.run([\"docker\", *args], capture_output=True, text=True, check=check)\n",
"\n",
"def ensure_nats(name=NATS_CONTAINER, port=NATS_PORT):\n",
" \"\"\"Arranca un broker NATS en Docker de forma idempotente. Devuelve el estado.\"\"\"\n",
" out = _docker(\"ps\", \"-a\", \"--filter\", f\"name=^{name}$\", \"--format\", \"{{.State}}\", check=False).stdout.strip()\n",
" if out == \"running\":\n",
" state = \"already-running\"\n",
" elif out in (\"exited\", \"created\", \"paused\"):\n",
" _docker(\"start\", name)\n",
" state = \"started\"\n",
" else:\n",
" _docker(\"run\", \"-d\", \"--name\", name, \"-p\", f\"{port}:4222\", \"-p\", \"8222:8222\",\n",
" \"nats:latest\", \"-js\", \"-m\", \"8222\")\n",
" state = \"created\"\n",
" time.sleep(1.0)\n",
" return state\n",
"\n",
"import asyncio\n",
"import nats\n",
"\n",
"print(\"Broker:\", ensure_nats())\n",
"nc = await nats.connect(NATS_URL, name=\"notebook-04\")\n",
"js = nc.jetstream()\n",
"print(\"JetStream context listo. account info:\")\n",
"ai = await js.account_info()\n",
"print(f\" streams={ai.streams} consumers={ai.consumers} memory={ai.memory} storage={ai.storage}\")"
]
},
{
"cell_type": "markdown",
"id": "b55873ec",
"metadata": {},
"source": [
"### 1 · Anatomía de un stream\n",
"\n",
"Un **stream** se define por:\n",
"\n",
"- **subjects** — qué subjects captura (`pedidos.>`).\n",
"- **storage** — `file` (persistente en disco) o `memory` (rápido, se pierde al reiniciar).\n",
"- **retention** — cuándo se descartan los mensajes:\n",
" - `limits` (por defecto): se guardan hasta tocar un límite (`max_msgs`, `max_bytes`, `max_age`).\n",
" - `interest`: se descartan cuando todos los consumers interesados los han recibido.\n",
" - `workqueue`: cada mensaje se borra en cuanto **un** consumer lo confirma (cola de trabajo).\n",
"- **límites** — `max_msgs`, `max_bytes`, `max_age` (segundos), `max_msg_size`.\n",
"- **duplicate_window** — ventana de deduplicación (ver §3).\n",
"\n",
"Creamos un stream `limits` con almacenamiento en disco y un tope de mensajes."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "67118a66",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Stream creado:\n",
" name : DEMO_LIMITS\n",
" subjects : ['demo.limits.>']\n",
" storage : file\n",
" retention : limits\n",
" max_msgs : 1000\n",
" max_age (s) : 3600.0\n",
" discard : old\n",
" dup_window (s): 120.0\n"
]
}
],
"source": [
"from nats.js.api import StreamConfig, RetentionPolicy, StorageType, DiscardPolicy\n",
"\n",
"# Recrear limpio para que la demo sea determinista\n",
"for s in (\"DEMO_LIMITS\", \"DEMO_DEDUP\", \"DEMO_WQ\"):\n",
" try:\n",
" await js.delete_stream(s)\n",
" except Exception:\n",
" pass\n",
"\n",
"cfg = StreamConfig(\n",
" name=\"DEMO_LIMITS\",\n",
" subjects=[\"demo.limits.>\"],\n",
" storage=StorageType.FILE,\n",
" retention=RetentionPolicy.LIMITS,\n",
" max_msgs=1000, # tope de mensajes\n",
" max_age=3600, # 1 hora (segundos)\n",
" discard=DiscardPolicy.OLD, # al llegar al tope, descarta los más viejos\n",
" duplicate_window=120, # ventana de dedup: 120 s\n",
")\n",
"info = await js.add_stream(cfg)\n",
"c = info.config\n",
"print(\"Stream creado:\")\n",
"print(f\" name : {c.name}\")\n",
"print(f\" subjects : {c.subjects}\")\n",
"print(f\" storage : {c.storage}\")\n",
"print(f\" retention : {c.retention}\")\n",
"print(f\" max_msgs : {c.max_msgs}\")\n",
"print(f\" max_age (s) : {c.max_age}\")\n",
"print(f\" discard : {c.discard}\")\n",
"print(f\" dup_window (s): {c.duplicate_window}\")"
]
},
{
"cell_type": "markdown",
"id": "9bf78aac",
"metadata": {},
"source": [
"### 2 · Consumers: pull, durable, ack\n",
"\n",
"Un **consumer** es la vista de lectura sobre un stream. Dos ejes:\n",
"\n",
"- **pull vs push**: en *pull* el cliente pide mensajes cuando quiere (`fetch`); en *push* el servidor los empuja según llegan.\n",
"- **durable vs ephemeral**: un consumer *durable* tiene nombre y **recuerda su posición** (cursor) entre reconexiones; uno *ephemeral* desaparece al cerrarse.\n",
"\n",
"El **ack** es la confirmación de procesado. Hasta que un mensaje no se confirma, el consumer lo considera *pendiente* y, si pasa el `ack_wait`, lo **reentrega**. Esto da entrega *at-least-once*."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "499e73f7",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Primer fetch (4 msgs):\n",
" seq=1 evento-0\n",
" seq=2 evento-1\n",
" seq=3 evento-2\n",
" seq=4 evento-3\n",
"\n",
"Consumer 'procesador-A':\n",
" num_pending : 2 (mensajes sin entregar todavía)\n",
" num_ack_pending: 0 (entregados sin ack)\n",
" delivered.stream_seq: 4\n",
"\n",
"Segundo fetch: 2 msgs restantes -> ['evento-4', 'evento-5']\n"
]
}
],
"source": [
"# Publicar 6 mensajes en el stream de límites\n",
"for i in range(6):\n",
" await js.publish(\"demo.limits.eventos\", f\"evento-{i}\".encode())\n",
"\n",
"# Pull consumer DURABLE: recuerda su cursor entre fetches\n",
"psub = await js.pull_subscribe(\"demo.limits.>\", durable=\"procesador-A\")\n",
"\n",
"# Traer 4 y confirmarlos (ack)\n",
"batch = await psub.fetch(4, timeout=2)\n",
"print(\"Primer fetch (4 msgs):\")\n",
"for m in batch:\n",
" print(f\" seq={m.metadata.sequence.stream} {m.data.decode()}\")\n",
" await m.ack()\n",
"\n",
"# Estado del consumer: cuántos quedan pendientes / entregados\n",
"ci = await psub.consumer_info()\n",
"print()\n",
"print(f\"Consumer 'procesador-A':\")\n",
"print(f\" num_pending : {ci.num_pending} (mensajes sin entregar todavía)\")\n",
"print(f\" num_ack_pending: {ci.num_ack_pending} (entregados sin ack)\")\n",
"print(f\" delivered.stream_seq: {ci.delivered.stream_seq}\")\n",
"\n",
"# Segundo fetch: continúa donde se quedó (recuerda el cursor)\n",
"batch2 = await psub.fetch(10, timeout=1)\n",
"print()\n",
"print(f\"Segundo fetch: {len(batch2)} msgs restantes ->\", [m.data.decode() for m in batch2])\n",
"for m in batch2:\n",
" await m.ack()"
]
},
{
"cell_type": "markdown",
"id": "15b96e37",
"metadata": {},
"source": [
"### 3 · Deduplicación por `Nats-Msg-Id`\n",
"\n",
"Si un publisher reintenta por un timeout de red, podría enviar el mismo mensaje dos veces. JetStream lo evita: si dos publicaciones llevan el mismo header **`Nats-Msg-Id`** dentro de la `duplicate_window`, la segunda se reconoce como **duplicada** y **no** se almacena. El `PubAck` lo indica con `duplicate=True`."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "fdc4c498",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"1a publicacion: seq=1 duplicate=None\n",
"2a publicacion: seq=1 duplicate=True <- detectada como duplicado\n",
"3a publicacion (id nuevo): seq=2 duplicate=None\n",
"\n",
"Mensajes realmente almacenados en el stream: 2 (2 publicaciones unicas, 1 descartada)\n"
]
}
],
"source": [
"await js.add_stream(name=\"DEMO_DEDUP\", subjects=[\"demo.dedup.>\"],\n",
" storage=StorageType.FILE, duplicate_window=120)\n",
"\n",
"# Publicar dos veces el MISMO Nats-Msg-Id\n",
"ack1 = await js.publish(\"demo.dedup.pago\", b\"cobro 50e\", headers={\"Nats-Msg-Id\": \"pago-0001\"})\n",
"ack2 = await js.publish(\"demo.dedup.pago\", b\"cobro 50e\", headers={\"Nats-Msg-Id\": \"pago-0001\"})\n",
"\n",
"print(f\"1a publicacion: seq={ack1.seq} duplicate={ack1.duplicate}\")\n",
"print(f\"2a publicacion: seq={ack2.seq} duplicate={ack2.duplicate} <- detectada como duplicado\")\n",
"\n",
"# Un Msg-Id distinto sí se almacena\n",
"ack3 = await js.publish(\"demo.dedup.pago\", b\"cobro 30e\", headers={\"Nats-Msg-Id\": \"pago-0002\"})\n",
"print(f\"3a publicacion (id nuevo): seq={ack3.seq} duplicate={ack3.duplicate}\")\n",
"\n",
"st = (await js.stream_info(\"DEMO_DEDUP\")).state\n",
"print()\n",
"print(f\"Mensajes realmente almacenados en el stream: {st.messages} (2 publicaciones unicas, 1 descartada)\")"
]
},
{
"cell_type": "markdown",
"id": "da997d2e",
"metadata": {},
"source": [
"### 4 · Retención `workqueue`: la cola de trabajo\n",
"\n",
"Con `retention=workqueue`, cada mensaje se **borra del stream en cuanto un consumer lo confirma**. Es el patrón de cola de tareas distribuida: los mensajes se reparten entre workers y desaparecen al procesarse, así el stream no crece sin fin."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "c7c4a35f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Tareas encoladas en el stream: 5\n",
" procesada: tarea-0\n",
" procesada: tarea-1\n",
" procesada: tarea-2\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Mensajes restantes en el stream tras 3 acks: 2 (workqueue borra lo confirmado: 5 -> 2)\n"
]
}
],
"source": [
"from nats.js.api import RetentionPolicy, StorageType\n",
"\n",
"await js.add_stream(name=\"DEMO_WQ\", subjects=[\"demo.wq.>\"],\n",
" storage=StorageType.FILE, retention=RetentionPolicy.WORK_QUEUE)\n",
"\n",
"# Encolar 5 tareas\n",
"for i in range(5):\n",
" await js.publish(\"demo.wq.tareas\", f\"tarea-{i}\".encode())\n",
"\n",
"antes = (await js.stream_info(\"DEMO_WQ\")).state.messages\n",
"print(f\"Tareas encoladas en el stream: {antes}\")\n",
"\n",
"# Un worker consume y confirma 3\n",
"wsub = await js.pull_subscribe(\"demo.wq.>\", durable=\"worker\")\n",
"tres = await wsub.fetch(3, timeout=2)\n",
"for m in tres:\n",
" print(f\" procesada: {m.data.decode()}\")\n",
" await m.ack() # al confirmar, JetStream BORRA el mensaje del stream\n",
"\n",
"await asyncio.sleep(0.3)\n",
"despues = (await js.stream_info(\"DEMO_WQ\")).state.messages\n",
"print()\n",
"print(f\"Mensajes restantes en el stream tras 3 acks: {despues} (workqueue borra lo confirmado: {antes} -> {despues})\")"
]
},
{
"cell_type": "markdown",
"id": "5a68f26f",
"metadata": {},
"source": [
"### 5 · Políticas de entrega (replay)\n",
"\n",
"Al crear un consumer se elige **desde dónde** empieza a leer (`DeliverPolicy`):\n",
"\n",
"- `ALL` — todo el historial desde el principio (lo habitual para reprocesar).\n",
"- `LAST` — solo el último mensaje del stream.\n",
"- `NEW` — solo lo que llegue a partir de ahora.\n",
"- `BY_START_SEQUENCE` / `BY_START_TIME` — desde una secuencia o instante concretos.\n",
"\n",
"Comparamos `ALL` vs `LAST` sobre el stream de límites (que tiene 6 mensajes)."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "a341929e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"DeliverPolicy.ALL -> 6 mensajes: ['evento-0', 'evento-1', 'evento-2', 'evento-3', 'evento-4', 'evento-5']\n",
"DeliverPolicy.LAST -> 1 mensaje : ['evento-5']\n"
]
}
],
"source": [
"from nats.js.api import ConsumerConfig, DeliverPolicy\n",
"\n",
"# Consumer que reproduce TODO el historial\n",
"all_sub = await js.pull_subscribe(\n",
" \"demo.limits.>\", durable=\"replay-all\",\n",
" config=ConsumerConfig(deliver_policy=DeliverPolicy.ALL),\n",
")\n",
"todos = await all_sub.fetch(50, timeout=1)\n",
"print(f\"DeliverPolicy.ALL -> {len(todos)} mensajes:\", [m.data.decode() for m in todos])\n",
"for m in todos:\n",
" await m.ack()\n",
"\n",
"# Consumer que solo entrega el ÚLTIMO\n",
"last_sub = await js.pull_subscribe(\n",
" \"demo.limits.>\", durable=\"replay-last\",\n",
" config=ConsumerConfig(deliver_policy=DeliverPolicy.LAST),\n",
")\n",
"ultimo = await last_sub.fetch(50, timeout=1)\n",
"print(f\"DeliverPolicy.LAST -> {len(ultimo)} mensaje :\", [m.data.decode() for m in ultimo])\n",
"for m in ultimo:\n",
" await m.ack()"
]
},
{
"cell_type": "markdown",
"id": "0fba1dc2",
"metadata": {},
"source": [
"## Parte B · Simulador de rendimiento (interactivo)\n",
"\n",
"Pulsa **▶ Ejecutar benchmark** y verás cómo **un publisher** inunda el broker con miles de mensajes que **varios subscribers** reciben simultáneamente (fan-out). La gráfica se actualiza **en movimiento** mientras corre:\n",
"\n",
"- **Izquierda** — mensajes acumulados: enviados (publisher) vs recibidos (suma de todos los subs).\n",
"- **Derecha** — throughput instantáneo (msgs/s recibidos) muestreado cada ~80 ms.\n",
"\n",
"Ajusta los sliders para cambiar el número de mensajes y de subscribers. Con más mensajes (p. ej. 100.000) la animación dura más y se aprecia mejor la curva."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "4240cd89",
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "bb949b40fc2a44ea8b4431ad1c9a8af9",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"HBox(children=(IntSlider(value=20000, description='Mensajes:', layout=Layout(width='380px'), max=100000, min=1…"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "771cf4615bbc4074801ab261414d56cc",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Button(button_style='success', description='▶ Ejecutar benchmark', layout=Layout(width='220px'), style=ButtonS…"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "7658637369604aae93b8636c1d7b9701",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Output()"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "106fa439c2554d77868e4feee7cedba8",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Output()"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Simulador listo. Pulsa el botón para lanzar el benchmark.\n"
]
}
],
"source": [
"import ipywidgets as widgets\n",
"from IPython.display import display, clear_output\n",
"import matplotlib.pyplot as plt\n",
"import asyncio, time\n",
"import nats\n",
"\n",
"# --- widgets ---\n",
"n_msgs_w = widgets.IntSlider(value=20000, min=1000, max=100000, step=1000,\n",
" description=\"Mensajes:\", style={\"description_width\": \"initial\"},\n",
" layout=widgets.Layout(width=\"380px\"))\n",
"n_subs_w = widgets.IntSlider(value=3, min=1, max=8, step=1,\n",
" description=\"Subscribers:\", style={\"description_width\": \"initial\"})\n",
"run_btn = widgets.Button(description=\"▶ Ejecutar benchmark\", button_style=\"success\",\n",
" layout=widgets.Layout(width=\"220px\"))\n",
"plot_out = widgets.Output()\n",
"log_out = widgets.Output()\n",
"\n",
"SUBJECT = \"bench.load\"\n",
"PAYLOAD = b\"x\" * 128 # 128 bytes por mensaje\n",
"\n",
"def _throughput(ts, recv):\n",
" thr = [0.0]\n",
" for i in range(1, len(ts)):\n",
" dt = ts[i] - ts[i-1]\n",
" thr.append((recv[i] - recv[i-1]) / dt if dt > 0 else 0.0)\n",
" return thr\n",
"\n",
"def render(history, n_subs, n_msgs, done=False):\n",
" ts = [h[0] for h in history]\n",
" sent = [h[1] for h in history]\n",
" recv = [h[2] for h in history]\n",
" thr = _throughput(ts, recv)\n",
" with plot_out:\n",
" clear_output(wait=True)\n",
" fig, (a1, a2) = plt.subplots(1, 2, figsize=(11, 3.6))\n",
" a1.plot(ts, sent, label=\"enviados (pub)\", color=\"#2563eb\", lw=2)\n",
" a1.plot(ts, recv, label=f\"recibidos (Σ {n_subs} subs)\", color=\"#16a34a\", lw=2)\n",
" a1.set_xlabel(\"segundos\"); a1.set_ylabel(\"mensajes acumulados\")\n",
" a1.set_title(\"Publisher vs subscribers\"); a1.legend(loc=\"upper left\")\n",
" a2.plot(ts, thr, color=\"#db2777\", lw=2)\n",
" a2.set_xlabel(\"segundos\"); a2.set_ylabel(\"msgs/s recibidos\")\n",
" a2.set_title(\"Throughput instantáneo\")\n",
" estado = \"✓ DONE\" if done else \"● corriendo…\"\n",
" fig.suptitle(f\"[{estado}] {n_msgs:,} msgs → {n_subs} subs \"\n",
" f\"enviados={sent[-1]:,} recibidos={recv[-1]:,}\", fontsize=11)\n",
" plt.tight_layout(); plt.show()\n",
"\n",
"async def run_benchmark(n_msgs, n_subs, live=True):\n",
" \"\"\"1 publisher -> n_subs subscribers. Devuelve (history, counters).\"\"\"\n",
" nc = await nats.connect(NATS_URL, name=\"benchmark\")\n",
" counters = [0] * n_subs\n",
"\n",
" def make_cb(i):\n",
" async def cb(msg):\n",
" counters[i] += 1\n",
" return cb\n",
"\n",
" subs = [await nc.subscribe(SUBJECT, cb=make_cb(i)) for i in range(n_subs)]\n",
" history = [] # (t, enviados, recibidos_total)\n",
" sent = 0\n",
" t0 = time.monotonic()\n",
"\n",
" async def publish_all():\n",
" nonlocal sent\n",
" for k in range(n_msgs):\n",
" await nc.publish(SUBJECT, PAYLOAD)\n",
" sent += 1\n",
" if k % 1000 == 0:\n",
" await nc.flush()\n",
" await asyncio.sleep(0) # ceder al event loop (deja correr callbacks)\n",
" await nc.flush()\n",
"\n",
" task = asyncio.create_task(publish_all())\n",
"\n",
" # Muestreo para la gráfica en movimiento\n",
" while not task.done() or sum(counters) < sent:\n",
" await asyncio.sleep(0.08)\n",
" history.append((time.monotonic() - t0, sent, sum(counters)))\n",
" if live:\n",
" render(history, n_subs, n_msgs)\n",
" if time.monotonic() - t0 > 30: # tope de seguridad\n",
" break\n",
" await task\n",
"\n",
" # Drenaje final (que los callbacks alcancen al publisher)\n",
" for _ in range(40):\n",
" if sum(counters) >= sent:\n",
" break\n",
" await asyncio.sleep(0.05)\n",
" history.append((time.monotonic() - t0, sent, sum(counters)))\n",
" if live:\n",
" render(history, n_subs, n_msgs, done=True)\n",
"\n",
" for s in subs:\n",
" await s.unsubscribe()\n",
" await nc.drain()\n",
" return history, counters\n",
"\n",
"def on_click(_):\n",
" run_btn.disabled = True\n",
" with log_out:\n",
" clear_output()\n",
" print(f\"Lanzando: {n_msgs_w.value:,} mensajes → {n_subs_w.value} subscribers …\")\n",
" async def go():\n",
" try:\n",
" history, counters = await run_benchmark(n_msgs_w.value, n_subs_w.value, live=True)\n",
" dur = history[-1][0]\n",
" recv = sum(counters)\n",
" with log_out:\n",
" print(f\"OK en {dur:.2f}s\")\n",
" print(f\" enviados : {n_msgs_w.value:,}\")\n",
" print(f\" recibidos: {recv:,} (fan-out ×{n_subs_w.value} = {recv/max(n_msgs_w.value,1):.2f} por mensaje)\")\n",
" print(f\" throughput pub : {n_msgs_w.value/dur:,.0f} msgs/s\")\n",
" print(f\" throughput recv: {recv/dur:,.0f} msgs/s (entregas totales)\")\n",
" print(f\" por subscriber : {counters}\")\n",
" finally:\n",
" run_btn.disabled = False\n",
" asyncio.ensure_future(go())\n",
"\n",
"run_btn.on_click(on_click)\n",
"display(widgets.HBox([n_msgs_w, n_subs_w]), run_btn, plot_out, log_out)\n",
"print(\"Simulador listo. Pulsa el botón para lanzar el benchmark.\")"
]
},
{
"cell_type": "markdown",
"id": "de3caaa5",
"metadata": {},
"source": [
"### Verificación (headless)\n",
"\n",
"La celda anterior renderiza el widget para pulsarlo en JupyterLab. Aquí ejecutamos el mismo benchmark **una vez de forma programática** (sin botón) para dejar evidencia ejecutada: una corrida real de 15.000 mensajes a 3 subscribers con su gráfica final."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "34f06087",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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0aNHNfq8+eabiIqKEh8nJydj6tSpOHz4MMzNzREaGorIyEjo6z+4tSNHjiA8PBwXLlyAq6sr5s+fj3Hjxmlc99NPP8XKlSuRmpoKf39/rF+/Ht26dav1vRARET2JBEHAzdy0sjV1pfvUxckTkFmUo7Wvs2mLctUvveBn440WxpYNEDURUfNXq0Rv2LBh4ucFBQX47LPP4OPjg8DAQADAX3/9hQsXLuCtt97S6cldXFywbNkyeHt7QxAEbNq0CS+88AL+/fdfdOjQAUBp8ZePPvpI7GNq+qBSllKpREhICBwdHXH8+HGkpKRg7NixMDAwwNKlSwEASUlJCAkJwZQpU7B582bExMRg0qRJcHJyQnBwMADgxx9/RHh4OKKiotC9e3esWbMGwcHBuHLlCiuJEhERlREEAbfz7pUmdWUJ3dn0eGQUZWvt62hiW1Yk5cH0S3sT6waImojoyaTzGj11krR48WKN4wsXLsTNmzfx7bffPlJANjY2WLlyJSZOnIi+ffsiICAAa9asqbLtvn37MGTIENy5cwcODg4AgKioKERERODevXswNDREREQE9uzZg/Pnz4v9xowZg8zMTOzfvx8A0L17d3Tt2hUbNmwAAKhUKri6umL69OmYO3dureLmGj0iImpOBEFASl46zsgfFEo5mx6P9EKF1r72xtYahVL8bbzgYGrTAFETETVv9bpGb/v27fjnn38qHX/ttdfQpUuXh070lEoltm/fjtzcXHGkEAA2b96MH374AY6Ojhg6dCg++OADcVQvNjYWvr6+YpIHAMHBwZg6dSouXLiATp06ITY2FkFBQRrPFRwcjJkzZwIAioqKcOrUKcybN088L5VKERQUhNjY2GrjLSwsRGHhg/17FArt//ERERE1Val56aWFUsqmX56RJ+B+QabWfi2MrRBg4wW/si0N/G294Ghi+8RW4SYiaip0TvRMTExw7NixShumHzt2DMbGxjoHcO7cOQQGBqKgoADm5ubYuXMnfHx8AACvvPIK3N3d4ezsjLi4OERERODKlSv4+eefAQCpqakaSR4A8XFqamqNbRQKBfLz85GRkQGlUlllm8uXL1cbd2RkJBYtWqTz/RIRETW2tHy5OPVSXQUzrSBDaz9bI5k47VI9Uuds2oJJHRFRE6Rzojdz5kxMnToVp0+fFouV/P333/j222/xwQcf6BxA27Zt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Jed45H30qBQTPSIiIiIiqk+65Bw6T90EgJiYGMTExCAtLQ0qlUrj3LfffvswlyQiIiIiIqI6onOit2jRInz00Ufo0qWLuE6PiIiIiIiImg6dE72oqChs3LgRr7/+en3EQ0RERERERI9I5+0VioqK0LNnz/qIhYiIiIiIiOqAzonepEmTsGXLlvqIhYiIiIiIiOqAzlM3CwoK8OWXX+LQoUPw8/ODgYGBxvlPPvmkzoIjIiIiIiIi3emc6MXFxSEgIAAAcP78eY1zLMxCRERERETU+HRO9A4fPlwfcRAREREREVEd0XmNnlpCQgIOHDiA/Px8AAD3XSciIiIiImoadE700tPT0b9/f7Rp0wbPPfccUlJSAAATJ07EO++8U+cBEhERERERkW50TvRmzZoFAwMDJCcnw9TUVDw+evRo7N+/v06DIyIiIiIiIt3pvEbv4MGDOHDgAFxcXDSOe3t748aNG3UWGBERERERET0cnUf0cnNzNUby1ORyOYyMjOokKCIiIiIiInp4Oid6zzzzDL7//nvxsUQigUqlwooVK9CvX786DY6IiIiIiIh0p/PUzRUrVqB///74559/UFRUhDlz5uDChQuQy+U4duxYfcRIREREREREOtB5RK9jx464evUqnn76abzwwgvIzc3F8OHD8e+//8LT07M+YiQiIiIiIiIdSARugFcnFAoFLC0tkZWVBZlM1tjhEBERERFRM6NLzqHz1E0AKCgoQFxcHNLS0qBSqTTOPf/88w9zSSIiIiIiIqojOid6+/fvx9ixY3H//v1K5yQSCZRKZZ0ERkRERERERA9H5zV606dPx0svvYSUlBSoVCqNDyZ5REREREREjU/nRO/u3bsIDw+Hg4NDfcRDREREREREj0jnRG/kyJE4cuRIPYRCREREREREdUHnRG/Dhg34+eefMW7cOKxatQrr1q3T+NDFH3/8gaFDh8LZ2RkSiQS//PKLxvlx48ZBIpFofAwaNEijjVwux6uvvgqZTAYrKytMnDgROTk5Gm3i4uLwzDPPwNjYGK6urlixYkWlWLZv34527drB2NgYvr6+2Lt3r073QkRERERE1FToXIzlf//7Hw4ePAhjY2McOXIEEolEPCeRSPD222/X+lq5ubnw9/fHhAkTMHz48CrbDBo0CN9995342MjISOP8q6++ipSUFERHR6O4uBjjx4/H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",
"text/plain": [
"<Figure size 900x360 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"enviados : 15,000\n",
"recibidos: 45,000 por sub -> [15000, 15000, 15000]\n",
"throughput pub : 89,955 msgs/s\n",
"throughput recv: 269,866 msgs/s (entregas totales, fan-out x3)\n"
]
}
],
"source": [
"hist, counters = await run_benchmark(15000, 3, live=False)\n",
"\n",
"ts = [h[0] for h in hist]\n",
"sent = [h[1] for h in hist]\n",
"recv = [h[2] for h in hist]\n",
"dur = ts[-1]\n",
"\n",
"fig, ax = plt.subplots(figsize=(9, 3.6))\n",
"ax.plot(ts, sent, label=\"enviados (pub)\", color=\"#2563eb\", lw=2)\n",
"ax.plot(ts, recv, label=\"recibidos (Σ 3 subs)\", color=\"#16a34a\", lw=2)\n",
"ax.set_xlabel(\"segundos\"); ax.set_ylabel(\"mensajes acumulados\")\n",
"ax.set_title(f\"Benchmark headless: 15.000 msgs → 3 subs en {dur:.2f}s\")\n",
"ax.legend(loc=\"upper left\")\n",
"plt.tight_layout(); plt.show()\n",
"\n",
"print(f\"enviados : 15,000\")\n",
"print(f\"recibidos: {sum(counters):,} por sub -> {counters}\")\n",
"print(f\"throughput pub : {15000/dur:,.0f} msgs/s\")\n",
"print(f\"throughput recv: {sum(counters)/dur:,.0f} msgs/s (entregas totales, fan-out x3)\")"
]
},
{
"cell_type": "markdown",
"id": "501f5185",
"metadata": {},
"source": [
"## Resumen\n",
"\n",
"**JetStream a fondo:**\n",
"- Un **stream** persiste mensajes con políticas de **storage** (file/memory), **retention** (limits/interest/workqueue) y **límites** (max_msgs/max_age).\n",
"- Los **consumers** (pull/push, durable/ephemeral) leen a su ritmo y **confirman** (ack) cada mensaje → entrega *at-least-once*.\n",
"- **Dedup** por `Nats-Msg-Id` evita duplicados por reintentos.\n",
"- **workqueue** borra cada mensaje al confirmarse → cola de trabajo.\n",
"- **DeliverPolicy** controla el replay (all/last/new/by_sequence/by_time).\n",
"\n",
"**Simulador:** demuestra el fan-out a escala — un publisher alimenta a N subscribers con miles de mensajes y la gráfica en vivo muestra que el throughput de recepción sigue al de envío (cada mensaje se entrega a los N subscribers).\n",
"\n",
"### Limpieza\n",
"\n",
"```python\n",
"for s in (\"DEMO_LIMITS\", \"DEMO_DEDUP\", \"DEMO_WQ\"):\n",
" try: await js.delete_stream(s)\n",
" except Exception: pass\n",
"await nc.drain()\n",
"```"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}