bug_bunny 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 877e5f5961f53d587b2bc67e6a8c39d247b996e1272ddad076236633df65f267
4
- data.tar.gz: 6928594ae30624f1dccd2841594bee4b0faed40ee0cd238a69387399559a757b
3
+ metadata.gz: 6ac482578cfbb87414309cc03502be5f11c4ee4b0e799338bfeb9bfcf65488f5
4
+ data.tar.gz: a4a9e64ac077bd6564790a5de762c804d38c65623f2c7cbc839217bb8f98bd25
5
5
  SHA512:
6
- metadata.gz: fea6d7206396aae6ff2b35277e651ed944bee590f74200a392f2fb962af8757f56057b6d637434a91603d272b59b6215b459b330e98999d37f72d1c2ad6af763
7
- data.tar.gz: b9d531b234ec094ab7127ea5921506d92a32e57961463904e6a62e158ce8a8e3020368ff1c6006cb30ae62a5beea942f3c004ce33e91362c910de2e87165aa6e
6
+ metadata.gz: e33cbdca7040793e46116fdf744d3f89bdd71a4b278a12aaa85f3845c6aba1ea86dbfded00c3108b60e04759dea07f52a7765d430fcc3bbc3ff2090b01f38c9a
7
+ data.tar.gz: be41989086784220c62b3e9ee4909f064956c3bf7dcaade12473d039c16743732a75f1d60844ffc22fcbe0afb36c518e14dbb74713cc0ef7007f9658d2be13d1
data/AGENTS.md CHANGED
@@ -12,7 +12,7 @@ proyecto y convenciones de equipo, ver `CLAUDE.md`. Para la entrada humana, ver
12
12
 
13
13
  | capa | artefacto | estado | qué responde |
14
14
  |---|---|---|---|
15
- | Comportamiento | `docs/behavior/behavior.md` | completo (6 flujos) | secuencias de publish/RPC/consume/confirms, contrato de error-wrapping |
15
+ | Comportamiento | `docs/behavior/behavior.md` | completo (7 flujos) | secuencias de publish/RPC/consume/confirms, contrato de error-wrapping |
16
16
  | Glosario | `docs/glossary/glossary.md` | parcial (acreta por PR) | término de negocio → binding físico en `lib/` |
17
17
  | Errores | `docs/errors/errors.md` | completo (§a/b/d estructura + §c política inferida) | jerarquía de excepciones públicas, mapeo `status→excepción`, shape del payload, política retry |
18
18
  | Configuración | `docs/config/configuracion.md` | completo (estructura + enrich §f/g/h) | opciones de `Configuration`, defaults, failure-mode/threading, inyecciones del `Railtie`, ENV sugeridas |
