From c9e28b8135b1310398a347a4fed2945c241f1763 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Wed, 3 Jun 2026 21:52:38 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20notebook=2004=20=E2=80=94=20JetStream?= =?UTF-8?q?=20a=20fondo=20+=20simulador=20de=20rendimiento=20interactivo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JetStream: anatomia de streams (storage/retention/limits), consumers pull durables con ack y cursor, dedup por Nats-Msg-Id, retencion workqueue, deliver policies. Simulador: boton ipywidgets que lanza 1 publisher -> N subscribers con miles de mensajes y grafica en movimiento (acumulado + throughput instantaneo). --- .ipython/profile_default/history.sqlite | Bin 57344 -> 86016 bytes .jupyter/collaboration_sessions.json | 12 +- .jupyter_ystore.db | Bin 450560 -> 913408 bytes .mcp.json | 14 +- analysis.md | 3 + build_notebook_04.py | 435 +++++++++++ jupyter.log | 362 +++++++-- .../04_jetstream_benchmark-checkpoint.ipynb | 539 ++++++++++++++ notebooks/01_core_pubsub.ipynb | 15 +- notebooks/02_queue_request_jetstream.ipynb | 43 +- notebooks/03_procesos_reales.ipynb | 11 +- notebooks/04_jetstream_benchmark.ipynb | 699 ++++++++++++++++++ 12 files changed, 2029 insertions(+), 104 deletions(-) create mode 100644 build_notebook_04.py create mode 100644 notebooks/.ipynb_checkpoints/04_jetstream_benchmark-checkpoint.ipynb create mode 100644 notebooks/04_jetstream_benchmark.ipynb diff --git a/.ipython/profile_default/history.sqlite b/.ipython/profile_default/history.sqlite index 2f356a20c0fc83131293da321d3de896617f5c1a..c66f8383067a17cbdef6c70c5a91a732602ecd6c 100644 GIT binary patch literal 86016 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 delta 120 zcmZozz}j$td4e>n7y|iadkYwQF|IPo6|0Dk!{^$IUfNIY2Pkw4IFUQNjkO3(3h5s%8GyePhH~25|pX5Kt WzmtC>|4ROaKy~%}n-}`WPXGYFCMC)M diff --git a/.jupyter/collaboration_sessions.json b/.jupyter/collaboration_sessions.json index be08011..0281896 100644 --- a/.jupyter/collaboration_sessions.json +++ b/.jupyter/collaboration_sessions.json @@ -6,7 +6,17 @@ }, "17291f8f-336e-4a5f-8407-a4d22149d581": { "version": "2.4.0", - "created_at": "2026-06-03T17:46:15.674895+00:00", + "created_at": "2026-06-03T19:04:11.779086+00:00", + "document_version": "2.0.0" + }, + "6a0e97d4-34d4-44f3-8a47-243934257256": { + "version": "2.4.0", + "created_at": "2026-06-03T19:16:39.520669+00:00", + "document_version": "2.0.0" + }, + "bc0d99c9-d3bf-49c2-a87a-b38af47d1bff": { + "version": "2.4.0", + "created_at": "2026-06-03T19:18:34.134263+00:00", "document_version": "2.0.0" } } \ No newline at end of file diff --git a/.jupyter_ystore.db b/.jupyter_ystore.db index f852f01fb8d74a66b20802d0edf65903dd5941ed..3eb0dfb597711f12ea00ddea700eeffa1dd62799 100644 GIT binary patch delta 162711 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%0AdlFmTkW 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 z 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 AHUIzs delta 438 zcmZp8VBT;*dV(}-H3I`f-b4j^#_Ej;djvN-Dm>wxq`<-<%0C?_!H_qZM?q_|pg{`% z2*CLBaBr>4C=>Ew;-ZVSK_Z${WUD!?2Hm$uye7UVe4a^#u~vgk0pUciTMfhGUg0sEv7e2>zE3dj2OQ# zZey%qv|{+fu#Z)0Vxt|z5%#AzwYTe>;{3u4vf6IC!zoU!>F-W*+D`X8$5}I-_Zp+a zbk_Tf!Vo8VUt<)VJi%Ub`_Xfp){K)I?CrKY++Y-AWdR1zu8ED?5FVTEagWh{`t>V7 zBirvW22Xdm#i-Tpbe|E3nShuXh*`Ef-Df@YfrE)Lm4WdTW9r7nNJggQ(&-70dCjLA Oykq0ozU(4f6dwRj{gT1} diff --git a/.mcp.json b/.mcp.json index 12c5f56..2b71491 100644 --- a/.mcp.json +++ b/.mcp.json @@ -1,12 +1,16 @@ { "mcpServers": { "jupyter": { - "command": "/home/enmanuel/fn_registry/analysis/nats/.venv/bin/jupyter-mcp-server", + "command": "bash", "args": [ - "--transport", "stdio", - "--jupyter-url", "http://localhost:8890", - "--jupyter-token", "" - ] + "/home/enmanuel/fn_registry/bash/functions/infra/jupyter_mcp_serve.sh" + ], + "env": { + "JUPYTER_MCP_VENV": "/home/enmanuel/fn_registry/analysis/nats/.venv", + "JUPYTER_MCP_ROOT": "/home/enmanuel/fn_registry/analysis/nats", + "JUPYTER_MCP_PORT": "8890", + "JUPYTER_MCP_TOKEN": "" + } } } } diff --git a/analysis.md b/analysis.md index 5b32239..6aa9962 100644 --- a/analysis.md +++ b/analysis.md @@ -21,9 +21,12 @@ Analisis didactico de **NATS** como sistema de mensajeria pub/sub entre procesos | `01_core_pubsub.ipynb` | Modelo base: conexion, publish/subscribe, fan-out a N subscribers, wildcards `*` y `>`. | | `02_queue_request_jetstream.ipynb` | Queue groups (reparto de carga), request/reply (RPC con inbox temporal), JetStream (stream persistente + consumer durable + replay). | | `03_procesos_reales.ipynb` | Publisher y subscribers como **procesos del SO independientes** (`subprocess`), cada uno con su PID. Demuestra el desacople real: el publisher no conoce a sus subscribers. | +| `04_jetstream_benchmark.ipynb` | **JetStream a fondo** (storage/retention/limits, consumers durables + ack + cursor, dedup por `Nats-Msg-Id`, retención `workqueue`, deliver policies) + **simulador de rendimiento interactivo**: un botón `ipywidgets` que lanza 1 publisher → N subscribers con miles de mensajes y una gráfica en movimiento (acumulado pub vs subs + throughput instantáneo). | Los scripts `notebooks/procs/publisher.py` y `notebooks/procs/subscriber.py` son los programas que el notebook 03 lanza como procesos reales. +El notebook 04 requiere `ipywidgets` (incluido en el `.venv` del análisis). El simulador es interactivo: al abrir el notebook en JupyterLab, ejecuta sus celdas hasta el widget y pulsa **▶ Ejecutar benchmark** (los sliders ajustan número de mensajes y de subscribers). La gráfica se anima mientras corre. + ### Como usar 1. Requiere Docker disponible (con permisos para el usuario actual). La primera celda de cada notebook arranca el broker de forma idempotente con la funcion `ensure_nats`, asi que cada notebook funciona de forma aislada. diff --git a/build_notebook_04.py b/build_notebook_04.py new file mode 100644 index 0000000..6d0c031 --- /dev/null +++ b/build_notebook_04.py @@ -0,0 +1,435 @@ +#!/usr/bin/env python3 +"""Generador del notebook 04 del analisis NATS: JetStream a fondo + simulador de +rendimiento interactivo. + +Construye el .ipynb con nbformat (sin ejecutar). La ejecucion se hace despues +contra el servidor Jupyter del analisis (puerto 8890, su propio venv) para que +los outputs queden persistidos y el widget interactivo se renderice en JupyterLab. + +Reproducible: re-ejecutar este script regenera el notebook 04 desde cero. +""" +from pathlib import Path + +import nbformat as nbf + +NBDIR = Path("/home/enmanuel/fn_registry/analysis/nats/notebooks") + + +def build(filename: str, cells: list[tuple[str, str]]) -> None: + nb = nbf.v4.new_notebook() + nb.metadata["kernelspec"] = { + "name": "python3", + "display_name": "Python 3 (ipykernel)", + "language": "python", + } + nb.cells = [ + nbf.v4.new_markdown_cell(src) if typ == "md" else nbf.v4.new_code_cell(src) + for typ, src in cells + ] + nbf.write(nb, str(NBDIR / filename)) + print(f"escrito {filename}: {len(cells)} celdas") + + +ENSURE_NATS = '''import subprocess, time, json + +NATS_CONTAINER = "nats_demo" +NATS_PORT = 4222 +NATS_URL = f"nats://127.0.0.1:{NATS_PORT}" + +def _docker(*args, check=True): + return subprocess.run(["docker", *args], capture_output=True, text=True, check=check) + +def ensure_nats(name=NATS_CONTAINER, port=NATS_PORT): + """Arranca un broker NATS en Docker de forma idempotente. Devuelve el estado.""" + out = _docker("ps", "-a", "--filter", f"name=^{name}$", "--format", "{{.State}}", check=False).stdout.strip() + if out == "running": + state = "already-running" + elif out in ("exited", "created", "paused"): + _docker("start", name) + state = "started" + else: + _docker("run", "-d", "--name", name, "-p", f"{port}:4222", "-p", "8222:8222", + "nats:latest", "-js", "-m", "8222") + state = "created" + time.sleep(1.0) + return state''' + + +nb4 = [ +("md", """# NATS pub/sub — 04 · JetStream a fondo y simulador de rendimiento + +Este notebook tiene dos partes: + +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. +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. + +> Requiere el broker `nats_demo` (arrancado por la primera celda) y `ipywidgets` (incluido en el venv del análisis)."""), + +# ---- Parte A: JetStream a fondo ---- +("md", """## Parte A · JetStream a fondo + +### Setup + +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."""), + +("code", ENSURE_NATS + ''' + +import asyncio +import nats + +print("Broker:", ensure_nats()) +nc = await nats.connect(NATS_URL, name="notebook-04") +js = nc.jetstream() +print("JetStream context listo. account info:") +ai = await js.account_info() +print(f" streams={ai.streams} consumers={ai.consumers} memory={ai.memory} storage={ai.storage}")'''), + +("md", """### 1 · Anatomía de un stream + +Un **stream** se define por: + +- **subjects** — qué subjects captura (`pedidos.>`). +- **storage** — `file` (persistente en disco) o `memory` (rápido, se pierde al reiniciar). +- **retention** — cuándo se descartan los mensajes: + - `limits` (por defecto): se guardan hasta tocar un límite (`max_msgs`, `max_bytes`, `max_age`). + - `interest`: se descartan cuando todos los consumers interesados los han recibido. + - `workqueue`: cada mensaje se borra en cuanto **un** consumer lo confirma (cola de trabajo). +- **límites** — `max_msgs`, `max_bytes`, `max_age` (segundos), `max_msg_size`. +- **duplicate_window** — ventana de deduplicación (ver §3). + +Creamos un stream `limits` con almacenamiento en disco y un tope de mensajes."""), + +("code", '''from nats.js.api import StreamConfig, RetentionPolicy, StorageType, DiscardPolicy + +# Recrear limpio para que la demo sea determinista +for s in ("DEMO_LIMITS", "DEMO_DEDUP", "DEMO_WQ"): + try: + await js.delete_stream(s) + except Exception: + pass + +cfg = StreamConfig( + name="DEMO_LIMITS", + subjects=["demo.limits.>"], + storage=StorageType.FILE, + retention=RetentionPolicy.LIMITS, + max_msgs=1000, # tope de mensajes + max_age=3600, # 1 hora (segundos) + discard=DiscardPolicy.OLD, # al llegar al tope, descarta los más viejos + duplicate_window=120, # ventana de dedup: 120 s +) +info = await js.add_stream(cfg) +c = info.config +print("Stream creado:") +print(f" name : {c.name}") +print(f" subjects : {c.subjects}") +print(f" storage : {c.storage}") +print(f" retention : {c.retention}") +print(f" max_msgs : {c.max_msgs}") +print(f" max_age (s) : {c.max_age}") +print(f" discard : {c.discard}") +print(f" dup_window (s): {c.duplicate_window}")'''), + +("md", """### 2 · Consumers: pull, durable, ack + +Un **consumer** es la vista de lectura sobre un stream. Dos ejes: + +- **pull vs push**: en *pull* el cliente pide mensajes cuando quiere (`fetch`); en *push* el servidor los empuja según llegan. +- **durable vs ephemeral**: un consumer *durable* tiene nombre y **recuerda su posición** (cursor) entre reconexiones; uno *ephemeral* desaparece al cerrarse. + +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*."""), + +("code", '''# Publicar 6 mensajes en el stream de límites +for i in range(6): + await js.publish("demo.limits.eventos", f"evento-{i}".encode()) + +# Pull consumer DURABLE: recuerda su cursor entre fetches +psub = await js.pull_subscribe("demo.limits.>", durable="procesador-A") + +# Traer 4 y confirmarlos (ack) +batch = await psub.fetch(4, timeout=2) +print("Primer fetch (4 msgs):") +for m in batch: + print(f" seq={m.metadata.sequence.stream} {m.data.decode()}") + await m.ack() + +# Estado del consumer: cuántos quedan pendientes / entregados +ci = await psub.consumer_info() +print() +print(f"Consumer 'procesador-A':") +print(f" num_pending : {ci.num_pending} (mensajes sin entregar todavía)") +print(f" num_ack_pending: {ci.num_ack_pending} (entregados sin ack)") +print(f" delivered.stream_seq: {ci.delivered.stream_seq}") + +# Segundo fetch: continúa donde se quedó (recuerda el cursor) +batch2 = await psub.fetch(10, timeout=1) +print() +print(f"Segundo fetch: {len(batch2)} msgs restantes ->", [m.data.decode() for m in batch2]) +for m in batch2: + await m.ack()'''), + +("md", """### 3 · Deduplicación por `Nats-Msg-Id` + +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`."""), + +("code", '''await js.add_stream(name="DEMO_DEDUP", subjects=["demo.dedup.>"], + storage=StorageType.FILE, duplicate_window=120) + +# Publicar dos veces el MISMO Nats-Msg-Id +ack1 = await js.publish("demo.dedup.pago", b"cobro 50e", headers={"Nats-Msg-Id": "pago-0001"}) +ack2 = await js.publish("demo.dedup.pago", b"cobro 50e", headers={"Nats-Msg-Id": "pago-0001"}) + +print(f"1a publicacion: seq={ack1.seq} duplicate={ack1.duplicate}") +print(f"2a publicacion: seq={ack2.seq} duplicate={ack2.duplicate} <- detectada como duplicado") + +# Un Msg-Id distinto sí se almacena +ack3 = await js.publish("demo.dedup.pago", b"cobro 30e", headers={"Nats-Msg-Id": "pago-0002"}) +print(f"3a publicacion (id nuevo): seq={ack3.seq} duplicate={ack3.duplicate}") + +st = (await js.stream_info("DEMO_DEDUP")).state +print() +print(f"Mensajes realmente almacenados en el stream: {st.messages} (2 publicaciones unicas, 1 descartada)")'''), + +("md", """### 4 · Retención `workqueue`: la cola de trabajo + +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."""), + +("code", '''from nats.js.api import RetentionPolicy, StorageType + +await js.add_stream(name="DEMO_WQ", subjects=["demo.wq.>"], + storage=StorageType.FILE, retention=RetentionPolicy.WORK_QUEUE) + +# Encolar 5 tareas +for i in range(5): + await js.publish("demo.wq.tareas", f"tarea-{i}".encode()) + +antes = (await js.stream_info("DEMO_WQ")).state.messages +print(f"Tareas encoladas en el stream: {antes}") + +# Un worker consume y confirma 3 +wsub = await js.pull_subscribe("demo.wq.>", durable="worker") +tres = await wsub.fetch(3, timeout=2) +for m in tres: + print(f" procesada: {m.data.decode()}") + await m.ack() # al confirmar, JetStream BORRA el mensaje del stream + +await asyncio.sleep(0.3) +despues = (await js.stream_info("DEMO_WQ")).state.messages +print() +print(f"Mensajes restantes en el stream tras 3 acks: {despues} (workqueue borra lo confirmado: {antes} -> {despues})")'''), + +("md", """### 5 · Políticas de entrega (replay) + +Al crear un consumer se elige **desde dónde** empieza a leer (`DeliverPolicy`): + +- `ALL` — todo el historial desde el principio (lo habitual para reprocesar). +- `LAST` — solo el último mensaje del stream. +- `NEW` — solo lo que llegue a partir de ahora. +- `BY_START_SEQUENCE` / `BY_START_TIME` — desde una secuencia o instante concretos. + +Comparamos `ALL` vs `LAST` sobre el stream de límites (que tiene 6 mensajes)."""), + +("code", '''from nats.js.api import ConsumerConfig, DeliverPolicy + +# Consumer que reproduce TODO el historial +all_sub = await js.pull_subscribe( + "demo.limits.>", durable="replay-all", + config=ConsumerConfig(deliver_policy=DeliverPolicy.ALL), +) +todos = await all_sub.fetch(50, timeout=1) +print(f"DeliverPolicy.ALL -> {len(todos)} mensajes:", [m.data.decode() for m in todos]) +for m in todos: + await m.ack() + +# Consumer que solo entrega el ÚLTIMO +last_sub = await js.pull_subscribe( + "demo.limits.>", durable="replay-last", + config=ConsumerConfig(deliver_policy=DeliverPolicy.LAST), +) +ultimo = await last_sub.fetch(50, timeout=1) +print(f"DeliverPolicy.LAST -> {len(ultimo)} mensaje :", [m.data.decode() for m in ultimo]) +for m in ultimo: + await m.ack()'''), + +# ---- Parte B: simulador interactivo ---- +("md", """## Parte B · Simulador de rendimiento (interactivo) + +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: + +- **Izquierda** — mensajes acumulados: enviados (publisher) vs recibidos (suma de todos los subs). +- **Derecha** — throughput instantáneo (msgs/s recibidos) muestreado cada ~80 ms. + +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."""), + +("code", '''import ipywidgets as widgets +from IPython.display import display, clear_output +import matplotlib.pyplot as plt +import asyncio, time +import nats + +# --- widgets --- +n_msgs_w = widgets.IntSlider(value=20000, min=1000, max=100000, step=1000, + description="Mensajes:", style={"description_width": "initial"}, + layout=widgets.Layout(width="380px")) +n_subs_w = widgets.IntSlider(value=3, min=1, max=8, step=1, + description="Subscribers:", style={"description_width": "initial"}) +run_btn = widgets.Button(description="▶ Ejecutar benchmark", button_style="success", + layout=widgets.Layout(width="220px")) +plot_out = widgets.Output() +log_out = widgets.Output() + +SUBJECT = "bench.load" +PAYLOAD = b"x" * 128 # 128 bytes por mensaje + +def _throughput(ts, recv): + thr = [0.0] + for i in range(1, len(ts)): + dt = ts[i] - ts[i-1] + thr.append((recv[i] - recv[i-1]) / dt if dt > 0 else 0.0) + return thr + +def render(history, n_subs, n_msgs, done=False): + ts = [h[0] for h in history] + sent = [h[1] for h in history] + recv = [h[2] for h in history] + thr = _throughput(ts, recv) + with plot_out: + clear_output(wait=True) + fig, (a1, a2) = plt.subplots(1, 2, figsize=(11, 3.6)) + a1.plot(ts, sent, label="enviados (pub)", color="#2563eb", lw=2) + a1.plot(ts, recv, label=f"recibidos (Σ {n_subs} subs)", color="#16a34a", lw=2) + a1.set_xlabel("segundos"); a1.set_ylabel("mensajes acumulados") + a1.set_title("Publisher vs subscribers"); a1.legend(loc="upper left") + a2.plot(ts, thr, color="#db2777", lw=2) + a2.set_xlabel("segundos"); a2.set_ylabel("msgs/s recibidos") + a2.set_title("Throughput instantáneo") + estado = "✓ DONE" if done else "● corriendo…" + fig.suptitle(f"[{estado}] {n_msgs:,} msgs → {n_subs} subs " + f"enviados={sent[-1]:,} recibidos={recv[-1]:,}", fontsize=11) + plt.tight_layout(); plt.show() + +async def run_benchmark(n_msgs, n_subs, live=True): + """1 publisher -> n_subs subscribers. Devuelve (history, counters).""" + nc = await nats.connect(NATS_URL, name="benchmark") + counters = [0] * n_subs + + def make_cb(i): + async def cb(msg): + counters[i] += 1 + return cb + + subs = [await nc.subscribe(SUBJECT, cb=make_cb(i)) for i in range(n_subs)] + history = [] # (t, enviados, recibidos_total) + sent = 0 + t0 = time.monotonic() + + async def publish_all(): + nonlocal sent + for k in range(n_msgs): + await nc.publish(SUBJECT, PAYLOAD) + sent += 1 + if k % 1000 == 0: + await nc.flush() + await asyncio.sleep(0) # ceder al event loop (deja correr callbacks) + await nc.flush() + + task = asyncio.create_task(publish_all()) + + # Muestreo para la gráfica en movimiento + while not task.done() or sum(counters) < sent: + await asyncio.sleep(0.08) + history.append((time.monotonic() - t0, sent, sum(counters))) + if live: + render(history, n_subs, n_msgs) + if time.monotonic() - t0 > 30: # tope de seguridad + break + await task + + # Drenaje final (que los callbacks alcancen al publisher) + for _ in range(40): + if sum(counters) >= sent: + break + await asyncio.sleep(0.05) + history.append((time.monotonic() - t0, sent, sum(counters))) + if live: + render(history, n_subs, n_msgs, done=True) + + for s in subs: + await s.unsubscribe() + await nc.drain() + return history, counters + +def on_click(_): + run_btn.disabled = True + with log_out: + clear_output() + print(f"Lanzando: {n_msgs_w.value:,} mensajes → {n_subs_w.value} subscribers …") + async def go(): + try: + history, counters = await run_benchmark(n_msgs_w.value, n_subs_w.value, live=True) + dur = history[-1][0] + recv = sum(counters) + with log_out: + print(f"OK en {dur:.2f}s") + print(f" enviados : {n_msgs_w.value:,}") + print(f" recibidos: {recv:,} (fan-out ×{n_subs_w.value} = {recv/max(n_msgs_w.value,1):.2f} por mensaje)") + print(f" throughput pub : {n_msgs_w.value/dur:,.0f} msgs/s") + print(f" throughput recv: {recv/dur:,.0f} msgs/s (entregas totales)") + print(f" por subscriber : {counters}") + finally: + run_btn.disabled = False + asyncio.ensure_future(go()) + +run_btn.on_click(on_click) +display(widgets.HBox([n_msgs_w, n_subs_w]), run_btn, plot_out, log_out) +print("Simulador listo. Pulsa el botón para lanzar el benchmark.")'''), + +("md", """### Verificación (headless) + +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."""), + +("code", '''hist, counters = await run_benchmark(15000, 3, live=False) + +ts = [h[0] for h in hist] +sent = [h[1] for h in hist] +recv = [h[2] for h in hist] +dur = ts[-1] + +fig, ax = plt.subplots(figsize=(9, 3.6)) +ax.plot(ts, sent, label="enviados (pub)", color="#2563eb", lw=2) +ax.plot(ts, recv, label="recibidos (Σ 3 subs)", color="#16a34a", lw=2) +ax.set_xlabel("segundos"); ax.set_ylabel("mensajes acumulados") +ax.set_title(f"Benchmark headless: 15.000 msgs → 3 subs en {dur:.2f}s") +ax.legend(loc="upper left") +plt.tight_layout(); plt.show() + +print(f"enviados : 15,000") +print(f"recibidos: {sum(counters):,} por sub -> {counters}") +print(f"throughput pub : {15000/dur:,.0f} msgs/s") +print(f"throughput recv: {sum(counters)/dur:,.0f} msgs/s (entregas totales, fan-out x3)")'''), + +("md", """## Resumen + +**JetStream a fondo:** +- Un **stream** persiste mensajes con políticas de **storage** (file/memory), **retention** (limits/interest/workqueue) y **límites** (max_msgs/max_age). +- Los **consumers** (pull/push, durable/ephemeral) leen a su ritmo y **confirman** (ack) cada mensaje → entrega *at-least-once*. +- **Dedup** por `Nats-Msg-Id` evita duplicados por reintentos. +- **workqueue** borra cada mensaje al confirmarse → cola de trabajo. +- **DeliverPolicy** controla el replay (all/last/new/by_sequence/by_time). + +**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). + +### Limpieza + +```python +for s in ("DEMO_LIMITS", "DEMO_DEDUP", "DEMO_WQ"): + try: await js.delete_stream(s) + except Exception: pass +await nc.drain() +```"""), +] + + +if __name__ == "__main__": + build("04_jetstream_benchmark.ipynb", nb4) + print("OK: notebook 04 generado en", NBDIR) diff --git a/jupyter.log b/jupyter.log index 166fb55..dda3f28 100644 --- a/jupyter.log +++ b/jupyter.log @@ -5,21 +5,21 @@ Abre: http://localhost:8890 Ctrl+C para detener -[W 2026-06-03 19:41:26.217 ServerApp] ServerApp.token config is deprecated in 2.0. Use IdentityProvider.token. -[I 2026-06-03 19:41:26.656 ServerApp] jupyter_lsp | extension was successfully linked. -[I 2026-06-03 19:41:26.658 ServerApp] jupyter_mcp_server | extension was successfully linked. -[I 2026-06-03 19:41:26.658 ServerApp] jupyter_mcp_tools | extension was successfully linked. -[I 2026-06-03 19:41:26.660 ServerApp] jupyter_server_fileid | extension was successfully linked. -[I 2026-06-03 19:41:26.662 ServerApp] jupyter_server_nbmodel | extension was successfully linked. -[I 2026-06-03 19:41:26.663 ServerApp] jupyter_server_terminals | extension was successfully linked. -[I 2026-06-03 19:41:26.665 ServerApp] jupyter_server_ydoc | extension was successfully linked. -[I 2026-06-03 19:41:26.667 ServerApp] jupyterlab | extension was successfully linked. -[I 2026-06-03 19:41:26.669 ServerApp] notebook | extension was successfully linked. -[I 2026-06-03 19:41:26.670 ServerApp] notebook_shim | extension was successfully linked. -[W 2026-06-03 19:41:26.680 ServerApp] All authentication is disabled. Anyone who can connect to this server will be able to run code. -[I 2026-06-03 19:41:26.681 ServerApp] notebook_shim | extension was successfully loaded. -[I 2026-06-03 19:41:26.682 ServerApp] jupyter_lsp | extension was successfully loaded. -[06/03/26 19:41:26] INFO Auto-enrolled document extension.py:195 +[W 2026-06-03 21:18:04.909 ServerApp] ServerApp.token config is deprecated in 2.0. Use IdentityProvider.token. +[I 2026-06-03 21:18:05.346 ServerApp] jupyter_lsp | extension was successfully linked. +[I 2026-06-03 21:18:05.348 ServerApp] jupyter_mcp_server | extension was successfully linked. +[I 2026-06-03 21:18:05.348 ServerApp] jupyter_mcp_tools | extension was successfully linked. +[I 2026-06-03 21:18:05.349 ServerApp] jupyter_server_fileid | extension was successfully linked. +[I 2026-06-03 21:18:05.351 ServerApp] jupyter_server_nbmodel | extension was successfully linked. +[I 2026-06-03 21:18:05.352 ServerApp] jupyter_server_terminals | extension was successfully linked. +[I 2026-06-03 21:18:05.354 ServerApp] jupyter_server_ydoc | extension was successfully linked. +[I 2026-06-03 21:18:05.356 ServerApp] jupyterlab | extension was successfully linked. +[I 2026-06-03 21:18:05.358 ServerApp] notebook | extension was successfully linked. +[I 2026-06-03 21:18:05.360 ServerApp] notebook_shim | extension was successfully linked. +[W 2026-06-03 21:18:05.369 ServerApp] All authentication is disabled. Anyone who can connect to this server will be able to run code. +[I 2026-06-03 21:18:05.369 ServerApp] notebook_shim | extension was successfully loaded. +[I 2026-06-03 21:18:05.370 ServerApp] jupyter_lsp | extension was successfully loaded. +[06/03/26 21:18:05] INFO Auto-enrolled document extension.py:195 'notebook.ipynb' as 'default' INFO Jupyter MCP Server Extension extension.py:197 settings initialized @@ -28,62 +28,43 @@ INFO - Health check: /mcp/healthz extension.py:235 INFO - List tools: /mcp/tools/list extension.py:236 INFO - Call tool: /mcp/tools/call extension.py:237 -[I 2026-06-03 19:41:26.784 ServerApp] jupyter_mcp_server | extension was successfully loaded. -[I 2026-06-03 19:41:26.784 ServerApp] Registered jupyter_mcp_tools server extension -[I 2026-06-03 19:41:26.784 ServerApp] jupyter_mcp_tools | extension was successfully loaded. -[I 2026-06-03 19:41:26.784 FileIdExtension] Configured File ID manager: ArbitraryFileIdManager -[I 2026-06-03 19:41:26.784 FileIdExtension] ArbitraryFileIdManager : Configured root dir: /home/enmanuel/fn_registry/analysis/nats -[I 2026-06-03 19:41:26.784 FileIdExtension] ArbitraryFileIdManager : Configured database path: /home/enmanuel/.local/share/jupyter/file_id_manager.db -[I 2026-06-03 19:41:26.785 FileIdExtension] ArbitraryFileIdManager : Successfully connected to database file. -[I 2026-06-03 19:41:26.785 FileIdExtension] ArbitraryFileIdManager : Creating File ID tables and indices with journal_mode = DELETE -[I 2026-06-03 19:41:26.785 FileIdExtension] Attached event listeners. -[I 2026-06-03 19:41:26.785 ServerApp] jupyter_server_fileid | extension was successfully loaded. -[I 2026-06-03 19:41:26.785 ServerApp] jupyter_server_nbmodel | extension was successfully loaded. -[I 2026-06-03 19:41:26.786 ServerApp] jupyter_server_terminals | extension was successfully loaded. -[I 2026-06-03 19:41:26.787 ServerApp] jupyter_server_ydoc | extension was successfully loaded. -[I 2026-06-03 19:41:26.789 LabApp] JupyterLab extension loaded from /home/enmanuel/fn_registry/analysis/nats/.venv/lib/python3.12/site-packages/jupyterlab -[I 2026-06-03 19:41:26.789 LabApp] JupyterLab application directory is /home/enmanuel/fn_registry/analysis/nats/.venv/share/jupyter/lab -[I 2026-06-03 19:41:26.789 LabApp] Extension Manager is 'pypi'. -[I 2026-06-03 19:41:26.810 ServerApp] jupyterlab | extension was successfully loaded. -[I 2026-06-03 19:41:26.811 ServerApp] notebook | extension was successfully loaded. -[I 2026-06-03 19:41:26.812 ServerApp] Serving notebooks from local directory: /home/enmanuel/fn_registry/analysis/nats -[I 2026-06-03 19:41:26.812 ServerApp] Jupyter Server 2.19.0 is running at: -[I 2026-06-03 19:41:26.812 ServerApp] http://localhost:8890/lab -[I 2026-06-03 19:41:26.812 ServerApp] http://127.0.0.1:8890/lab -[I 2026-06-03 19:41:26.812 ServerApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation). -[I 2026-06-03 19:41:26.876 ServerApp] Skipped non-installed server(s): basedpyright, bash-language-server, dockerfile-language-server-nodejs, javascript-typescript-langserver, jedi-language-server, julia-language-server, pyrefly, pyright, python-language-server, python-lsp-server, r-languageserver, sql-language-server, texlab, typescript-language-server, unified-language-server, vscode-css-languageserver-bin, vscode-html-languageserver-bin, vscode-json-languageserver-bin, yaml-language-server -[I 2026-06-03 19:46:15.569 ServerApp] Request for Y document (previously indexed) 'notebooks/01_core_pubsub.ipynb' with room ID: f0c81420-7285-42fa-afcf-aa618e651df6 -[I 2026-06-03 19:46:15.654 LabApp] Build is up to date -[I 2026-06-03 19:46:15.656 ServerApp] MCP Tools WebSocket connection opened -[W 2026-06-03 19:46:15.669 ServerApp] The websocket_ping_timeout (90000) cannot be longer than the websocket_ping_interval (30000). +[I 2026-06-03 21:18:05.464 ServerApp] jupyter_mcp_server | extension was successfully loaded. +[I 2026-06-03 21:18:05.464 ServerApp] Registered jupyter_mcp_tools server extension +[I 2026-06-03 21:18:05.464 ServerApp] jupyter_mcp_tools | extension was successfully loaded. +[I 2026-06-03 21:18:05.464 FileIdExtension] Configured File ID manager: ArbitraryFileIdManager +[I 2026-06-03 21:18:05.464 FileIdExtension] ArbitraryFileIdManager : Configured root dir: /home/enmanuel/fn_registry/analysis/nats +[I 2026-06-03 21:18:05.464 FileIdExtension] ArbitraryFileIdManager : Configured database path: /home/enmanuel/.local/share/jupyter/file_id_manager.db +[I 2026-06-03 21:18:05.464 FileIdExtension] ArbitraryFileIdManager : Successfully connected to database file. +[I 2026-06-03 21:18:05.464 FileIdExtension] ArbitraryFileIdManager : Creating File ID tables and indices with journal_mode = DELETE +[I 2026-06-03 21:18:05.464 FileIdExtension] Attached event listeners. +[I 2026-06-03 21:18:05.464 ServerApp] jupyter_server_fileid | extension was successfully loaded. +[I 2026-06-03 21:18:05.465 ServerApp] jupyter_server_nbmodel | extension was successfully loaded. +[I 2026-06-03 21:18:05.465 ServerApp] jupyter_server_terminals | extension was successfully loaded. +[I 2026-06-03 21:18:05.467 ServerApp] jupyter_server_ydoc | extension was successfully loaded. +[I 2026-06-03 21:18:05.468 LabApp] JupyterLab extension loaded from /home/enmanuel/fn_registry/analysis/nats/.venv/lib/python3.12/site-packages/jupyterlab +[I 2026-06-03 21:18:05.468 LabApp] JupyterLab application directory is /home/enmanuel/fn_registry/analysis/nats/.venv/share/jupyter/lab +[I 2026-06-03 21:18:05.469 LabApp] Extension Manager is 'pypi'. +[I 2026-06-03 21:18:05.489 ServerApp] jupyterlab | extension was successfully loaded. +[I 2026-06-03 21:18:05.491 ServerApp] notebook | extension was successfully loaded. +[I 2026-06-03 21:18:05.491 ServerApp] Serving notebooks from local directory: /home/enmanuel/fn_registry/analysis/nats +[I 2026-06-03 21:18:05.491 ServerApp] Jupyter Server 2.19.0 is running at: +[I 2026-06-03 21:18:05.491 ServerApp] http://localhost:8890/lab +[I 2026-06-03 21:18:05.491 ServerApp] http://127.0.0.1:8890/lab +[I 2026-06-03 21:18:05.491 ServerApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation). +[I 2026-06-03 21:18:05.555 ServerApp] Skipped non-installed server(s): basedpyright, bash-language-server, dockerfile-language-server-nodejs, javascript-typescript-langserver, jedi-language-server, julia-language-server, pyrefly, pyright, python-language-server, python-lsp-server, r-languageserver, sql-language-server, texlab, typescript-language-server, unified-language-server, vscode-css-languageserver-bin, vscode-html-languageserver-bin, vscode-json-languageserver-bin, yaml-language-server +[I 2026-06-03 21:18:25.232 ServerApp] 302 GET / (@127.0.0.1) 0.24ms +[I 2026-06-03 21:18:33.966 LabApp] Build is up to date +[I 2026-06-03 21:18:33.967 ServerApp] Request for Y document (previously indexed) 'notebooks/01_core_pubsub.ipynb' with room ID: f0c81420-7285-42fa-afcf-aa618e651df6 +[I 2026-06-03 21:18:34.060 ServerApp] MCP Tools WebSocket connection opened +[I 2026-06-03 21:18:34.061 ServerApp] Request for Y document (previously indexed) 'notebooks/02_queue_request_jetstream.ipynb' with room ID: fadc19b0-8f98-429c-a6d5-056a05bf47cd +[I 2026-06-03 21:18:34.061 ServerApp] Request for Y document (previously indexed) 'notebooks/03_procesos_reales.ipynb' with room ID: 61f241c1-9f7e-46e7-8fce-e61e1c568d1c +[W 2026-06-03 21:18:34.072 ServerApp] The websocket_ping_timeout (90000) cannot be longer than the websocket_ping_interval (30000). Setting websocket_ping_timeout=30000 -[I 2026-06-03 19:46:15.672 YDocExtension] Creating FileLoader for: notebooks/01_core_pubsub.ipynb -[I 2026-06-03 19:46:15.673 YDocExtension] Watching file: notebooks/01_core_pubsub.ipynb -[I 2026-06-03 19:46:15.675 ServerApp] Initializing room json:notebook:f0c81420-7285-42fa-afcf-aa618e651df6 -[I 2026-06-03 19:46:15.676 ServerApp] Registered 414 tools -[I 2026-06-03 19:46:15.716 ServerApp] Content in room json:notebook:f0c81420-7285-42fa-afcf-aa618e651df6 loaded from the ystore SQLiteYStore -[I 2026-06-03 19:46:15.719 ServerApp] Content in file notebooks/01_core_pubsub.ipynb is out-of-sync with the ystore SQLiteYStore -[I 2026-06-03 19:46:15.720 ServerApp] Content in room json:notebook:f0c81420-7285-42fa-afcf-aa618e651df6 loaded from file notebooks/01_core_pubsub.ipynb -[I 2026-06-03 19:46:16.735 ServerApp] Saving the content from room json:notebook:f0c81420-7285-42fa-afcf-aa618e651df6 -[I 2026-06-03 19:46:16.738 YDocExtension] Saving file: notebooks/01_core_pubsub.ipynb -[I 2026-06-03 19:47:15.407 YDocExtension] Processed 31 Y patches in one minute -[I 2026-06-03 19:47:15.407 YDocExtension] Connected Y users: 2 -[I 2026-06-03 19:48:15.407 YDocExtension] Processed 16 Y patches in one minute -[I 2026-06-03 19:48:15.408 YDocExtension] Connected Y users: 2 -[I 2026-06-03 19:49:15.408 YDocExtension] Processed 16 Y patches in one minute -[I 2026-06-03 19:49:15.409 YDocExtension] Connected Y users: 2 -[I 2026-06-03 19:49:52.746 ServerApp] Out-of-band changes. Overwriting the content in room json:notebook:f0c81420-7285-42fa-afcf-aa618e651df6 -[W 2026-06-03 19:49:52.770 ServerApp] Notebook notebooks/01_core_pubsub.ipynb is not trusted -[I 2026-06-03 19:50:15.409 YDocExtension] Processed 16 Y patches in one minute -[I 2026-06-03 19:50:15.409 YDocExtension] Connected Y users: 2 -[I 2026-06-03 19:50:16.900 ServerApp] Out-of-band changes. Overwriting the content in room json:notebook:f0c81420-7285-42fa-afcf-aa618e651df6 -[I 2026-06-03 19:50:17.929 ServerApp] Out-of-band changes. Overwriting the content in room json:notebook:f0c81420-7285-42fa-afcf-aa618e651df6 -[I 2026-06-03 19:50:22.970 ServerApp] Out-of-band changes. Overwriting the content in room json:notebook:f0c81420-7285-42fa-afcf-aa618e651df6 -[I 2026-06-03 19:50:26.008 ServerApp] Out-of-band changes. Overwriting the content in room json:notebook:f0c81420-7285-42fa-afcf-aa618e651df6 -[I 2026-06-03 19:50:30.051 ServerApp] Out-of-band changes. Overwriting the content in room json:notebook:f0c81420-7285-42fa-afcf-aa618e651df6 -[I 2026-06-03 19:50:34.088 ServerApp] Out-of-band changes. Overwriting the content in room json:notebook:f0c81420-7285-42fa-afcf-aa618e651df6 -[I 2026-06-03 19:50:37.127 ServerApp] Out-of-band changes. Overwriting the content in room json:notebook:f0c81420-7285-42fa-afcf-aa618e651df6 -[06/03/26 19:50:37] ERROR Notebook JSON is invalid: Additional __init__.py:98 +[I 2026-06-03 21:18:34.076 YDocExtension] Creating FileLoader for: notebooks/01_core_pubsub.ipynb +[I 2026-06-03 21:18:34.078 YDocExtension] Watching file: notebooks/01_core_pubsub.ipynb +[I 2026-06-03 21:18:34.078 ServerApp] Registered 414 tools +[I 2026-06-03 21:18:34.081 ServerApp] Initializing room json:notebook:f0c81420-7285-42fa-afcf-aa618e651df6 +[06/03/26 21:18:34] ERROR Notebook JSON is invalid: Additional __init__.py:98 properties are not allowed ('transient' was unexpected) @@ -101,10 +82,237 @@ 'metadata': {}, 'output_type': 'display_data', 'transient': {}} -[W 2026-06-03 19:50:37.149 ServerApp] Notebook notebooks/01_core_pubsub.ipynb is not trusted -[I 2026-06-03 19:51:15.410 YDocExtension] Processed 16 Y patches in one minute -[I 2026-06-03 19:51:15.411 YDocExtension] Connected Y users: 2 -[I 2026-06-03 19:52:15.414 YDocExtension] Processed 16 Y patches in one minute -[I 2026-06-03 19:52:15.414 YDocExtension] Connected Y users: 2 -[I 2026-06-03 19:53:15.417 YDocExtension] Processed 16 Y patches in one minute -[I 2026-06-03 19:53:15.417 YDocExtension] Connected Y users: 2 +[I 2026-06-03 21:18:34.118 YDocExtension] Creating FileLoader for: notebooks/02_queue_request_jetstream.ipynb +[W 2026-06-03 21:18:34.120 ServerApp] Notebook notebooks/01_core_pubsub.ipynb is not trusted +[I 2026-06-03 21:18:34.121 YDocExtension] Watching file: notebooks/02_queue_request_jetstream.ipynb +[I 2026-06-03 21:18:34.122 ServerApp] Initializing room json:notebook:fadc19b0-8f98-429c-a6d5-056a05bf47cd + ERROR Notebook JSON is invalid: Additional __init__.py:98 + properties are not allowed + ('transient' was unexpected) + + Failed validating + 'additionalProperties' in + display_data: + + On + instance['cells'][5]['outputs'][0]: + {'data': {'image/png': + 'iVBORw0KGgoAAAANSUhEUgAAArIAAAEiCAY + AAAAF9zFeAAAAOnRFWHRTb2Z0d2Fy...', + 'text/plain': '
'}, + 'metadata': {}, + 'output_type': 'display_data', + 'transient': {}} +[W 2026-06-03 21:18:34.130 ServerApp] Notebook notebooks/02_queue_request_jetstream.ipynb is not trusted +[I 2026-06-03 21:18:34.133 YDocExtension] Creating FileLoader for: notebooks/03_procesos_reales.ipynb +[I 2026-06-03 21:18:34.133 YDocExtension] Watching file: notebooks/03_procesos_reales.ipynb +[I 2026-06-03 21:18:34.134 ServerApp] Initializing room json:notebook:61f241c1-9f7e-46e7-8fce-e61e1c568d1c + ERROR Notebook JSON is invalid: Additional __init__.py:98 + properties are not allowed + ('transient' was unexpected) + + Failed validating + 'additionalProperties' in + display_data: + + On + instance['cells'][8]['outputs'][0]: + {'data': {'image/png': + 'iVBORw0KGgoAAAANSUhEUgAAArIAAAExCAY + AAACAtUFrAAAAOnRFWHRTb2Z0d2Fy...', + 'text/plain': '
'}, + 'metadata': {}, + 'output_type': 'display_data', + 'transient': {}} +[W 2026-06-03 21:18:34.148 ServerApp] Notebook notebooks/03_procesos_reales.ipynb is not trusted +[I 2026-06-03 21:18:34.153 ServerApp] Content in room json:notebook:61f241c1-9f7e-46e7-8fce-e61e1c568d1c loaded from the ystore SQLiteYStore +[I 2026-06-03 21:18:34.157 ServerApp] Content in room json:notebook:f0c81420-7285-42fa-afcf-aa618e651df6 loaded from the ystore SQLiteYStore +[I 2026-06-03 21:18:34.158 ServerApp] Content in file notebooks/01_core_pubsub.ipynb is out-of-sync with the ystore SQLiteYStore +[I 2026-06-03 21:18:34.158 ServerApp] Content in room json:notebook:f0c81420-7285-42fa-afcf-aa618e651df6 loaded from file notebooks/01_core_pubsub.ipynb +[I 2026-06-03 21:18:34.162 ServerApp] Content in room json:notebook:fadc19b0-8f98-429c-a6d5-056a05bf47cd loaded from the ystore SQLiteYStore +[I 2026-06-03 21:18:34.164 ServerApp] Content in file notebooks/02_queue_request_jetstream.ipynb is out-of-sync with the ystore SQLiteYStore +[I 2026-06-03 21:18:34.164 ServerApp] Content in room json:notebook:fadc19b0-8f98-429c-a6d5-056a05bf47cd loaded from file notebooks/02_queue_request_jetstream.ipynb +[I 2026-06-03 21:18:34.561 ServerApp] Kernel started: adb9c688-aaa0-442b-a665-77464065e37c +[I 2026-06-03 21:18:34.563 ServerApp] Kernel started: 97684b48-3a52-4e4b-9c7f-4d1dd08ab5a6 +[I 2026-06-03 21:18:34.564 ServerApp] Kernel started: 3a4122ce-ea0f-4bd5-8953-7726be1f5de2 +[I 2026-06-03 21:18:34.860 ServerApp] Adapting from protocol version 5.3 (kernel 3a4122ce-ea0f-4bd5-8953-7726be1f5de2) to 5.4 (client). +[I 2026-06-03 21:18:34.861 ServerApp] Connecting to kernel 3a4122ce-ea0f-4bd5-8953-7726be1f5de2. +[I 2026-06-03 21:18:34.883 ServerApp] Adapting from protocol version 5.3 (kernel adb9c688-aaa0-442b-a665-77464065e37c) to 5.4 (client). +[I 2026-06-03 21:18:34.884 ServerApp] Connecting to kernel adb9c688-aaa0-442b-a665-77464065e37c. +[I 2026-06-03 21:18:34.905 ServerApp] Adapting from protocol version 5.3 (kernel 97684b48-3a52-4e4b-9c7f-4d1dd08ab5a6) to 5.4 (client). +[I 2026-06-03 21:18:34.906 ServerApp] Connecting to kernel 97684b48-3a52-4e4b-9c7f-4d1dd08ab5a6. +[I 2026-06-03 21:18:34.921 ServerApp] Adapting from protocol version 5.3 (kernel 3a4122ce-ea0f-4bd5-8953-7726be1f5de2) to 5.4 (client). +[I 2026-06-03 21:18:34.922 ServerApp] Connecting to kernel 3a4122ce-ea0f-4bd5-8953-7726be1f5de2. +[I 2026-06-03 21:18:34.939 ServerApp] Adapting from protocol version 5.3 (kernel 3a4122ce-ea0f-4bd5-8953-7726be1f5de2) to 5.4 (client). +[I 2026-06-03 21:18:34.944 ServerApp] Connecting to kernel 3a4122ce-ea0f-4bd5-8953-7726be1f5de2. +[I 2026-06-03 21:18:35.172 ServerApp] Saving the content from room json:notebook:61f241c1-9f7e-46e7-8fce-e61e1c568d1c +[I 2026-06-03 21:18:35.178 YDocExtension] Saving file: notebooks/03_procesos_reales.ipynb +[W 2026-06-03 21:18:35.179 ServerApp] Notebook notebooks/03_procesos_reales.ipynb is not trusted +[06/03/26 21:18:35] ERROR Notebook JSON is invalid: __init__.py:134 + Additional properties are not + allowed ('transient' was + unexpected) + + Failed validating + 'additionalProperties' in + display_data: + + On + instance['cells'][8]['outputs'][0]: + {'data': {'image/png': + 'iVBORw0KGgoAAAANSUhEUgAAArIAAAExCA + YAAACAtUFrAAAAOnRFWHRTb2Z0d2Fy...', + 'text/plain': '
'}, + 'metadata': {}, + 'output_type': 'display_data', + 'transient': {}} +[I 2026-06-03 21:18:35.269 ServerApp] Saving the content from room json:notebook:f0c81420-7285-42fa-afcf-aa618e651df6 +[I 2026-06-03 21:18:35.275 YDocExtension] Saving file: notebooks/01_core_pubsub.ipynb +[W 2026-06-03 21:18:35.276 ServerApp] Notebook notebooks/01_core_pubsub.ipynb is not trusted + ERROR Notebook JSON is invalid: __init__.py:134 + Additional properties are not + allowed ('transient' was + unexpected) + + Failed validating + 'additionalProperties' in + display_data: + + On + instance['cells'][12]['outputs'][0] + : + {'data': {'image/png': + 'iVBORw0KGgoAAAANSUhEUgAAA3oAAAD5CA + YAAABxn0eTAAAAOnRFWHRTb2Z0d2Fy...', + 'text/plain': '
'}, + 'metadata': {}, + 'output_type': 'display_data', + 'transient': {}} +[I 2026-06-03 21:18:35.436 ServerApp] Saving the content from room json:notebook:fadc19b0-8f98-429c-a6d5-056a05bf47cd +[I 2026-06-03 21:18:35.442 YDocExtension] Saving file: notebooks/02_queue_request_jetstream.ipynb +[W 2026-06-03 21:18:35.444 ServerApp] Notebook notebooks/02_queue_request_jetstream.ipynb is not trusted + ERROR Notebook JSON is invalid: __init__.py:134 + Additional properties are not + allowed ('transient' was + unexpected) + + Failed validating + 'additionalProperties' in + display_data: + + On + instance['cells'][5]['outputs'][0]: + {'data': {'image/png': + 'iVBORw0KGgoAAAANSUhEUgAAArIAAAEiCA + YAAAAF9zFeAAAAOnRFWHRTb2Z0d2Fy...', + 'text/plain': '
'}, + 'metadata': {}, + 'output_type': 'display_data', + 'transient': {}} +[I 2026-06-03 21:19:33.804 YDocExtension] Processed 59 Y patches in one minute +[I 2026-06-03 21:19:33.805 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:20:33.805 YDocExtension] Processed 34 Y patches in one minute +[I 2026-06-03 21:20:33.806 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:21:33.811 YDocExtension] Processed 32 Y patches in one minute +[I 2026-06-03 21:21:33.811 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:22:33.815 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:22:33.815 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:23:33.818 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:23:33.818 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:24:33.824 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:24:33.824 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:25:33.828 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:25:33.828 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:26:33.830 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:26:33.830 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:27:33.831 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:27:33.831 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:28:33.836 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:28:33.836 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:29:33.838 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:29:33.838 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:30:33.838 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:30:33.839 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:31:33.842 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:31:33.842 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:32:33.845 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:32:33.845 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:33:33.851 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:33:33.851 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:34:33.854 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:34:33.855 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:35:33.857 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:35:33.857 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:36:33.863 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:36:33.863 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:37:33.866 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:37:33.867 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:38:33.868 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:38:33.868 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:39:33.875 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:39:33.875 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:40:33.878 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:40:33.879 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:41:33.881 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:41:33.881 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:42:33.882 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:42:33.883 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:43:33.886 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:43:33.886 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:44:33.888 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:44:33.888 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:45:33.889 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:45:33.890 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:46:33.894 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:46:33.894 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:47:33.896 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:47:33.896 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:48:33.898 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:48:33.898 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:49:33.903 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:49:33.903 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:50:33.906 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:50:33.906 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:51:18.556 ServerApp] Kernel started: 8391112c-c8cd-40ef-a0fd-2590d1861f16 +[W 2026-06-03 21:51:18.559 ServerApp] Notebook notebooks/04_jetstream_benchmark.ipynb is not trusted +[I 2026-06-03 21:51:18.873 ServerApp] Adapting from protocol version 5.3 (kernel 8391112c-c8cd-40ef-a0fd-2590d1861f16) to 5.4 (client). +[I 2026-06-03 21:51:18.875 ServerApp] Connecting to kernel 8391112c-c8cd-40ef-a0fd-2590d1861f16. +[I 2026-06-03 21:51:20.065 ServerApp] Starting buffering for 8391112c-c8cd-40ef-a0fd-2590d1861f16:edf9cdd2-e2e57dbe9e1db13c2369bd49 +[I 2026-06-03 21:51:20.070 ServerApp] Saving file at /notebooks/04_jetstream_benchmark.ipynb +[I 2026-06-03 21:51:20.129 ServerApp] Adapting from protocol version 5.3 (kernel 8391112c-c8cd-40ef-a0fd-2590d1861f16) to 5.4 (client). +[I 2026-06-03 21:51:20.131 ServerApp] Connecting to kernel 8391112c-c8cd-40ef-a0fd-2590d1861f16. +[I 2026-06-03 21:51:20.190 ServerApp] Starting buffering for 8391112c-c8cd-40ef-a0fd-2590d1861f16:ce162d97-bcd67ae41b07a72954c689b8 +[I 2026-06-03 21:51:20.194 ServerApp] Saving file at /notebooks/04_jetstream_benchmark.ipynb +[I 2026-06-03 21:51:20.250 ServerApp] Adapting from protocol version 5.3 (kernel 8391112c-c8cd-40ef-a0fd-2590d1861f16) to 5.4 (client). +[I 2026-06-03 21:51:20.251 ServerApp] Connecting to kernel 8391112c-c8cd-40ef-a0fd-2590d1861f16. +[I 2026-06-03 21:51:20.314 ServerApp] Starting buffering for 8391112c-c8cd-40ef-a0fd-2590d1861f16:833d59f8-a7593d93633152b0c03ea130 +[I 2026-06-03 21:51:20.318 ServerApp] Saving file at /notebooks/04_jetstream_benchmark.ipynb +[I 2026-06-03 21:51:20.379 ServerApp] Adapting from protocol version 5.3 (kernel 8391112c-c8cd-40ef-a0fd-2590d1861f16) to 5.4 (client). +[I 2026-06-03 21:51:20.380 ServerApp] Connecting to kernel 8391112c-c8cd-40ef-a0fd-2590d1861f16. +[I 2026-06-03 21:51:20.433 ServerApp] Starting buffering for 8391112c-c8cd-40ef-a0fd-2590d1861f16:cbe487ea-c1f5e2dffb3b06659f933964 +[I 2026-06-03 21:51:20.439 ServerApp] Saving file at /notebooks/04_jetstream_benchmark.ipynb +[I 2026-06-03 21:51:20.490 ServerApp] Adapting from protocol version 5.3 (kernel 8391112c-c8cd-40ef-a0fd-2590d1861f16) to 5.4 (client). +[I 2026-06-03 21:51:20.492 ServerApp] Connecting to kernel 8391112c-c8cd-40ef-a0fd-2590d1861f16. +[I 2026-06-03 21:51:20.849 ServerApp] Starting buffering for 8391112c-c8cd-40ef-a0fd-2590d1861f16:e5ea749a-9c5b0957ba57e53d7545e31c +[I 2026-06-03 21:51:20.853 ServerApp] Saving file at /notebooks/04_jetstream_benchmark.ipynb +[I 2026-06-03 21:51:20.904 ServerApp] Adapting from protocol version 5.3 (kernel 8391112c-c8cd-40ef-a0fd-2590d1861f16) to 5.4 (client). +[I 2026-06-03 21:51:20.906 ServerApp] Connecting to kernel 8391112c-c8cd-40ef-a0fd-2590d1861f16. +[I 2026-06-03 21:51:20.957 ServerApp] Starting buffering for 8391112c-c8cd-40ef-a0fd-2590d1861f16:30826324-d09e241fc54796adcd6a376c +[I 2026-06-03 21:51:20.959 ServerApp] Saving file at /notebooks/04_jetstream_benchmark.ipynb +[I 2026-06-03 21:51:33.909 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:51:33.909 YDocExtension] Connected Y users: 4 +[I 2026-06-03 21:51:42.074 ServerApp] Adapting from protocol version 5.3 (kernel 8391112c-c8cd-40ef-a0fd-2590d1861f16) to 5.4 (client). +[I 2026-06-03 21:51:42.075 ServerApp] Connecting to kernel 8391112c-c8cd-40ef-a0fd-2590d1861f16. +[I 2026-06-03 21:51:42.431 ServerApp] Starting buffering for 8391112c-c8cd-40ef-a0fd-2590d1861f16:c27a340b-675498de282e4e38aec57c86 +[I 2026-06-03 21:51:42.436 ServerApp] Saving file at /notebooks/04_jetstream_benchmark.ipynb +[I 2026-06-03 21:51:42.492 ServerApp] Adapting from protocol version 5.3 (kernel 8391112c-c8cd-40ef-a0fd-2590d1861f16) to 5.4 (client). +[I 2026-06-03 21:51:42.493 ServerApp] Connecting to kernel 8391112c-c8cd-40ef-a0fd-2590d1861f16. +[I 2026-06-03 21:51:42.762 ServerApp] Starting buffering for 8391112c-c8cd-40ef-a0fd-2590d1861f16:a54123fe-7e8fcf758646f2ed9ec519e2 +[I 2026-06-03 21:51:42.764 ServerApp] Saving file at /notebooks/04_jetstream_benchmark.ipynb +[I 2026-06-03 21:52:33.911 YDocExtension] Processed 20 Y patches in one minute +[I 2026-06-03 21:52:33.912 YDocExtension] Connected Y users: 4 diff --git a/notebooks/.ipynb_checkpoints/04_jetstream_benchmark-checkpoint.ipynb b/notebooks/.ipynb_checkpoints/04_jetstream_benchmark-checkpoint.ipynb new file mode 100644 index 0000000..50a6e44 --- /dev/null +++ b/notebooks/.ipynb_checkpoints/04_jetstream_benchmark-checkpoint.ipynb @@ -0,0 +1,539 @@ +{ + "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": null, + "id": "67118a66", + "metadata": {}, + "outputs": [], + "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": null, + "id": "499e73f7", + "metadata": {}, + "outputs": [], + "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": null, + "id": "fdc4c498", + "metadata": {}, + "outputs": [], + "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": null, + "id": "c7c4a35f", + "metadata": {}, + "outputs": [], + "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": null, + "id": "a341929e", + "metadata": {}, + "outputs": [], + "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": null, + "id": "4240cd89", + "metadata": {}, + "outputs": [], + "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": null, + "id": "34f06087", + "metadata": {}, + "outputs": [], + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/01_core_pubsub.ipynb b/notebooks/01_core_pubsub.ipynb index 9402ea6..071dbae 100644 --- a/notebooks/01_core_pubsub.ipynb +++ b/notebooks/01_core_pubsub.ipynb @@ -427,12 +427,21 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "NATS analysis", "language": "python", - "name": "python3" + "name": "nats" }, "language_info": { - "name": "" + "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, diff --git a/notebooks/02_queue_request_jetstream.ipynb b/notebooks/02_queue_request_jetstream.ipynb index 99d90a4..91d2a6a 100644 --- a/notebooks/02_queue_request_jetstream.ipynb +++ b/notebooks/02_queue_request_jetstream.ipynb @@ -1,21 +1,5 @@ { "cells": [ - { - "cell_type": "markdown", - "id": "a5e95055", - "metadata": {}, - "source": [ - "# NATS pub/sub — 02 · Queue groups, Request/Reply y JetStream\n", - "\n", - "En el notebook 01 vimos el *fan-out* del core: una publicación llega a **todos** los subscribers. Aquí cubrimos tres patrones que construyen sobre esa base:\n", - "\n", - "1. **Queue groups** — repartir la carga: varios *workers* comparten el trabajo y cada mensaje lo procesa **uno solo**.\n", - "2. **Request/Reply** — RPC sobre mensajería: un cliente pregunta y espera la respuesta de un servicio.\n", - "3. **JetStream** — la capa de **persistencia**: streams que almacenan los mensajes y permiten *replay*, a diferencia del core *fire-and-forget*.\n", - "\n", - "> Requiere el broker `nats_demo` del notebook 01. La primera celda lo arranca de forma idempotente, así que este notebook también funciona de forma aislada." - ] - }, { "cell_type": "markdown", "id": "1ab4e0d1", @@ -374,6 +358,22 @@ "\n", "**Siguiente** → `03_procesos_reales.ipynb`: hasta ahora todo ha ocurrido dentro de un mismo kernel. Allí lanzamos publisher y subscribers como **procesos del sistema operativo independientes** para ver el desacople real." ] + }, + { + "cell_type": "markdown", + "id": "a5e95055", + "metadata": {}, + "source": [ + "# NATS pub/sub — 02 · Queue groups, Request/Reply y JetStream\n", + "\n", + "En el notebook 01 vimos el *fan-out* del core: una publicación llega a **todos** los subscribers. Aquí cubrimos tres patrones que construyen sobre esa base:\n", + "\n", + "1. **Queue groups** — repartir la carga: varios *workers* comparten el trabajo y cada mensaje lo procesa **uno solo**.\n", + "2. **Request/Reply** — RPC sobre mensajería: un cliente pregunta y espera la respuesta de un servicio.\n", + "3. **JetStream** — la capa de **persistencia**: streams que almacenan los mensajes y permiten *replay*, a diferencia del core *fire-and-forget*.\n", + "\n", + "> Requiere el broker `nats_demo` del notebook 01. La primera celda lo arranca de forma idempotente, así que este notebook también funciona de forma aislada." + ] } ], "metadata": { @@ -383,7 +383,16 @@ "name": "python3" }, "language_info": { - "name": "" + "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, diff --git a/notebooks/03_procesos_reales.ipynb b/notebooks/03_procesos_reales.ipynb index 9e5d425..cbeab38 100644 --- a/notebooks/03_procesos_reales.ipynb +++ b/notebooks/03_procesos_reales.ipynb @@ -510,7 +510,16 @@ "name": "python3" }, "language_info": { - "name": "" + "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, diff --git a/notebooks/04_jetstream_benchmark.ipynb b/notebooks/04_jetstream_benchmark.ipynb new file mode 100644 index 0000000..0207053 --- /dev/null +++ b/notebooks/04_jetstream_benchmark.ipynb @@ -0,0 +1,699 @@ +{ + "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/H5MmTsWXLFgClJUgHDhyIoKAgREVF4dy5c5gwYQKsrKwwefJkAMDx48fx8ssvIzIyEkOGDMGWLVswbNgwnD59Gh07dqz1/RARERERETUFOu+j5+joiLfffhtz586FVKrzgGD1gUgk2LlzJ4YNGyYeGzduHDIzMyuN9KldunQJPj4+OHnyJLp06QKgtCroc889h1u3bsHZ2Rmff/453n//faSmpsLQ0BAAMHfuXPzyyy+4fPkyAGD06NHIzc3F7t27xWv36NEDAQEBiIqKqlX83EePiIiIiKh5KC4RkJ0vIDtPgFIQ4GKrB0MDifaO9axe99ErKirC6NGj6zTJq8mRI0dgb28Pa2trPPvss/j4449ha2sLAIiNjYWVlZWY5AFAUFAQpFIp/v77b7z44ouIjY1F7969xSQPAIKDg7F8+XJkZGTA2toasbGxCA8P13je4ODgahNMACgsLERhYaH4WKFQ1NEdExERERHRoyhRPkjUFHkCFPkqZOcJyMoVkJ2vKj2WV3a+7HHp5wKy81TIK9S83p5FVvBu+VBbkDcanaMNDQ3Fjz/+iPfee68+4tEwaNAgDB8+HB4eHkhMTMR7772HwYMHIzY2Fnp6ekhNTYW9vb1GH319fdjY2CA1NRUAkJqaCg8PD4026oqhqampsLa2RmpqaqUqog4ODuI1qhIZGYlFixbVxW0SEREREVE5SlX5xEuAIq8sGcsXkJWrQna+UC5ZU5VrV/pvbqFOkxabJZ0TPaVSiRUrVuDAgQPw8/ODgYGBxvlPPvmkzoIbM2aM+Lmvry/8/Pzg6emJI0eOoH///nX2PA9j3rx5GqOACoUCrq6ujRgREREREVHToFQJyClLxkqTsnKjZupRtFzNRE6d2CnyBOQWNHyiZmoEWJhKITORwMJUApmpBBYmUujrARamjT9tU1c6J3rnzp1Dp06dAADnz5/XOFe+MEt9aN26NVq0aIGEhAT0798fjo6OSEtL02hTUlICuVwOR0dHAKVrCu/evavRRv1YWxv1+aoYGRlVKgxDRERERNQcqFQCcgqqHzVT5GmOqmkkcvmlSV5DMzEsS9RMJeWSNWnpvyZliVsV50sTOgkM9B+/ZK4mOid6hw8fro84auXWrVtIT0+Hk5MTACAwMBCZmZk4deoUOnfuDAD4/fffoVKp0L17d7HN+++/j+LiYnH0MTo6Gm3btoW1tbXYJiYmBjNnzhSfKzo6GoGBgQ14d0REREREdUOlKh0VU4+QVZz+KCZv6lG0XM1ELqdAgG4lGx+dkQFKk7ByyZf4uZiclSVqFc+bSJpEsZSmpFFXFObk5CAhIUF8nJSUhDNnzsDGxgY2NjZYtGgRRowYAUdHRyQmJmLOnDnw8vJCcHAwAKB9+/YYNGgQ3njjDURFRaG4uBjTpk3DmDFj4OzsDAB45ZVXsGjRIkycOBERERE4f/481q5di9WrV4vPO2PGDPTp0werVq1CSEgItm7din/++Qdffvllw74gREREREQABKEsUcurZvpjfrnpjlWsXcvOb/hEzUAfsCwbNbM0fTD9UWYiffB52XRI8fNy542YqNUpnbdXqEtHjhxBv379Kh0PDQ3F559/jmHDhuHff/9FZmYmnJ2dMXDgQCxevFijcIpcLse0adPw22+/QSqVYsSIEVi3bh3Mzc3FNnFxcQgLC8PJkyfRokULTJ8+HRERERrPuX37dsyfPx/Xr1+Ht7c3VqxYgeeee67W98LtFYiIiIhITRBKC4I8mOr4YPpj+RG2qoqKKHJLEzVVQydqeg9G1MonZjJTadnoWc3TH5mo1T9dco5GTfSaEyZ6RERERM2HIAjIL4I4pVFj+mM1a9UqnleqGjZmfTFR0xw1qzj9sXwiV376o5FB/dfcoEdTr/voERERERE1dYIgoKAIGvunqfdIq276o8b5fAElyoaNWV8P4siZuvqjmKCZlk/cqp7+aGzIRI0eYKJHRERERE2OIAgoLIbGqFl1m11rTI8st/l1cQMnanpSaB0105j+WCGRM2GiRnWIiR4RERER1YvC4nLTG8vtkVaxwmN10x+LSxo2XqkEWis8WphKYFlxHVtZW1MjJmrUdDDRIyIiIqIqFakTtWpGzbJyH1SDLJ/Iqac/FjVwoiaRQGOz6/JFRMTpjxXOl5/+aGYkYaJGzQYTPSIiIqJmqqik+g2uy1d4VJSb7lg+kSssbth4JZJya9TKT3+ssNn1gwqQmtMhTY0kkEqZqBEBTPSIiIiImqziEqFcclb1qJmYrFWc/pivQkFRw8esMYJW0wbX5c5bmpV+bmbMRI2orjDRIyIiIqon6kRNnXhVNWpWPjFT5GomcvmNkKiZm0hqnv5oUi5Rq3De3EQCPSZqRE0CEz0iIiKiapQoyyVqldaqVT0VMjtPQFbZ47zCho/ZzEgiFgxRr0vTNv1Rfd7cRAJ9PSZqRM0BEz0iIiJqtpSqB/ulidUc84RyUxwfTH/Mrjg9MldAbqHQ4DGbGuFB6X2z2kx/fPC5BRM1IirDRI+IiIiaLJVKQE6BZnXH8tMfy4+wVTqfLyAnv+ETNRNDPEjMTNSja+VK8Zfb7PpBeX6pmKgZ6DNRI6JHx0SPiIiI6o1KJSC3oHb7p1Wc/qjIK03yhAbO1YwNISZeGiNoZpWnPz5YyyYV16gZMlEjoiaAiR4RERFVS6USkFdYfv80AVm5tZj+mP+gfUMnakYGqHbUTOtUSBMJDA2YqBHR44+JHhERUTMmCKXrzDQ2u9bYP63curQqpj9m5wtQNXCiZqCPskIilUfN1AVELM0eJHLl16pZmEpgxESNiIiJHhERUVMmCALyCvEgOcsrV1Skiv3Tqpr+2OCJmh40qzlWs3+azKzq6Y9M1IiIHh0TPSIionokCALyi6AxalaahGkWDcnKrbDZdblkTalq2Jj1xUSt+lEzmfghrZTIGRkAEgmTNSKixsREj4iIqAaCIKCgCBX2T6t6s+vsvNLqkBqJXL6AEmXDxqwnLZeoqcv0l9tTrfxm2OrzlmYPEjljQyZqRESPOyZ6RETUrAmCgMJiVKrwWHHUrPz50lL+D0bcihshUXuwwbWW6Y+mlUfYTJioERE98ZjoERFRk1dYXC4RU29mXb7aY7kiIuXPq0v5F5c0bLxSCTSSsaoqPD6oBFnhvKkEZkYSJmpERPRImOgREVG9K1InarWY/lh+M+zsfBWycgUUNXCiJpFArO4oFhWpYfpj+fMyUwlMjSSQSpmoERFR42GiR0REWhWVCMgpm9KorcJjpemR+SoUFDV8zOUTM0v1CFqFza4fJGeaI2xmxkzUiIjo8cZEj4joCVBcUjq1URw1y61i+mP5za4rTI/Mb4REzdxEUmnUrMoNrquY/mhmLIEeEzUiInqCNWqi98cff2DlypU4deoUUlJSsHPnTgwbNkw8LwgCFi5ciK+++gqZmZno1asXPv/8c3h7e4tt5HI5pk+fjt9++w1SqRQjRozA2rVrYW5uLraJi4tDWFgYTp48CTs7O0yfPh1z5szRiGX79u344IMPcP36dXh7e2P58uV47rnn6v01ICKqjRJluUStfLKmbfpjWfXHvMKGj9nMuPwatAqJWll1xwdTITWnP5qbMFEjIiJ6FI2a6OXm5sLf3x8TJkzA8OHDK51fsWIF1q1bh02bNsHDwwMffPABgoODcfHiRRgbGwMAXn31VaSkpCA6OhrFxcUYP348Jk+ejC1btgAAFAoFBg4ciKCgIERFReHcuXOYMGECrKysMHnyZADA8ePH8fLLLyMyMhJDhgzBli1bMGzYMJw+fRodO3ZsuBeEiJotpap84lVW7bGsgEj5/dOqmgqZnScgt7CBd7wGYGZUvgS/5qiZZU3TH01KEzV9PSZqREREjUUiCELD//ZQBYlEojGiJwgCnJ2d8c477+Ddd98FAGRlZcHBwQEbN27EmDFjcOnSJfj4+ODkyZPo0qULAGD//v147rnncOvWLTg7O+Pzzz/H+++/j9TUVBgaGgIA5s6di19++QWXL18GAIwePRq5ubnYvXu3GE+PHj0QEBCAqKioWsWvUChgaWmJrKwsyGSyunpZiKiJUKpK16iVr+5YvmiIQsv0x9yChv9Ra2oEcY+0B+vPqtnsusIImwUTNSIioiZHl5yjya7RS0pKQmpqKoKCgsRjlpaW6N69O2JjYzFmzBjExsbCyspKTPIAICgoCFKpFH///TdefPFFxMbGonfv3mKSBwDBwcFYvnw5MjIyYG1tjdjYWISHh2s8f3BwMH755Zd6v08iahgqlYCcAu0FREr3T6uQyOWXJnkNzcQQD9adlZv+WLGoiKVp5UTOwkQCA30makRERE+qJpvopaamAgAcHBw0jjs4OIjnUlNTYW9vr3FeX18fNjY2Gm08PDwqXUN9ztraGqmpqTU+T1UKCwtRWPhg0YtCodDl9ohIRypV6aiYouJm1+II24OiIllVJHI5BQIaev6CsSE0RtBqKiDyIFmTimvUDJmoERER0UNqsoleUxcZGYlFixY1dhhEjw1BKEvU8qqZ/lh+s+sq1q5l5zd8omZkULbpdcWiIRWmP6rPa0yFNJHA0ICJGhERETWOJpvoOTo6AgDu3r0LJycn8fjdu3cREBAgtklLS9PoV1JSArlcLvZ3dHTE3bt3NdqoH2troz5flXnz5mlM91QoFHB1ddXlFokeK4JQWhBEY7PrslGz8iNsFYuKZJWdy84XoGrgRM1AHxpFQzSmP5bb3LriefWea0ZM1IiIiOgx1WQTPQ8PDzg6OiImJkZM7BQKBf7++29MnToVABAYGIjMzEycOnUKnTt3BgD8/vvvUKlU6N69u9jm/fffR3FxMQwMDAAA0dHRaNu2LaytrcU2MTExmDlzpvj80dHRCAwMrDY+IyMjGBkZ1fVtE9UbQRCQV4gqk7MaN7sud16patiYDfRQaTPrqqY/VnXe0oyJGhERET25GjXRy8nJQUJCgvg4KSkJZ86cgY2NDdzc3DBz5kx8/PHH8Pb2FrdXcHZ2Fitztm/fHoMGDcIbb7yBqKgoFBcXY9q0aRgzZgycnZ0BAK+88goWLVqEiRMnIiIiAufPn8fatWuxevVq8XlnzJiBPn36YNWqVQgJCcHWrVvxzz//4Msvv2zQ14OoJoIgoKAIGvunqfdIq276o8b5fAElyoaNWV8PsDCRwNJMc3qjhZbpj+rzxoalFXmJiIiISDeNur3CkSNH0K9fv0rHQ0NDsXHjRnHD9C+//BKZmZl4+umn8dlnn6FNmzZiW7lcjmnTpmlsmL5u3bpqN0xv0aIFpk+fjoiICI3n3L59O+bPny9umL5ixQqdNkzn9gqkjSAIKCxGFRUeK292rTE9stzm18UNnKjpSfFg1MxMWq7SY9moWrmy/eoy/uXPmzBRIyIiIqozuuQcTWYfvccdE70nQ2FxuemN6hG03NpPfywuadh4pRJUqvBYOrpW8/RHdVtTIyZqRERERE1Fs9hHj6g+FKkTtSpGzbJyNatBlt/sWj39sagREjWL8ptdlxURKZ+sVUzOyk9/NDOSMFEjIiIiegIx0aPHSlFJ1aNmFSs8KspNdyw//bGwuGHjlZQlauWLhDyYCll5+mPFapCmRhJIpUzUiIiIiEg3TPSoQRWVCMjJr2L6Y7lRM0V10x/zVSgoaviYLcqvO6s4amYigaVZ+eRMc4TNzJiJGhERERE1PCZ6pJPiktKpjTWNmqmPZVWRyOU3QqJmbiKpdtRMXd3R0qzqRM7MWAI9JmpERERE9JhhoveEKVGWS9QqrVWrPBVSLDZSVqY/r7DhYzYzLr8GrcL0R1PNxKxiImduwkSNiIiIiJ48TPSaoZwCFRZtzhWrPZaOrpUmd7mFDV9k1cyofAn+qqc/lq8AaVkukTM3kUBfj4kaEREREZEumOg1Q/pSCX6NrbuhN1MjiHukPVh/VvVm1+pj6mTN3FgCA30makREREREDYmJXjNkZAAY6EPcs83EEA9G0LRNfyx3Xj3axkSNiIiIiOjxwkSvGZJIJNj3kTXMjEsTNUMDJmpERERERE8SJnrNlJu9XmOHQEREREREjUTa2AEQERERERFR3WKiR0RERERE1Mww0SMiIiIiImpmmOgRERERERE1M0z0iIiIiIiImhkmekRERERERM0MEz0iIiIiIqJmhokeERERERFRM8MN0+uIIAgAAIVC0ciREBERERFRc6TONdS5R02Y6NWR7OxsAICrq2sjR0JERERERM1ZdnY2LC0ta2wjEWqTDpJWKpUKd+7cgYWFBSQSSWOHQ1RrCoUCrq6uuHnzJmQyWWOHQ6QTvn/pccX3Lj3O+P5tPIIgIDs7G87OzpBKa16FxxG9OiKVSuHi4tLYYRA9NJlMxh/W9Nji+5ceV3zv0uOM79/GoW0kT43FWIiIiIiIiJoZJnpERERERETNDBM9oieckZERFi5cCCMjo8YOhUhnfP/S44rvXXqc8f37eGAxFiIiIiIiomaGI3pERERERETNDBM9IiIiIiKiZoaJHhERERERUTPDRI+oGfr000/RqlUrGBsbo3v37jhx4kS1bS9cuIARI0agVatWkEgkWLNmzSNfk+hh1fV7NzIyEl27doWFhQXs7e0xbNgwXLlypR7vgJ5k9fGzV23ZsmWQSCSYOXNm3QZNhPp5796+fRuvvfYabG1tYWJiAl9fX/zzzz/1dAdUFSZ6RM3Mjz/+iPDwcCxcuBCnT5+Gv78/goODkZaWVmX7vLw8tG7dGsuWLYOjo2OdXJPoYdTHe/fo0aMICwvDX3/9hejoaBQXF2PgwIHIzc2tz1uhJ1B9vH/VTp48iS+++AJ+fn71ETo94erjvZuRkYFevXrBwMAA+/btw8WLF7Fq1SpYW1vX561QRQIRNSvdunUTwsLCxMdKpVJwdnYWIiMjtfZ1d3cXVq9eXafXJKqt+njvVpSWliYAEI4ePfoooRJVUl/v3+zsbMHb21uIjo4W+vTpI8yYMaOOIiYqVR/v3YiICOHpp5+uyzDpIXBEj6gZKSoqwqlTpxAUFCQek0qlCAoKQmxsbJO5JlFFDfU+y8rKAgDY2NjU2TWJ6vP9GxYWhpCQEI1rE9WV+nrv7tq1C126/H979x9TVf3Hcfx15UIX+ZVSOEJTTMFLXoO4wyk12mAmLObSYP2Y3JzScjgim9NJI/1DaxVbi1YJNrCE+qMppqOlQ6dlk37obTCcOmnSnGi1uyL+kOCe7x/feb5d7bsE7/XSuc/HdjbPj/vx/Tn77Gwvzvmc41ZZWZlSUlKUk5OjpqamYJSMMSDoARbyyy+/aHR0VNOmTQvYPm3aNA0MDEyYNoHr3Y5x5vf7VVNTo/z8fM2fPz8obQJS6MbvJ598opMnT+rVV1+91RKBvxWqsdvX16f33ntPc+fO1RdffKG1a9equrpau3btutWSMQb2cBcAAMDtUFVVpZ6eHn311VfhLgX4Rz/99JNeeOEFHTp0SA6HI9zlAGPi9/vldru1fft2SVJOTo56enr0/vvvy+PxhLm6yMEdPcBC7rrrLkVFReny5csB2y9fvvyPk/1vZ5vA9UI9ztatW6cDBw7oyJEjmj59+i23B/xVKMbv999/rytXrujBBx+U3W6X3W7X0aNH9fbbb8tut2t0dDQYpSPCheram5qaqqysrIBtTqdT/f39424TY0fQAywkJiZGubm56uzsNLf5/X51dnZq0aJFE6ZN4HqhGmeGYWjdunXau3evDh8+rPT09GCUCwQIxfgtLCxUd3e3vF6vubjdbj3zzDPyer2KiooKVvmIYKG69ubn59/wKZuzZ89q5syZ424TY8ejm4DFrF+/Xh6PR263W3l5eXrrrbc0NDSkVatWSZIqKiqUlpZmzvkYHh5Wb2+v+e+LFy/K6/UqPj5ec+bMuak2gWAIxditqqpSW1ub9u3bp4SEBHPOSVJSkmJjY8PQS1hVsMdvQkLCDXNJ4+LilJyczBxTBFUorr0vvviiFi9erO3bt6u8vFzffPONGhsb1djYGJ5ORqpwv/YTQPA1NDQY9957rxETE2Pk5eUZJ06cMPcVFBQYHo/HXP/xxx8NSTcsBQUFN90mECzBHrt/t1+S0dzcfPs6hYgRimvvX/F5BYRKKMbu/v37jfnz5xt33HGHMW/ePKOxsfE29QbX2AzDMG5XqAQAAAAAhB5z9AAAAADAYgh6AAAAAGAxBD0AAAAAsBiCHgAAAABYDEEPAAAAACyGoAcAAAAAFkPQAwAAAACLIegBAAAAgMUQ9AAA+Bd65JFHVFNTE+4yAAATFEEPAAAAACyGoAcAAAAAFkPQAwBEtE8//VQul0uxsbFKTk5WUVGRhoaGJEk7d+6U0+mUw+HQvHnz9O677wb89uuvv1Z2drYcDofcbrfa29tls9nk9XolSS0tLbrzzjsDfnPtmGu2bNmi7OxsffTRR5o1a5aSkpL05JNPanBw0DxmaGhIFRUVio+PV2pqqurr62/oh8/nU0VFhaZMmaLJkyeruLhY586dM/dfuHBBpaWlmjJliuLi4nT//fero6PjVk8fAGCCsoe7AAAAwuXSpUt66qmn9Prrr+vxxx/X4OCgvvzySxmGodbWVtXV1emdd95RTk6OTp06pcrKSsXFxcnj8ej3339XaWmpSkpK1NbWpgsXLox7ztz58+fV3t6uAwcOyOfzqby8XK+99pq2bdsmSdqwYYOOHj2qffv2KSUlRZs3b9bJkyeVnZ1ttvHss8/q3Llz+uyzz5SYmKiNGzeqpKREvb29io6OVlVVlYaHh3Xs2DHFxcWpt7dX8fHxQTiLAICJiKAHAIhYly5d0sjIiJYvX66ZM2dKklwulyTplVdeUX19vZYvXy5JSk9PV29vr3bs2CGPx6O2tjbZbDY1NTXJ4XAoKytLFy9eVGVl5Zjr8Pv9amlpUUJCgiRp5cqV6uzs1LZt2/THH3/ogw8+0O7du1VYWChJ2rVrl6ZPn27+/lrAO378uBYvXixJam1t1YwZM9Te3q6ysjL19/drxYoVZv9mz549zrMGAPg3IOgBACLWAw88oMLCQrlcLj366KNasmSJnnjiCcXExOj8+fNavXp1QHAbGRlRUlKSJOnMmTNasGCBHA6HuT8vL29cdcyaNcsMeZKUmpqqK1euSPrv3b7h4WEtXLjQ3D916lRlZmaa66dPn5bdbg84Jjk5WZmZmTp9+rQkqbq6WmvXrtXBgwdVVFSkFStWaMGCBeOqFwAw8TFHDwAQsaKionTo0CF9/vnnysrKUkNDgzIzM9XT0yNJampqktfrNZeenh6dOHHiptufNGmSDMMI2Pbnn3/ecFx0dHTAus1mk9/vH0eP/r81a9aor69PK1euVHd3t9xutxoaGoL6fwAAJg6CHgAgotlsNuXn52vr1q06deqUYmJidPz4cd1zzz3q6+vTnDlzApb09HRJUmZmprq7u3X16lWzrW+//Tag7bvvvluDg4Pmy10kmS9quVn33XefoqOj1dXVZW7z+Xw6e/asue50OjUyMhJwzK+//qozZ84oKyvL3DZjxgw9//zz2rNnj1566SU1NTWNqRYAwL8Hj24CACJWV1eXOjs7tWTJEqWkpKirq0s///yznE6ntm7dqurqaiUlJWnp0qW6evWqvvvuO/l8Pq1fv15PP/20amtr9dxzz2nTpk3q7+/Xm2++KUnmWzUXLlyoyZMna/PmzaqurlZXV5daWlrGVGN8fLxWr16tDRs2KDk5WSkpKaqtrdWkSf/7W+3cuXO1bNkyVVZWaseOHUpISNCmTZuUlpamZcuWSZJqampUXFysjIwM+Xw+HTlyRE6nMzgnEgAw4XBHDwAQsRITE3Xs2DGVlJQoIyNDL7/8surr61VcXKw1a9Zo586dam5ulsvlUkFBgVpaWsw7eomJidq/f7+8Xq+ys7NVW1ururo6STLn7U2dOlW7d+9WR0eHXC6XPv74Y23ZsmXMdb7xxht6+OGHVVpaqqKiIj300EPKzc0NOKa5uVm5ubl67LHHtGjRIhmGoY6ODvOx0NHRUVVVVcnpdGrp0qXKyMi44XMRAADrsBnXTx4AAADj0traqlWrVum3335TbGxsuMsBAEQwHt0EAGCcPvzwQ82ePVtpaWn64YcftHHjRpWXlxPyAABhR9ADAGCcBgYGVFdXp4GBAaWmpqqsrMz8yDkAAOHEo5sAAAAAYDG8jAUAAAAALIagBwAAAAAWQ9ADAAAAAIsh6AEAAACAxRD0AAAAAMBiCHoAAAAAYDEEPQAAAACwGIIeAAAAAFgMQQ8AAAAALOY/eQIEzQQwZSoAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}