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 +4 -4
- data/AGENTS.md +1 -1
- data/CHANGELOG.md +24 -0
- data/CLAUDE.md +1 -1
- data/README.md +47 -6
- data/docs/behavior/behavior.md +37 -9
- data/docs/config/configuracion.md +54 -47
- data/docs/consumed/rabbitmq.md +4 -4
- data/docs/errors/errors.md +16 -10
- data/docs/glossary/glossary.md +2 -2
- data/docs/release/release.md +24 -14
- data/docs/test/testing.md +16 -6
- data/lib/bug_bunny/configuration.rb +22 -2
- data/lib/bug_bunny/consumer.rb +184 -31
- data/lib/bug_bunny/drain_tracker.rb +54 -0
- data/lib/bug_bunny/exception.rb +9 -5
- data/lib/bug_bunny/observability.rb +83 -3
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +1 -0
- data/skill/SKILL.md +26 -8
- data/skill/references/consumer.md +27 -1
- data/skill/references/errores.md +8 -4
- data/skill/references/routing.md +1 -1
- data/spec/integration/drain_spec.rb +177 -0
- data/spec/unit/configuration_spec.rb +27 -1
- data/spec/unit/drain_tracker_spec.rb +37 -0
- data/spec/unit/observability_spec.rb +158 -0
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ac482578cfbb87414309cc03502be5f11c4ee4b0e799338bfeb9bfcf65488f5
|
|
4
|
+
data.tar.gz: a4a9e64ac077bd6564790a5de762c804d38c65623f2c7cbc839217bb8f98bd25
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
|
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
|
|
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:**
|
|
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
|
|
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) ·
|
data/docs/behavior/behavior.md
CHANGED
|
@@ -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 `
|
|
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:
|
|
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:
|
|
17
|
-
|
|
|
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 `
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 `
|
|
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`):**
|
|
24
|
-
-
|
|
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:**
|
|
28
|
-
- **Con rango validado:**
|
|
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:
|
|
41
|
-
| `port` | Integer | sí (validate!, 1..65535) | `5672` | code-default | `:
|
|
42
|
-
| `username` | String | sí (validate!) | `'guest'` | code-default | `:
|
|
43
|
-
| `password` | String | sí (validate!) | `'guest'` | code-default | `:
|
|
44
|
-
| `vhost` | String | sí (validate!) | `'/'` | code-default | `:
|
|
45
|
-
| `logger` | Logger | no | `Logger.new($stdout)` INFO | code-default | `:
|
|
46
|
-
| `bunny_logger` | Logger | no | `Logger.new($stdout)` WARN | code-default | `:
|
|
47
|
-
| `automatically_recover` | Boolean | no | `true` | code-default | `:
|
|
48
|
-
| `network_recovery_interval` | Integer | no | `5` | code-default | `:
|
|
49
|
-
| `max_reconnect_attempts` | Integer/nil | no | `nil` (reintenta ∞) | code-default | `:
|
|
50
|
-
| `max_reconnect_interval` | Integer | no | `60` | code-default | `:
|
|
51
|
-
| `connection_timeout` | Integer | no (1..300) | `10` | code-default | `:
|
|
52
|
-
| `read_timeout` | Integer | no (1..300) | `30` | code-default | `:
|
|
53
|
-
| `write_timeout` | Integer | no (1..300) | `30` | code-default | `:
|
|
54
|
-
| `heartbeat` | Integer | no (0..3600) | `15` | code-default | `:
|
|
55
|
-
| `continuation_timeout` | Integer (ms) | no | `15000` | code-default | `:
|
|
56
|
-
| `channel_prefetch` | Integer | no (1..10000) | `1` | code-default | `:
|
|
57
|
-
| `rpc_timeout` | Integer | no (1..3600) | `10` | code-default | `:
|
|
58
|
-
| `health_check_interval` | Integer | no | `60` | code-default | `:
|
|
59
|
-
| `health_check_file` | String/nil | no | `nil` (desactivado) | code-default | `:
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
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:
|
|
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:
|
|
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
|
-
|
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
142
|
-
| `on_rpc_reply` | **hilo llamante** tras recibir el reply RPC (`configuration.rb:
|
|
143
|
-
| `rpc_reply_headers` | hilo del consumer, justo antes del `basic_publish` del reply (`configuration.rb:
|
|
144
|
-
| reconexión del Consumer | hilo del `subscribe` loop (`consumer.rb:
|
|
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
|
|
data/docs/consumed/rabbitmq.md
CHANGED
|
@@ -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:
|
|
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:
|
|
74
|
-
| ack | `manual_ack: true` (`consumer.rb:
|
|
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:
|
|
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
|
|
data/docs/errors/errors.md
CHANGED
|
@@ -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 `
|
|
5
|
-
> `remote_error.rb`, `middleware/raise_error.rb`, `controller.rb`, `consumer.rb
|
|
6
|
-
> · fecha 2026-
|
|
7
|
-
> completa (enrich, **inferida** de HTTP/AMQP —
|
|
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:
|
|
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:
|
|
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:
|
|
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)
|
|
160
|
-
>
|
|
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:
|
|
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
|
data/docs/glossary/glossary.md
CHANGED
|
@@ -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 `
|
|
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;
|
|
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`
|
data/docs/release/release.md
CHANGED
|
@@ -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
|
|
6
|
-
> `lib/bug_bunny/version.rb`,
|
|
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: **
|
|
24
|
-
- **Source of truth:** tag remoto (`
|
|
25
|
-
`lib/bug_bunny/version.rb` (`VERSION = '
|
|
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
|
-
`
|
|
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", "~>
|
|
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
|
-
`~>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|