data/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.2.0] - 2026-09-29
4
+
5
+ ### Nuevas funcionalidades
6
+ - **`Consumer.drain` / `Consumer#drain` — consumir hasta vaciar la cola y retornar (#65):** el modo para correr un consumidor **como job** (Sidekiq, un Job de k8s) en vez de como proceso eterno. Se suscribe respetando `channel_prefetch` y retorna cuando pasan `drain_idle_timeout` segundos sin entregas y sin nada en proceso; con la cola vacía retorna `0` al instante. Devuelve cuántos mensajes procesó (incluye los rechazados: también salieron de la cola). Un mensaje que llega dentro de la ventana se procesa en esa vuelta; los posteriores, en la próxima. No tiene loop de reconexión ni health check (el reintento es del framework del job). **La conexión es del llamador:** `drain` cierra su canal, no la conexión — si la creás por corrida, cerrala (`ensure connection.close`), o cada corrida deja una abierta. **Con un flujo sostenido más rápido que `drain_idle_timeout`, no retorna:** acotalo desde el job. — @gedera
7
+ - **`drain_idle_timeout` (default `5`) y `drain_poll_interval` (default `0.1`) en `Configuration`**, validados en `validate!`. — @gedera
8
+
9
+ ### ⚠️ Cambio de conducta — `subscribe` y `drain`
10
+ - **Una entrega cuyo middleware —o `handle_fatal_error`— levanta antes del ack ahora se rechaza sin requeue** y se loguea `consumer.delivery_failed` (#65). Antes quedaba **sin ack ni reject**: con `channel_prefetch = 1` ocupaba el único lugar de prefetch y el consumidor dejaba de recibir, sin error. Es lo mismo que `process_message` ya hacía con sus propios errores. Si el error llega **después** del ack, sólo se loguea (rechazar un tag ya confirmado cierra el canal). **A mirar si consumís:** si un middleware tuyo levantaba a propósito para que el mensaje se reintentara, ahora el mensaje **se pierde** — el reintento tiene que ser tuyo. — @gedera
11
+
12
+ ### Documentación
13
+ - `subscribe(block: false)` **no** es un modo "drenar": retorna al instante y el `ensure shutdown` cierra el canal, así que no consume nada (medido en #65). Queda documentado en el README; no se cambia en este release. — @gedera
14
+
15
+ ### Tests
16
+ - `configuration_spec` restaura la configuración original en vez de dejar una con defaults: los specs de integración que corrían después se conectaban como `guest` y quedaban *pending* como "RabbitMQ no disponible" aun con broker, según el seed (#65). — @gedera
17
+
18
+ ## [5.1.1] - 2026-07-31
19
+
20
+ ### Correcciones
21
+ - **`safe_log` redacta credenciales embebidas en el VALOR, no solo por nombre de clave (#61):** el filtro por-clave solo ve el NOMBRE, así que una credencial en texto libre pasaba entera al log. El caso canónico es el `message` de una excepción inesperada, que llega como `reason=`/`error_message=` —nombres no sensibles— y puede arrastrar `Authorization: "Bearer …"`. Se agrega `Observability.redact_value`, aplicada a todo valor no numérico que `safe_log` serializa, con tres reglas: esquemas de auth HTTP (`Bearer`/`Basic`), key sensible dentro del texto (conserva el nombre de la key y filtra solo el valor) y credenciales en URL (`user:pass@host`). Cubre las variantes con separador y camelCase (`access_token=`, `user_password=`, `accessToken=`), igual que el matching por substring de `sensitive_key?`. No-breaking: no cambia firmas ni el formato `k=v`, y un log sin credenciales sale idéntico. Riesgo asumido: sobre-redacción de contenido diagnóstico anclado a marcadores de credencial (un `token=missing` sale filtrado). — @gedera
22
+ - **Un valor `Hash` sigue siendo JSON parseable después de redactar:** se agrega `Observability.redact_structure`, que redacta la estructura **antes** de serializar (recorre `Hash` anidado y `Array`) en vez de aplicar el regex sobre el JSON ya serializado, que colapsaba el par `"token": "abc"` y dejaba el campo sin parsear — el secreto desaparecía, pero quien consume el log perdía el objeto entero. De paso, las keys internas del `Hash` ahora también pasan por `sensitive_key?`. — @gedera
23
+
24
+ ### Documentación
25
+ - La lista de claves sensibles **deja de replicarse**: el docblock de `Error#raw_response` y `docs/errors/` apuntan a `Observability::SENSITIVE_KEYS` como canónica, en vez de duplicarla. La copia venía desincronizada (incluía `pass` bare, que está excluido a propósito para no filtrar `passport_number`, y le faltaban `authorization`, `credential`, `private_key`, `csrf`, `session_id`). `docs/glossary/`, README y `skill/` describen ahora las dos capas de redacción. — @Pslp
26
+
3
27
  ## [5.1.0] - 2026-07-22
4
28
 
5
29
  ### Nuevas funcionalidades
data/CLAUDE.md CHANGED
@@ -15,7 +15,7 @@ BugBunny es una gema Ruby que implementa una capa de enrutamiento RESTful sobre
15
15
  `dev-compose`. Verificación humana antes de commitear.
16
16
  - **Estado actual:**
17
17
  - `docs/data` = n/a (gema sin DB, declarado solo en índice).
18
- - `docs/behavior` completo (6 flujos, backfill on-demand).
18
+ - `docs/behavior` completo (7 flujos, backfill on-demand).
19
19
  - `docs/glossary` parcial (acreta por PR).
20
20
  - `docs/errors` (RFC-020) completo: §a/§b/§d (estructura) + §c política
21
21
  (enrich, **inferida** de HTTP/AMQP, verificación humana pendiente).
data/README.md CHANGED
@@ -66,6 +66,28 @@ BugBunny::Consumer.subscribe(
66
66
  )
67
67
  ```
68
68
 
69
+ To run a consumer **as a job** (Sidekiq, a k8s Job) instead of a long-lived process, use `drain`: it consumes until the queue goes quiet and returns how many messages it processed. With an empty queue it returns `0` right away.
70
+
71
+ ```ruby
72
+ connection = BugBunny.create_connection
73
+ begin
74
+ processed_count = BugBunny::Consumer.drain(
75
+ connection: connection,
76
+ queue_name: 'inventory_queue',
77
+ exchange_name: 'inventory',
78
+ routing_key: 'nodes'
79
+ )
80
+ ensure
81
+ connection.close # drain closes its channel, not the connection: the connection is yours
82
+ end
83
+ ```
84
+
85
+ - **The connection belongs to the caller.** `drain` closes its channel when it returns, not the connection, because it may be shared (a pool, the publisher's). If you create one per job run, close it — otherwise every run leaks a connection. Reusing the process connection works too.
86
+ - **A delivery that fails leaves the queue.** If a middleware raises before the ack, the message is rejected without requeue (same as `process_message` errors), so it does not block the prefetch.
87
+ - **With a sustained flow, `drain` does not return.** If messages arrive faster than `drain_idle_timeout`, the idle window never closes. Bound it from outside (the Sidekiq job timeout, `activeDeadlineSeconds` in k8s).
88
+
89
+ > `subscribe(block: false)` is **not** this mode: it returns immediately and closes the channel, so it consumes nothing.
90
+
69
91
  ### Service A — Producer
70
92
 
71
93
  ```ruby
@@ -146,6 +168,11 @@ BugBunny.configure do |config|
146
168
  # Health check file for Kubernetes / Docker Swarm liveness probes
147
169
  config.health_check_file = '/tmp/bug_bunny_health'
148
170
 
171
+ # Consumer.drain — seconds without deliveries before the queue counts as empty (default: 5),
172
+ # and how often that is checked (default: 0.1)
173
+ config.drain_idle_timeout = 5
174
+ config.drain_poll_interval = 0.1
175
+
149
176
  # Publisher Confirms — fail-loud defaults (both flags default to true).
150
177
  # Set to false to restore legacy log-only behavior.
151
178
  config.nack_raise = true # broker NACK → raise BugBunny::PublishNacked
@@ -365,7 +392,10 @@ BugBunny measures and emits durations automatically — **there is no need to wr
365
392
  | `consumer.message_processed` | `duration_s` | Message processing (router + controller + reply). |
366
393
  | `consumer.execution_error` | `duration_s` | Elapsed time until the error. |
367
394
 
368
- Sensitive keys (`password`, `token`, `secret`, `api_key`, `authorization`, etc.) are automatically filtered to `[FILTERED]` across all log output.
395
+ Credentials are redacted to `[FILTERED]` across all log output, in two layers:
396
+
397
+ - **By key name** — sensitive keys (`password`, `token`, `secret`, `api_key`, `authorization`, etc.) are matched as substrings, so variants like `user_password` or `accessToken` are covered too.
398
+ - **By value content** — a credential embedded in free text is redacted even when the key name is *not* sensitive. The canonical case: an unexpected exception whose `message` carries `Authorization: "Bearer …"` and reaches the log as `reason=`. Covers HTTP auth schemes, a sensitive key inside the text, and credentials in a URL. `Hash` values are redacted **before** being serialized, so the field stays valid JSON.
369
399
 
370
400
  ---
371
401
 
@@ -429,20 +459,31 @@ end
429
459
 
430
460
  Artefactos de detalle (modelo `dev-*`, RFC-001). El README indexa; no duplica.
431
461
 
462
+ Anclado a `v5.2.0`.
463
+
432
464
  | Capa | Artefacto | Estado |
433
465
  |---|---|---|
434
- | Datos | — | n/a — gema sin DB (sin schema/models) |
435
466
  | Glosario | [docs/glossary/glossary.md](docs/glossary/glossary.md) | parcial, acreta por PR |
436
467
  | Comportamiento | [docs/behavior/behavior.md](docs/behavior/behavior.md) | completa — 6 flujos (backfill on-demand) |
437
- | Operaciones / Interfaz / Topología | — | F2 no implementado (dev-structure) — ver nota |
468
+ | Configuración | [docs/config/configuracion.md](docs/config/configuracion.md) | §a-§e/§i estructura + §f/§g/§h enrich — completa |
469
+ | Dependencias consumidas | [docs/consumed/rabbitmq.md](docs/consumed/rabbitmq.md) | §a/§b/§d estructura + §c/§e enrich |
470
+ | Errores | [docs/errors/errors.md](docs/errors/errors.md) | §a/§b/§d completas; §c política inferida (verificación humana pendiente) |
471
+ | Test | [docs/test/testing.md](docs/test/testing.md) | §a-§h — 16 unit / 7 integration |
472
+ | Release | [docs/release/release.md](docs/release/release.md) | completa (régimen gema: build→publish; §e/f/g n/a) |
473
+ | Datos | — | n/a — gema sin DB (sin schema/models) |
474
+ | Eventos | — | n/a — la gema **es** el transporte; no declara catálogo de eventos de dominio propio |
475
+ | Seguridad | — | n/a — sin authn/authz propias; el guard anti-RCE (403) se documenta en `docs/errors/` |
476
+ | Operaciones / Interfaz / Topología | — | pendiente — ver nota |
438
477
 
439
- **Coexistencia transitoria con destino pendiente (RFC-008 §2 — interim de migración):** mientras la capa de detalle destino (operaciones/interfaz/topología) esté declarada pero **no implementada** (dev-structure F1, F2 del plan), permanecen embebidos/cruzados, bajo el interim normado:
478
+ **Coexistencia transitoria con destino pendiente (RFC-008 §2 — interim de migración):** mientras las capas `docs/api/` (operaciones), `docs/interface/` y `docs/topology/` no estén generadas para este repo, el contrato que les correspondería permanece embebido:
440
479
 
441
- - **En este README:** el contrato (jerarquía de excepciones, API de configuración, modos de entrega).
480
+ - **En este README:** la jerarquía de excepciones, la API de configuración y los modos de entrega.
442
481
  - **En `skill/SKILL.md`:** además el diagrama de arquitectura (flujo RPC).
443
482
  - **Guías how-to** (`skill/references/*.md`, pre-estándar): el README las enlaza pese a la regla "no referenciar `skill/` desde el README" — destino futuro `docs/howto/`.
444
483
 
445
- Por RFC-008 §2: no se fabrica la capa, no se borra contrato sin destino, no se duplica; migra cuando F2 entregue, mismo PR. Estado transitorio declarado, no excepción permanente. Origen del gap (resuelto, normado): [sequre/ai_knowledge#95](https://github.com/sequre/ai_knowledge/issues/95).
484
+ Por RFC-008 §2: no se fabrica la capa, no se borra contrato sin destino, no se duplica; migra cuando se generen, mismo PR. Estado transitorio declarado, no excepción permanente. Origen del gap (resuelto, normado): [sequre/ai_knowledge#95](https://github.com/sequre/ai_knowledge/issues/95).
485
+
486
+ > **Nota de alcance (2026-07-31):** la justificación original de este interim era que el generador no implementaba esas capas. Ya las implementa, así que el pendiente es de **este repo**, no de tooling — en particular `docs/interface/` (RFC-004), que es donde correspondería registrar la API pública de la gema. Queda como trabajo propio, fuera del alcance de este release.
446
487
 
447
488
  How-to (pre-estándar):
448
489
  [Routing](skill/references/routing.md) ·
@@ -1,6 +1,6 @@
1
1
  # Comportamiento — bug_bunny
2
2
 
3
- > meta: artefacto `comportamiento` · RFC-007 (cadencia incremental default / completo on-demand) · generado dev-enrich 1.3.0 (backfill on-demand) · anclado a `94de2b4` · cobertura: completa (6 flujos) · verificado por humano 2026-05-18 (base) · 2026-05-26 (refresco scoped: contrato de error wrapping post-#49)
3
+ > meta: artefacto `comportamiento` · RFC-007 (cadencia incremental default / completo on-demand) · generado dev-enrich 1.3.0 (backfill on-demand) · anclado a `35b075a` (squash de #65 en `main`) · cobertura: completa (7 flujos) · verificado por humano 2026-05-18 (base) · 2026-05-26 (refresco scoped: contrato de error wrapping post-#49) — incremento 2026-09-28 (#64, flujo `drain` + re-mapeo de `file:line`): escrito por el agente, **sin verificación humana todavía**
4
4
 
5
5
  ## 1. Resumen
6
6
 
@@ -10,14 +10,15 @@ Flujos de ejecución de la gema. Generado en **modo completo on-demand** (RFC-00
10
10
 
11
11
  | Flujo | Estado | Anclaje principal |
12
12
  |---|---|---|
13
- | RPC síncrono | **documentado** | `producer.rb:103-134`, `consumer.rb:152-293` |
13
+ | RPC síncrono | **documentado** | `producer.rb:103-134`, `consumer.rb:305-446` |
14
14
  | Fire-and-forget | **documentado** | `producer.rb:47-51,146-161` |
15
15
  | Confirmed + basic.return bridge | **documentado** | `producer.rb:72-93,299-333`, `session.rb:204-250` |
16
- | Consumer subscribe loop + reconnect + health | **documentado** | `consumer.rb:66-127,340-361` |
17
- | Error handling / RemoteError | **documentado** | `consumer.rb:320-329`, `remote_error.rb`, `raise_error.rb:32-65` |
16
+ | Consumer subscribe loop + reconnect + health | **documentado** | `consumer.rb:76-111,493-514` |
17
+ | Consumer drain (drenar y salir) | **documentado** | `consumer.rb:158-176,271-290`, `drain_tracker.rb:28-47` |
18
+ | Error handling / RemoteError | **documentado** | `consumer.rb:473-482`, `remote_error.rb`, `raise_error.rb:32-65` |
18
19
  | Client middleware stack (onion) | **documentado** | `middleware/stack.rb:43-47`, `base.rb:35-43` |
19
20
 
20
- Cobertura completa a `a5cdb10`. Acreta incremental en cada PR que toque un flujo (default RFC-007). Ausencia futura ≠ inexistencia.
21
+ Cobertura completa a `35b075a`. Acreta incremental en cada PR que toque un flujo (default RFC-007). Ausencia futura ≠ inexistencia.
21
22
 
22
23
  ## 2. Cuerpo
23
24
 
@@ -50,7 +51,7 @@ sequenceDiagram
50
51
  P-->>CL: response hidratada
51
52
  Note over P: bloqueo L122 · timeout → RequestTimeout L124
52
53
  ```
53
- Contexto: `client.rb:97-101` → `producer.rb:103-134` (bloqueo L122) → reply listener `producer.rb:405-424` → `consumer.rb:247,272-293`.
54
+ Contexto: `client.rb:97-101` → `producer.rb:103-134` (bloqueo L122) → reply listener `producer.rb:405-424` → `consumer.rb:400,425-446`.
54
55
 
55
56
  ### Flujo: Fire-and-forget
56
57
  Publica y retorna `{ 'status' => 202 }` sin esperar broker ni consumer.
@@ -116,7 +117,34 @@ sequenceDiagram
116
117
  Note over C: rescue StandardError → attempt++ · backoff min(nri*2^(n-1), max) · sleep · retry (redeclara)
117
118
  Note over C: max_reconnect_attempts alcanzado → raise (fatal) · ensure → shutdown
118
119
  ```
119
- Contexto: `consumer.rb:66-127` (retry L106-124), `consumer.rb:340-361` (health). **Honestidad:** health check es thread aparte (TimerTask); no es parte del manejo de error del loop — se acoplan sólo vía cierre de session. Marcado, no fingido como un único flujo.
120
+ Contexto: `consumer.rb:76-111` (retry L90-108), `consumer.rb:493-514` (health). **Honestidad:** health check es thread aparte (TimerTask); no es parte del manejo de error del loop — se acoplan sólo vía cierre de session. Marcado, no fingido como un único flujo.
121
+
122
+ ### Flujo: Consumer drain (drenar y salir)
123
+ Consume hasta que la cola queda quieta y retorna la cantidad procesada: el modo para correr un consumidor **como job** (#64). A diferencia del loop de `subscribe`, **no** tiene loop de reconexión ni health check (Bunny sí recupera la conexión por su cuenta con `automatically_recover`).
124
+
125
+ ```mermaid
126
+ sequenceDiagram
127
+ participant J as Job (llamador)
128
+ participant C as Consumer
129
+ participant T as DrainTracker
130
+ participant BR as RabbitMQ
131
+ J->>C: Consumer.drain(connection:, queue_name:, …)
132
+ C->>BR: exchange/queue declare · bind · message_count
133
+ alt message_count == 0
134
+ C-->>J: 0 (sin esperar) · ensure → shutdown
135
+ else hay mensajes
136
+ C->>BR: subscribe(manual_ack, block:false) — respeta channel_prefetch
137
+ loop por entrega (work pool de Bunny)
138
+ BR->>C: deliver → T.track { middlewares → process_message → ack/reject }
139
+ end
140
+ loop cada drain_poll_interval
141
+ C->>T: idle?(drain_idle_timeout) — nada en proceso y sin actividad en la ventana
142
+ end
143
+ C->>BR: cancel · espera a que T no esté busy
144
+ C-->>J: T.processed · ensure → shutdown
145
+ end
146
+ ```
147
+ Contexto: `consumer.rb:158-176` (`drain`), `consumer.rb:271-290` (`consume_until_idle`), `drain_tracker.rb:28-47`. **Mensajes que llegan mientras drena:** entran en esta vuelta si llegan antes de que venza la ventana; los posteriores quedan para la próxima corrida. Una entrega ya recibida al cancelar se procesa antes de volver (Bunny drena su work pool al cancelar — leído de la fuente de Bunny 2.24.0, no medido); si igual no se ack-eara, **vuelve a la cola** (at-least-once). El conteo devuelto incluye los rechazados. **Una entrega que falla fuera del rescue de `process_message`** (un middleware, `handle_fatal_error`) se rechaza sin requeue en `handle_delivery` y se loguea `consumer.delivery_failed`; antes quedaba sin ack, trababa el prefetch y `drain` volvía "con éxito" con la cola llena (medido en el review de #65). **Con un flujo sostenido no retorna:** si los mensajes llegan más seguido que `drain_idle_timeout` la ventana no vence — la duración la decide el productor; se acota desde afuera (timeout del job). **La conexión es del llamador:** `drain` cierra el canal, no la conexión. **Distinto de `subscribe(block: false)`:** ese modo retorna al instante y el `ensure shutdown` cierra el canal, así que no consume nada (medido en #64).
120
148
 
121
149
  ### Flujo: Error handling / RemoteError
122
150
  Excepción no manejada en controller → serializada (clase/mensaje/backtrace[0..25]) → reply 500 → reconstruida client-side por `Middleware::RaiseError`.
@@ -137,7 +165,7 @@ sequenceDiagram
137
165
  RE->>RE: status 500..599 + bug_bunny_exception
138
166
  RE-->>CL: raise BugBunny::RemoteError(class,message,backtrace)
139
167
  ```
140
- Contexto: `controller.rb:200-234`, `consumer.rb:320-329`, `remote_error.rb:29-48`, `raise_error.rb:32-65`. **Honestidad:** backtrace truncado a 25 líneas en serialize; si el controller nunca llega a responder, el cliente expira por timeout en vez de recibir el error (no hay path de error garantizado).
168
+ Contexto: `controller.rb:200-234`, `consumer.rb:473-482`, `remote_error.rb:29-48`, `raise_error.rb:32-65`. **Honestidad:** backtrace truncado a 25 líneas en serialize; si el controller nunca llega a responder, el cliente expira por timeout en vez de recibir el error (no hay path de error garantizado).
141
169
 
142
170
  ### Flujo: Client middleware stack (onion)
143
171
  `Stack#build` hace `@middlewares.reverse.inject(final_action)` → el **primer `use` queda como el más externo** (corre `on_request` primero, `on_complete` último). Documentado en `stack.rb:37-39`; sigue la convención Rack/Faraday (primer registrado = capa externa).
@@ -167,7 +195,7 @@ Contexto: `middleware/stack.rb:31-47` (build L43-47, `reverse.inject`), `base.rb
167
195
  |---|---|---|
168
196
  | Secuencias y `file:line` extraídos por el LLM del código a `a5cdb10`; 2ª pasada LLM corrigió 3 discrepancias (flujo middleware invertido, timeout RPC `producer.rb:124`, timeout confirmed `producer.rb:214-215`) | confirmed | **verificado por humano 2026-05-18** (invariante RFC-001 §3.3 satisfecho) |
169
197
  | Orden wire `basic.return → basic.ack` garantizado por AMQP; `RETURN_RACE_WINDOW_S` cubre GVL | declared (código) / inferred (garantía AMQP) | confirmar lectura de `producer.rb:299-308` + spec AMQP |
170
- | Health check acoplado flojo al loop vía cierre de session | inferred | confirmar `consumer.rb:340-361` vs `106-124` |
198
+ | Health check acoplado flojo al loop vía cierre de session | inferred | confirmar `consumer.rb:493-514` vs `90-108` |
171
199
  | Frontera de error del Client: cualquier `Bunny::Exception` durante `@pool.with` (try_create o in-flight) → `BugBunny::CommunicationError` con `.cause` preservada (`client.rb:155-167`). `Producer#confirmed` rescate estrechado a `Bunny::Exception` (`producer.rb:87-90`) — no traga bugs Ruby. `BugBunny.create_connection` también envuelve (`bug_bunny.rb:96-99`). | declared (código post-#49) | confirmar lectura de `client.rb:155-167`, `producer.rb:87-90`, `bug_bunny.rb:96-99` + specs `communication_error_wrapping_spec.rb` |
172
200
 
173
201
  ## 4. Cobertura y fronteras
@@ -1,12 +1,14 @@
1
1
  # Configuración — bug_bunny
2
2
 
3
3
  > meta: artefacto configuración · RFC-012 · generado `arch-structure` (inventario
4
- > §a-§e/§i) + `arch-enrich` (§f/§g/§h/§j) · anclado a `24ea397`,
4
+ > §a-§e/§i) + `arch-enrich` (§f/§g/§h/§j) · anclado a `35b075a` (squash de #65 en `main`),
5
5
  > `lib/bug_bunny/configuration.rb`, `lib/bug_bunny.rb`, `lib/bug_bunny/railtie.rb`,
6
6
  > `lib/bug_bunny/consumer.rb`,
7
7
  > `lib/generators/bug_bunny/install/templates/initializer.rb` · fecha 2026-06-30
8
8
  > · cobertura: §a-§e/§i (estructura) + §f/§g/§h (enrich, anclado a YARD) completas;
9
- > §j n/a.
9
+ > §j n/a. Incremento 2026-09-28 (#64): `drain_idle_timeout`/`drain_poll_interval`,
10
+ > anclados a `configuration.rb:37-38,99-105,273-275` y `consumer.rb:271-290`; todas las
11
+ > citas `file:line` re-mapeadas al árbol de `35b075a` (el refactor de #64 las corrió).
10
12
 
11
13
  ## 1. Resumen
12
14
 
@@ -20,13 +22,15 @@ en `lib/`**; el `ENV.fetch` vive en el **template** que sugiere al consumidor
20
22
 
21
23
  ### a. Hecho verificable
22
24
 
23
- - **Total opciones (`Configuration`):** 29 (attr_accessor/reader).
24
- - **Validadas requeridas (`VALIDATIONS`, `configuration.rb:25-37`):** 5 (`host`,
25
+ - **Total opciones (`Configuration`):** 32 (31 `attr_accessor` + 1 `attr_reader`; medido
26
+ 2026-09-28 — el `29` anterior ya estaba desfasado del código antes de #64).
27
+ - **Validadas requeridas (`VALIDATIONS`, `configuration.rb:25-39`):** 5 (`host`,
25
28
  `port`, `username`, `password`, `vhost` — `required: true`; igual tienen
26
29
  default, `validate!` exige no-vacío).
27
- - **Con default:** 29 (todas; defaults en `initialize`/`init_callback_defaults`).
28
- - **Con rango validado:** 7 (`port`, `heartbeat`, `connection_timeout`,
29
- `read_timeout`, `write_timeout`, `rpc_timeout`, `channel_prefetch`).
30
+ - **Con default:** 32 (todas; defaults en `initialize`/`init_callback_defaults`/`init_drain_defaults`).
31
+ - **Con rango validado:** 9 (`port`, `heartbeat`, `connection_timeout`,
32
+ `read_timeout`, `write_timeout`, `rpc_timeout`, `channel_prefetch`,
33
+ `drain_idle_timeout`, `drain_poll_interval`).
30
34
  - **Secretas (por nombre):** 1 (`password`).
31
35
  - **ENV leídas por la gema en `lib/`:** 0 (config 100% por code-default + bloque).
32
36
 
@@ -37,36 +41,38 @@ Origen `code-default` salvo nota. Consumidor = `configuration.rb` (default en
37
41
 
38
42
  | nombre | tipo | requerida | default | origen | consumidor | secret? |
39
43
  |---|---|---|---|---|---|---|
40
- | `host` | String | sí (validate!) | `'127.0.0.1'` | code-default | `configuration.rb:182` | no |
41
- | `port` | Integer | sí (validate!, 1..65535) | `5672` | code-default | `:183` | no |
42
- | `username` | String | sí (validate!) | `'guest'` | code-default | `:184` | no |
43
- | `password` | String | sí (validate!) | `'guest'` | code-default | `:185` | **sí** |
44
- | `vhost` | String | sí (validate!) | `'/'` | code-default | `:186` | no |
45
- | `logger` | Logger | no | `Logger.new($stdout)` INFO | code-default | `:188` | no |
46
- | `bunny_logger` | Logger | no | `Logger.new($stdout)` WARN | code-default | `:191` | no |
47
- | `automatically_recover` | Boolean | no | `true` | code-default | `:193` | no |
48
- | `network_recovery_interval` | Integer | no | `5` | code-default | `:194` | no |
49
- | `max_reconnect_attempts` | Integer/nil | no | `nil` (reintenta ∞) | code-default | `:195` | no |
50
- | `max_reconnect_interval` | Integer | no | `60` | code-default | `:196` | no |
51
- | `connection_timeout` | Integer | no (1..300) | `10` | code-default | `:197` | no |
52
- | `read_timeout` | Integer | no (1..300) | `30` | code-default | `:198` | no |
53
- | `write_timeout` | Integer | no (1..300) | `30` | code-default | `:199` | no |
54
- | `heartbeat` | Integer | no (0..3600) | `15` | code-default | `:200` | no |
55
- | `continuation_timeout` | Integer (ms) | no | `15000` | code-default | `:201` | no |
56
- | `channel_prefetch` | Integer | no (1..10000) | `1` | code-default | `:202` | no |
57
- | `rpc_timeout` | Integer | no (1..3600) | `10` | code-default | `:203` | no |
58
- | `health_check_interval` | Integer | no | `60` | code-default | `:204` | no |
59
- | `health_check_file` | String/nil | no | `nil` (desactivado) | code-default | `:207` | no |
60
- | `controller_namespace` | String | no | `'BugBunny::Controllers'` | code-default | `:210` | no |
61
- | `log_tags` | Array | no | `[:uuid]` | code-default | `:212` | no |
62
- | `exchange_options` | Hash | no | `{}` | code-default | `:215` | no |
63
- | `queue_options` | Hash | no | `{}` | code-default | `:216` | no |
64
- | `consumer_middlewares` | Stack (attr_reader) | no | `Stack.new` | code-default | `:218` | no |
65
- | `rpc_reply_headers` | Proc/nil | no | `nil` | code-default | `:251` | no |
66
- | `on_rpc_reply` | Proc/nil | no | `nil` | code-default | `:252` | no |
67
- | `on_return` | Proc/nil | no | `nil` | code-default | `:253` | no |
68
- | `nack_raise` | Boolean | no | `true` | code-default | `:254` | no |
69
- | `return_raise` | Boolean | no | `true` | code-default | `:255` | no |
44
+ | `host` | String | sí (validate!) | `'127.0.0.1'` | code-default | `configuration.rb:192` | no |
45
+ | `port` | Integer | sí (validate!, 1..65535) | `5672` | code-default | `:193` | no |
46
+ | `username` | String | sí (validate!) | `'guest'` | code-default | `:194` | no |
47
+ | `password` | String | sí (validate!) | `'guest'` | code-default | `:195` | **sí** |
48
+ | `vhost` | String | sí (validate!) | `'/'` | code-default | `:196` | no |
49
+ | `logger` | Logger | no | `Logger.new($stdout)` INFO | code-default | `:198` | no |
50
+ | `bunny_logger` | Logger | no | `Logger.new($stdout)` WARN | code-default | `:201` | no |
51
+ | `automatically_recover` | Boolean | no | `true` | code-default | `:203` | no |
52
+ | `network_recovery_interval` | Integer | no | `5` | code-default | `:204` | no |
53
+ | `max_reconnect_attempts` | Integer/nil | no | `nil` (reintenta ∞) | code-default | `:205` | no |
54
+ | `max_reconnect_interval` | Integer | no | `60` | code-default | `:206` | no |
55
+ | `connection_timeout` | Integer | no (1..300) | `10` | code-default | `:207` | no |
56
+ | `read_timeout` | Integer | no (1..300) | `30` | code-default | `:208` | no |
57
+ | `write_timeout` | Integer | no (1..300) | `30` | code-default | `:209` | no |
58
+ | `heartbeat` | Integer | no (0..3600) | `15` | code-default | `:210` | no |
59
+ | `continuation_timeout` | Integer (ms) | no | `15000` | code-default | `:211` | no |
60
+ | `channel_prefetch` | Integer | no (1..10000) | `1` | code-default | `:212` | no |
61
+ | `rpc_timeout` | Integer | no (1..3600) | `10` | code-default | `:213` | no |
62
+ | `health_check_interval` | Integer | no | `60` | code-default | `:214` | no |
63
+ | `health_check_file` | String/nil | no | `nil` (desactivado) | code-default | `:217` | no |
64
+ | `drain_idle_timeout` | Integer (s) | no (1..3600) | `5` | code-default | `:274` | no |
65
+ | `drain_poll_interval` | Numeric (s) | no (0.01..10) | `0.1` | code-default | `:275` | no |
66
+ | `controller_namespace` | String | no | `'BugBunny::Controllers'` | code-default | `:220` | no |
67
+ | `log_tags` | Array | no | `[:uuid]` | code-default | `:222` | no |
68
+ | `exchange_options` | Hash | no | `{}` | code-default | `:225` | no |
69
+ | `queue_options` | Hash | no | `{}` | code-default | `:226` | no |
70
+ | `consumer_middlewares` | Stack (attr_reader) | no | `Stack.new` | code-default | `:228` | no |
71
+ | `rpc_reply_headers` | Proc/nil | no | `nil` | code-default | `:262` | no |
72
+ | `on_rpc_reply` | Proc/nil | no | `nil` | code-default | `:263` | no |
73
+ | `on_return` | Proc/nil | no | `nil` | code-default | `:264` | no |
74
+ | `nack_raise` | Boolean | no | `true` | code-default | `:265` | no |
75
+ | `return_raise` | Boolean | no | `true` | code-default | `:266` | no |
70
76
 
71
77
  > **Override por request:** `nack_raise` y `return_raise` se sobreescriben por
72
78
  > llamada con `nack_raise:` / `return_raise:` en `Client#publish` (scope-override
@@ -87,7 +93,7 @@ El install template (`initializer.rb`) **sugiere al consumidor** wirear 4 ENV
87
93
  ### d. Derivaciones simples
88
94
 
89
95
  - `url` ← `"amqp://#{username}:#{password}@#{host}:#{port}/#{vhost}"`
90
- (`configuration.rb:225`).
96
+ (`configuration.rb:236`).
91
97
  - `create_connection(**options)` ← `merge_connection_options(options)` sobre la
92
98
  config global; las options explícitas pisan los defaults (`bug_bunny.rb:88`).
93
99
 
@@ -116,21 +122,22 @@ del código.
116
122
  | Conexión (`host`/`port`/`username`/`password`/`vhost`) | conectividad | valor inválido/vacío → `ConfigurationError` en `validate!`; credencial/host errados → `CommunicationError` al conectar (`bug_bunny.rb:95`) | abre socket TCP al broker | identidad y destino del broker; `vhost` aísla ambientes |
117
123
  | Timeouts (`connection_timeout`/`read_timeout`/`write_timeout`/`heartbeat`/`continuation_timeout`) | resiliencia/latencia | muy bajo → cortes espurios bajo carga; muy alto → detección de fallo lenta | — | tuning de la conexión Bunny; `heartbeat` detecta conexiones zombi |
118
124
  | `rpc_timeout` | latencia | el worker remoto no responde a tiempo → `RequestTimeout` (`producer.rb:124,214`) | bloquea el hilo llamante hasta el timeout | techo de espera de un RPC síncrono |
119
- | Resiliencia (`automatically_recover`/`network_recovery_interval`/`max_reconnect_attempts`/`max_reconnect_interval`) | resiliencia | `max_reconnect_attempts` agotado → el Consumer re-levanta y muere (`consumer.rb:110-112`) | reintentos con **backoff exponencial** `network_recovery_interval * 2^(n-1)` cap `max_reconnect_interval` (`consumer.rb:115-118`) | sobrevivir caídas transitorias del broker sin perder el worker |
125
+ | Resiliencia (`automatically_recover`/`network_recovery_interval`/`max_reconnect_attempts`/`max_reconnect_interval`) | resiliencia | `max_reconnect_attempts` agotado → el Consumer re-levanta y muere (`consumer.rb:94-96`) | reintentos con **backoff exponencial** `network_recovery_interval * 2^(n-1)` cap `max_reconnect_interval` (`consumer.rb:99-102`) | sobrevivir caídas transitorias del broker sin perder el worker |
120
126
  | QoS (`channel_prefetch`) | rendimiento | alto → un worker lento acapara mensajes; `1` → menor throughput | controla unacked in-flight (backpressure) | balancea fairness vs throughput (default `1` = fair round-robin) |
121
127
  | Health (`health_check_interval`/`health_check_file`) | observabilidad | `health_check_file` no escribible → el touch falla (degradación de visibilidad, no del flujo) | **escribe (touch) un archivo** en cada health check OK; `nil` desactiva | probe para orquestadores (K8s/Swarm) |
122
- | Callbacks (`on_return`/`on_rpc_reply`/`rpc_reply_headers`) | extensibilidad | una excepción en `on_return` se captura pero **degrada visibilidad** (YARD `configuration.rb:147`) | corren en hilos sensibles (ver §h) | propagar trace-context / alertar unroutable |
128
+ | Drain (`drain_idle_timeout`/`drain_poll_interval`) | latencia | `drain_idle_timeout` muy bajo → un productor lento deja mensajes para la próxima corrida (no se pierden); muy alto → el job tarda más en terminar tras vaciar la cola | ninguno: sólo acota cuánto espera `Consumer#drain` (`consumer.rb:285`) | correr un consumidor como job que termina (#64) |
129
+ | Callbacks (`on_return`/`on_rpc_reply`/`rpc_reply_headers`) | extensibilidad | una excepción en `on_return` se captura pero **degrada visibilidad** (YARD `configuration.rb:157`) | corren en hilos sensibles (ver §h) | propagar trace-context / alertar unroutable |
123
130
  | Confirms (`nack_raise`/`return_raise`) | integridad de entrega | `false` → NACK/return solo se logea, la llamada retorna `202` (modo legacy, posible pérdida silenciosa) | habilitan el raise de `PublishNacked`/`PublishUnroutable` | elegir entre fail-fast vs best-effort en publish confirmado |
124
- | Routing (`controller_namespace`) | seguridad | clase resuelta no subclase de `BugBunny::Controller` → el worker responde **403** + reject (guard anti-RCE, `consumer.rb:222-228`) | acota qué clases son enrutables | superficie de control de RCE |
131
+ | Routing (`controller_namespace`) | seguridad | clase resuelta no subclase de `BugBunny::Controller` → el worker responde **403** + reject (guard anti-RCE, `consumer.rb:375-381`) | acota qué clases son enrutables | superficie de control de RCE |
125
132
  | Logging (`logger`/`bunny_logger`/`log_tags`) | observabilidad | — | salida a `$stdout` por default | trazabilidad estructurada |
126
133
  | Infra (`exchange_options`/`queue_options`) | infraestructura | options incompatibles con el broker → `PreconditionFailed` (vía `CommunicationError`) | defaults globales mergeados por recurso | declaración AMQP por default |
127
134
 
128
135
  ### g. Ramificadores intra-config
129
136
 
130
137
  - `health_check_file = nil` (default) **desactiva** el touchfile aunque
131
- `health_check_interval` siga corriendo (`configuration.rb:99-101,207`).
138
+ `health_check_interval` siga corriendo (`configuration.rb:109-111,217`).
132
139
  - `return_raise` es **inerte cuando `mandatory: false`** — sin `mandatory` el
133
- broker nunca retorna, así que el flag no tiene efecto (`configuration.rb:171`).
140
+ broker nunca retorna, así que el flag no tiene efecto (`configuration.rb:181`).
134
141
  - `nack_raise`/`return_raise` se sobreescriben **por request** (`Client#publish`),
135
142
  ganando sobre el valor global (scope-override).
136
143
 
@@ -138,10 +145,10 @@ del código.
138
145
 
139
146
  | opción | hilo de ejecución | restricción |
140
147
  |---|---|---|
141
- | `on_return` | **hilo interno del consumidor de Bunny** (`configuration.rb:147`) | debe ser rápido y no lanzar; BugBunny captura, pero degrada visibilidad |
142
- | `on_rpc_reply` | **hilo llamante** tras recibir el reply RPC (`configuration.rb:132`) | hidrata trace-context en el publisher |
143
- | `rpc_reply_headers` | hilo del consumer, justo antes del `basic_publish` del reply (`configuration.rb:125`) | debe retornar un Hash de headers |
144
- | reconexión del Consumer | hilo del `subscribe` loop (`consumer.rb:90,122`) | `sleep wait` bloquea ese hilo durante el backoff |
148
+ | `on_return` | **hilo interno del consumidor de Bunny** (`configuration.rb:157`) | debe ser rápido y no lanzar; BugBunny captura, pero degrada visibilidad |
149
+ | `on_rpc_reply` | **hilo llamante** tras recibir el reply RPC (`configuration.rb:142`) | hidrata trace-context en el publisher |
150
+ | `rpc_reply_headers` | hilo del consumer, justo antes del `basic_publish` del reply (`configuration.rb:135`) | debe retornar un Hash de headers |
151
+ | reconexión del Consumer | hilo del `subscribe` loop (`consumer.rb:87,106`) | `sleep wait` bloquea ese hilo durante el backoff |
145
152
 
146
153
  ### j. Inyección a gemas configuradas
147
154
 
@@ -69,16 +69,16 @@ catálogo `docs/errors/errors.md` (RFC-020), no lo redefine.
69
69
 
70
70
  | aspecto | comportamiento (anclado) |
71
71
  |---|---|
72
- | recuperación de conexión | `automatically_recover = true` (default): Bunny recupera canales/suscripciones tras corte TCP (`configuration.rb:60-61`). |
73
- | retry del Consumer | `Consumer#subscribe` reintenta en cualquier `StandardError` con **backoff exponencial** `network_recovery_interval * 2^(n-1)` cap `max_reconnect_interval`, hasta `max_reconnect_attempts` (`nil` = ∞) (`consumer.rb:106-124`). |
74
- | ack | `manual_ack: true` (`consumer.rb:90`) → entrega **at-least-once**: si el worker cae tras procesar pero antes del ack, el mensaje se re-entrega. **El handler debe ser idempotente.** |
72
+ | recuperación de conexión | `automatically_recover = true` (default): Bunny recupera canales/suscripciones tras corte TCP (`configuration.rb:62-63`). |
73
+ | retry del Consumer | `Consumer#subscribe` reintenta en cualquier `StandardError` con **backoff exponencial** `network_recovery_interval * 2^(n-1)` cap `max_reconnect_interval`, hasta `max_reconnect_attempts` (`nil` = ∞) (`consumer.rb:90-108`). |
74
+ | ack | `manual_ack: true` (`consumer.rb:87`) → entrega **at-least-once**: si el worker cae tras procesar pero antes del ack, el mensaje se re-entrega. **El handler debe ser idempotente.** |
75
75
  | retry de publish | **no automático** — un publish fallido levanta `CommunicationError`/`PublishNacked`; reintentarlo puede **duplicar** salvo dedup aguas abajo. |
76
76
  | RPC | `request` espera reply hasta `rpc_timeout`; el retry de un RPC mutante requiere idempotencia (correlación por `correlation_id`). |
77
77
 
78
78
  ### e. Degradación (si RabbitMQ cae)
79
79
 
80
80
  - **Al conectar:** `create_connection` levanta `CommunicationError` (`bug_bunny.rb:95`); no hay fallback ni cola local — el caller decide (circuit-break/alertar).
81
- - **Consumer:** entra al loop de reconexión con backoff; por default (`max_reconnect_attempts = nil`) **reintenta indefinidamente**, logueando `consumer.connection_error` con `retry_in_s` (`consumer.rb:120-122`). Si se fija un máximo y se agota → `consumer.reconnect_exhausted` y re-raise (el worker muere).
81
+ - **Consumer:** entra al loop de reconexión con backoff; por default (`max_reconnect_attempts = nil`) **reintenta indefinidamente**, logueando `consumer.connection_error` con `retry_in_s` (`consumer.rb:104-106`). Si se fija un máximo y se agota → `consumer.reconnect_exhausted` y re-raise (el worker muere).
82
82
  - **Publisher:** cada publish/RPC sobre conexión caída levanta `CommunicationError` (envuelto en `client.rb:168`); **sin buffering** — el mensaje no sale.
83
83
  - **Sin circuit-breaker propio:** la gema no trae breaker ni outbox; la resiliencia aguas arriba (reintentar el comando, encolar, alertar) es responsabilidad del consumidor.
84
84
 
@@ -1,10 +1,11 @@
1
1
  # Errores — bug_bunny
2
2
 
3
3
  > meta: artefacto errores · RFC-020 · generado `arch-structure` (§a/§b/§d) +
4
- > `arch-enrich` (§c) · anclado a `d0533bf`, `lib/bug_bunny/exception.rb`,
5
- > `remote_error.rb`, `middleware/raise_error.rb`, `controller.rb`, `consumer.rb`
6
- > · fecha 2026-06-30 · cobertura: §a/§b/§d completas (estructura); §c política
7
- > completa (enrich, **inferida** de HTTP/AMQP — verificación humana pendiente).
4
+ > `arch-enrich` (§c) · anclado a `5697541`, `lib/bug_bunny/exception.rb`,
5
+ > `remote_error.rb`, `middleware/raise_error.rb`, `controller.rb`, `consumer.rb`,
6
+ > `observability.rb` · fecha 2026-07-31 · cobertura: §a/§b/§d completas
7
+ > (estructura); §c política completa (enrich, **inferida** de HTTP/AMQP —
8
+ > verificación humana pendiente).
8
9
 
9
10
  ## 1. Resumen
10
11
 
@@ -45,7 +46,7 @@ Jerarquía (todas bajo `BugBunny::Error < ::StandardError`):
45
46
  |---|---|---|
46
47
  | `Error` | `::StandardError` | base — no se levanta directa salvo `resource.rb:122` (pool ausente) |
47
48
  | `CommunicationError` | `Error` | `bug_bunny.rb:96`, `session.rb:177,285`, `producer.rb:90`, `client.rb:168` — envuelve cualquier `Bunny::Exception` en la frontera del gem; original en `.cause` |
48
- | `ConfigurationError` | `Error` | `configuration.rb:262,269,277` — validación al final de `BugBunny.configure` |
49
+ | `ConfigurationError` | `Error` | `configuration.rb:282,289,297` — validación al final de `BugBunny.configure` |
49
50
  | `PublishNacked` | `Error` | `producer.rb:235` — NACK del broker en modo `:confirmed`. Attrs: `path`, `nacked_count`. Opt-out `nack_raise: false` |
50
51
  | `PublishUnroutable` | `Error` | `producer.rb:325` — `basic.return` con `mandatory: true`. Attrs: `path`, `exchange`, `routing_key`, `reply_code`, `reply_text`, `correlation_id`. Opt-out `return_raise: false` |
51
52
  | `ClientError` | `Error` | `raise_error.rb:170` — 4xx no mapeado explícito |
@@ -84,7 +85,7 @@ operaciones de RFC-003 (`docs/api/`, hoy pendiente — ver §4), no las redefine
84
85
  | Cliente RPC | otro ≥400 | `ClientError` | 4xx no mapeado |
85
86
  | Productor (publish `:confirmed`) | n/a (AMQP) | `PublishNacked` / `PublishUnroutable` | NACK / return del broker — no es status HTTP |
86
87
  | Transporte (frontera gem) | n/a (AMQP) | `CommunicationError` | fallo de red/broker, envuelve `Bunny::Exception` |
87
- | **Worker (dispatch)** | **403** | — (no excepción; responde 403 + reject) | guard anti-RCE: clase enrutada no subclase de `BugBunny::Controller` (`consumer.rb:222-228`) |
88
+ | **Worker (dispatch)** | **403** | — (no excepción; responde 403 + reject) | guard anti-RCE: clase enrutada no subclase de `BugBunny::Controller` (`consumer.rb:375-381`) |
88
89
 
89
90
  ### c. Política por error
90
91
 
@@ -132,7 +133,7 @@ ante una excepción no mapeada por `rescue_from`:
132
133
  ```
133
134
 
134
135
  - El envelope `bug_bunny_exception` lo arma `RemoteError.serialize` (`remote_error.rb:29`);
135
- también lo agrega `Consumer` cuando `status == 500 && exception` (`consumer.rb:325`).
136
+ también lo agrega `Consumer` cuando `status == 500 && exception` (`consumer.rb:478`).
136
137
  Es lo que el cliente reconstruye como `RemoteError` (`raise_error.rb:61-64`).
137
138
  - `render status:, json:` (`controller.rb:242`) deja el shape del body de error de
138
139
  dominio a criterio del worker — la gema no lo impone.
@@ -156,13 +157,18 @@ parsea el body, devuelve `parsed['errors']` por convención o el cuerpo completo
156
157
 
157
158
  > **Seguridad (cruza RFC-017):** `raw_response` puede contener datos sensibles en
158
159
  > `details`. La gema lo entrega crudo a propósito; **sanitizar antes de cualquier
159
- > sink** (Sentry/logs) filtrando `password|pass|passwd|secret|token|api_key|auth`
160
- > → `[FILTERED]` es responsabilidad del consumidor (`exception.rb:25-30`).
160
+ > sink** (Sentry/logs) es responsabilidad del consumidor (`exception.rb:25-34`).
161
+ > La lista canónica de claves sensibles es `Observability::SENSITIVE_KEYS`
162
+ > (`observability.rb:13-16`) y **no se replica acá** — replicarla la desincroniza
163
+ > (ojo: `pass` bare NO está en la lista, a propósito, para no filtrar
164
+ > `passport_number`). Para sanear se reusa `Observability.sensitive_key?` (filtra
165
+ > por NOMBRE de clave) y `Observability.redact_structure` (recorre la estructura
166
+ > y además redacta credenciales embebidas en el VALOR → `[FILTERED]`).
161
167
 
162
168
  ## 3. Inferencias
163
169
 
164
170
  - **Guard anti-RCE = 403, no excepción.** El control de seguridad que valida la
165
- herencia de la clase enrutada vive en `consumer.rb:222-228`: si la clase
171
+ herencia de la clase enrutada vive en `consumer.rb:375-381`: si la clase
166
172
  resuelta no es subclase de `BugBunny::Controller`, el worker loguea
167
173
  `consumer.security_violation`, responde **403 'Forbidden'** (`handle_fatal_error`)
168
174
  y rechaza el mensaje sin requeue — **no levanta una excepción dedicada**. La ex
@@ -1,6 +1,6 @@
1
1
  # Glosario — bug_bunny
2
2
 
3
- > meta: artefacto `glosario` · RFC-009 (binding opcional, r: §2 materialización no-tabular) · generado dev-enrich (siembra) · anclado a `a5cdb10` · cobertura: parcial, acreta por PR
3
+ > meta: artefacto `glosario` · RFC-009 (binding opcional, r: §2 materialización no-tabular) · generado dev-enrich (siembra) · anclado a `5697541` · fecha 2026-07-31 · cobertura: parcial, acreta por PR
4
4
 
5
5
  ## 1. Resumen
6
6
 
@@ -127,7 +127,7 @@ Cadena transversal que corre antes del dispatch al controller (tracing, auth, lo
127
127
  - `lib/bug_bunny/consumer_middleware.rb` — `BugBunny::ConsumerMiddleware`
128
128
 
129
129
  ## Observability
130
- Mixin de logging estructurado `key=value` que implementa OTel semantic conventions for messaging; `safe_log` nunca lanza; filtra claves sensibles a `[FILTERED]`.
130
+ Mixin de logging estructurado `key=value` que implementa OTel semantic conventions for messaging; `safe_log` nunca lanza; redacta a `[FILTERED]` en dos capas — por **NOMBRE de clave** (`sensitive_key?`, substring sobre `SENSITIVE_KEYS`) y por **CONTENIDO del valor** (`redact_value`, para la credencial embebida en texto libre que la capa de clave no ve; `redact_structure` para un `Hash`, redactado antes de serializar para no romper el JSON).
131
131
 
132
132
  **Binding:**
133
133
  - `lib/bug_bunny/observability.rb` — `BugBunny::Observability`
@@ -2,8 +2,9 @@
2
2
 
3
3
  > meta: artefacto release · RFC-014 (`accepted`) · generado por `arch-structure`
4
4
  > + `arch-enrich` (híbrido RFC-014 §2; nació como piloto manual #51, re-anclado
5
- > a la RFC vigente) · anclado a `.github/workflows/release.yml`, `*.gemspec`,
6
- > `lib/bug_bunny/version.rb`, `CHANGELOG.md`, git · fecha 2026-06-30 · cobertura:
5
+ > a la RFC vigente) · anclado a `5697541`
6
+ > (`.github/workflows/release.yml`, `*.gemspec`, `lib/bug_bunny/version.rb`,
7
+ > `CHANGELOG.md`) · fecha 2026-07-31 · cobertura:
7
8
  > completa (régimen gema: build→publish, §e/f/g n/a).
8
9
 
9
10
  ## 1. Resumen
@@ -20,9 +21,9 @@ out-of-repo).
20
21
 
21
22
  ### a. Hecho verificable
22
23
 
23
- - **Convención de versión:** SemVer `vX.X.X`. Actual: **4.19.0**.
24
- - **Source of truth:** tag remoto (`v4.19.0`) + **triple mirror**
25
- `lib/bug_bunny/version.rb` (`VERSION = '4.19.0'`) ← `bug_bunny.gemspec:7`
24
+ - **Convención de versión:** SemVer `vX.X.X`. Actual: **5.2.0**.
25
+ - **Source of truth:** tag remoto (`v5.2.0`) + **triple mirror**
26
+ `lib/bug_bunny/version.rb` (`VERSION = '5.2.0'`) ← `bug_bunny.gemspec:7`
26
27
  (`spec.version = BugBunny::VERSION`).
27
28
  - **Changelog canónico:** `CHANGELOG.md` único.
28
29
  - **Patrón de trigger:** `gema-tag` (patrón 1).
@@ -33,7 +34,7 @@ out-of-repo).
33
34
 
34
35
  - **Convención:** SemVer `vX.X.X` (**con `v`** — distinto al servicio).
35
36
  - **Source of truth:** tag remoto canónico (`git tag --sort=-v:refname` →
36
- `v4.19.0`).
37
+ `v5.2.0`).
37
38
  - **Mirror:** `lib/bug_bunny/version.rb` (`VERSION`), leído por
38
39
  `bug_bunny.gemspec:7` (`spec.version = BugBunny::VERSION`).
39
40
  `required_ruby_version >= 2.6.0` (`bug_bunny.gemspec:17`).
@@ -58,8 +59,8 @@ out-of-repo).
58
59
  `on: push: tags: ['v*']` → `ruby/setup-ruby@v1` → `gem build *.gemspec` +
59
60
  `gem push *.gem` (auth `secrets.RUBYGEMS_API_KEY`). Auditable y versionado con
60
61
  el código; se ancla a `file:line`, no se referencia como caja negra.
61
- - **Consumo:** los servicios la pinnean por versión (`gem "bug_bunny", "~> 4.19.0"`)
62
- desde RubyGems — **no** git-source.
62
+ - **Consumo:** los servicios la pinnean por versión (`gem "bug_bunny", "~> X.Y.0"`,
63
+ hoy `~> 5.1.0`) desde RubyGems — **no** git-source.
63
64
 
64
65
  ### e. Deploy / publish
65
66
 
@@ -79,12 +80,21 @@ procedimiento per-repo porque no vive acá. Una versión yankeada se anotaría e
79
80
 
80
81
  ### h. Dependencias de deploy inter-servicio
81
82
 
82
- - **Consumidores** (cruza RFC-018): servicios del fleet la pinnean
83
- `~> 4.19.0` (semántica minor-compatible). Un cambio de contrato del gem
84
- (ej. el behavior-change de `4.18.0` — `Bunny::Exception` → `CommunicationError`)
85
- obliga a los consumidores a migrar; el `CHANGELOG.md` lo documenta como
86
- breaking note. **Orden de deploy:** los consumidores adoptan al hacer `bundle
87
- update bug_bunny` — no hay deploy coordinado (cada servicio elige cuándo).
83
+ - **Consumidores** (cruza RFC-018): servicios del fleet la pinnean con la forma
84
+ `~> X.Y.0` — **patch-compatible**, no minor: `~> 5.1.0` resuelve
85
+ `>= 5.1.0, < 5.2.0`, así que un minor **no entra** sin editar el `Gemfile`.
86
+ Consecuencia operativa: un fix publicado como **patch** lo toman con
87
+ `bundle update bug_bunny`; uno publicado como **minor** requiere tocar el pin
88
+ en cada consumidor. Estado medido el 2026-09-29 (Gemfiles de los clones del
89
+ workspace): `box_manager_service` en `~> 5.1.1`; `box_acs_manager`,
90
+ `box_cluster_manager` y `box_radius_manager` en `~> 5.1.0` — o sea que
91
+ **5.2.0 no le entra a ninguno** sin tocar el pin. *(El estado anterior decía
92
+ `box_acs_manager`/`box_cluster_manager` en `~> 4.19.0`: ya no es así.)* Un cambio de contrato del gem (ej. el
93
+ behavior-change de `4.18.0` — `Bunny::Exception` → `CommunicationError`, o la
94
+ eliminación de `BugBunny::SecurityError` en `5.0.0`) obliga a los consumidores
95
+ a migrar; el `CHANGELOG.md` lo documenta como breaking note. **Orden de
96
+ deploy:** los consumidores adoptan al hacer `bundle update bug_bunny` — no hay
97
+ deploy coordinado (cada servicio elige cuándo).
88
98
 
89
99
  ### i. Contrato con la skill productora
90
100