bug_bunny 5.1.1 → 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 +15 -0
- data/CLAUDE.md +1 -1
- data/README.md +28 -1
- 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 +4 -4
- data/docs/release/release.md +9 -7
- 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/version.rb +1 -1
- data/lib/bug_bunny.rb +1 -0
- data/skill/SKILL.md +11 -4
- data/skill/references/consumer.md +27 -1
- data/skill/references/errores.md +1 -1
- 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
- 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,20 @@
|
|
|
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
|
+
|
|
3
18
|
## [5.1.1] - 2026-07-31
|
|
4
19
|
|
|
5
20
|
### Correcciones
|
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
|
|
@@ -432,7 +459,7 @@ end
|
|
|
432
459
|
|
|
433
460
|
Artefactos de detalle (modelo `dev-*`, RFC-001). El README indexa; no duplica.
|
|
434
461
|
|
|
435
|
-
Anclado a `v5.
|
|
462
|
+
Anclado a `v5.2.0`.
|
|
436
463
|
|
|
437
464
|
| Capa | Artefacto | Estado |
|
|
438
465
|
|---|---|---|
|
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
|
@@ -46,7 +46,7 @@ Jerarquía (todas bajo `BugBunny::Error < ::StandardError`):
|
|
|
46
46
|
|---|---|---|
|
|
47
47
|
| `Error` | `::StandardError` | base — no se levanta directa salvo `resource.rb:122` (pool ausente) |
|
|
48
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` |
|
|
49
|
-
| `ConfigurationError` | `Error` | `configuration.rb:
|
|
49
|
+
| `ConfigurationError` | `Error` | `configuration.rb:282,289,297` — validación al final de `BugBunny.configure` |
|
|
50
50
|
| `PublishNacked` | `Error` | `producer.rb:235` — NACK del broker en modo `:confirmed`. Attrs: `path`, `nacked_count`. Opt-out `nack_raise: false` |
|
|
51
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` |
|
|
52
52
|
| `ClientError` | `Error` | `raise_error.rb:170` — 4xx no mapeado explícito |
|
|
@@ -85,7 +85,7 @@ operaciones de RFC-003 (`docs/api/`, hoy pendiente — ver §4), no las redefine
|
|
|
85
85
|
| Cliente RPC | otro ≥400 | `ClientError` | 4xx no mapeado |
|
|
86
86
|
| Productor (publish `:confirmed`) | n/a (AMQP) | `PublishNacked` / `PublishUnroutable` | NACK / return del broker — no es status HTTP |
|
|
87
87
|
| Transporte (frontera gem) | n/a (AMQP) | `CommunicationError` | fallo de red/broker, envuelve `Bunny::Exception` |
|
|
88
|
-
| **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`) |
|
|
89
89
|
|
|
90
90
|
### c. Política por error
|
|
91
91
|
|
|
@@ -133,7 +133,7 @@ ante una excepción no mapeada por `rescue_from`:
|
|
|
133
133
|
```
|
|
134
134
|
|
|
135
135
|
- El envelope `bug_bunny_exception` lo arma `RemoteError.serialize` (`remote_error.rb:29`);
|
|
136
|
-
también lo agrega `Consumer` cuando `status == 500 && exception` (`consumer.rb:
|
|
136
|
+
también lo agrega `Consumer` cuando `status == 500 && exception` (`consumer.rb:478`).
|
|
137
137
|
Es lo que el cliente reconstruye como `RemoteError` (`raise_error.rb:61-64`).
|
|
138
138
|
- `render status:, json:` (`controller.rb:242`) deja el shape del body de error de
|
|
139
139
|
dominio a criterio del worker — la gema no lo impone.
|
|
@@ -168,7 +168,7 @@ parsea el body, devuelve `parsed['errors']` por convención o el cuerpo completo
|
|
|
168
168
|
## 3. Inferencias
|
|
169
169
|
|
|
170
170
|
- **Guard anti-RCE = 403, no excepción.** El control de seguridad que valida la
|
|
171
|
-
herencia de la clase enrutada vive en `consumer.rb:
|
|
171
|
+
herencia de la clase enrutada vive en `consumer.rb:375-381`: si la clase
|
|
172
172
|
resuelta no es subclase de `BugBunny::Controller`, el worker loguea
|
|
173
173
|
`consumer.security_violation`, responde **403 'Forbidden'** (`handle_fatal_error`)
|
|
174
174
|
y rechaza el mensaje sin requeue — **no levanta una excepción dedicada**. La ex
|
data/docs/release/release.md
CHANGED
|
@@ -21,9 +21,9 @@ out-of-repo).
|
|
|
21
21
|
|
|
22
22
|
### a. Hecho verificable
|
|
23
23
|
|
|
24
|
-
- **Convención de versión:** SemVer `vX.X.X`. Actual: **5.
|
|
25
|
-
- **Source of truth:** tag remoto (`v5.
|
|
26
|
-
`lib/bug_bunny/version.rb` (`VERSION = '5.
|
|
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`
|
|
27
27
|
(`spec.version = BugBunny::VERSION`).
|
|
28
28
|
- **Changelog canónico:** `CHANGELOG.md` único.
|
|
29
29
|
- **Patrón de trigger:** `gema-tag` (patrón 1).
|
|
@@ -34,7 +34,7 @@ out-of-repo).
|
|
|
34
34
|
|
|
35
35
|
- **Convención:** SemVer `vX.X.X` (**con `v`** — distinto al servicio).
|
|
36
36
|
- **Source of truth:** tag remoto canónico (`git tag --sort=-v:refname` →
|
|
37
|
-
`v5.
|
|
37
|
+
`v5.2.0`).
|
|
38
38
|
- **Mirror:** `lib/bug_bunny/version.rb` (`VERSION`), leído por
|
|
39
39
|
`bug_bunny.gemspec:7` (`spec.version = BugBunny::VERSION`).
|
|
40
40
|
`required_ruby_version >= 2.6.0` (`bug_bunny.gemspec:17`).
|
|
@@ -85,9 +85,11 @@ procedimiento per-repo porque no vive acá. Una versión yankeada se anotaría e
|
|
|
85
85
|
`>= 5.1.0, < 5.2.0`, así que un minor **no entra** sin editar el `Gemfile`.
|
|
86
86
|
Consecuencia operativa: un fix publicado como **patch** lo toman con
|
|
87
87
|
`bundle update bug_bunny`; uno publicado como **minor** requiere tocar el pin
|
|
88
|
-
en cada consumidor. Estado
|
|
89
|
-
en `~> 5.1.
|
|
90
|
-
`~>
|
|
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
|
|
91
93
|
behavior-change de `4.18.0` — `Bunny::Exception` → `CommunicationError`, o la
|
|
92
94
|
eliminación de `BugBunny::SecurityError` en `5.0.0`) obliga a los consumidores
|
|
93
95
|
a migrar; el `CHANGELOG.md` lo documenta como breaking note. **Orden de
|
data/docs/test/testing.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# Test — bug_bunny
|
|
2
2
|
|
|
3
3
|
> meta: artefacto test · RFC-013 · generado `arch-structure` (§a-§d) +
|
|
4
|
-
> `arch-enrich` (§e-§h) · anclado a `7bf1da7
|
|
4
|
+
> `arch-enrich` (§e-§h) · anclado a `35b075a` (squash de #65 en `main`; el ancla anterior `7bf1da7` no era ancestro de `main`), `Rakefile`, `bug_bunny.gemspec`,
|
|
5
5
|
> `spec/spec_helper.rb`, `spec/support/integration_helper.rb`,
|
|
6
6
|
> `.github/workflows/main.yml`, `CHANGELOG.md` · fecha 2026-07-22 · cobertura:
|
|
7
7
|
> §a-§d (estructura) + §e-§h (enrich, anclado a specs/CHANGELOG) completas.
|
|
8
|
+
> Incremento 2026-09-28 (#64): conteos, `drain_*_spec` y el gap de aislamiento.
|
|
8
9
|
|
|
9
10
|
## 1. Resumen
|
|
10
11
|
|
|
11
|
-
Suite principal **RSpec** (`spec/`,
|
|
12
|
+
Suite principal **RSpec** (`spec/`, 25 specs: 17 unit + 8 integration). Tarea
|
|
12
13
|
`:test` legacy de **Minitest** (`test/`, 2 archivos) fuera del default y del CI.
|
|
13
14
|
CI corre `bundle exec rake` (= `:spec`) en Ruby 3.4.4. Sin coverage tool
|
|
14
15
|
configurado.
|
|
@@ -19,8 +20,8 @@ configurado.
|
|
|
19
20
|
|
|
20
21
|
| framework | dir | nivel | nº | propósito |
|
|
21
22
|
|---|---|---|---|---|
|
|
22
|
-
| **RSpec** `~> 3.0` | `spec/unit/` | unit |
|
|
23
|
-
| **RSpec** | `spec/integration/` | integration |
|
|
23
|
+
| **RSpec** `~> 3.0` | `spec/unit/` | unit | 17 | client/session pool, configuration, consumer, `drain_tracker`, producer, controller, raise_error, remote_error, request, route, observability, otel, resource, middleware, `extra_top_level_params` (hook de params hermanos en `Resource#save`) |
|
|
24
|
+
| **RSpec** | `spec/integration/` | integration | 8 | client, consumer_middleware, controller, drain, error_handling, infrastructure, publisher_confirms, resource — **requieren RabbitMQ real** (usan `BugBunny.create_connection` + pool) |
|
|
24
25
|
| **Minitest** `~> 5.0` (+ `mocha`, `minitest-reporters`) | `test/integration/` | integration (legacy) | 2 | `manual_client_test.rb`, `infrastructure_test.rb` — tarea `:test`, **no** en default ni CI |
|
|
25
26
|
|
|
26
27
|
Sin tags declarados (`:slow`/`:js`) en la config de RSpec.
|
|
@@ -58,9 +59,9 @@ umbral de coverage declarado.
|
|
|
58
59
|
### e. Gaps de cobertura
|
|
59
60
|
|
|
60
61
|
- **Integration specs no corren en CI:** `main.yml` no declara servicio RabbitMQ;
|
|
61
|
-
las
|
|
62
|
+
las 8 integration specs **se skipean** vía `rabbitmq_available?`
|
|
62
63
|
(`spec/support/integration_helper.rb:14`, ver `publisher_confirms_spec.rb:10`).
|
|
63
|
-
En CI solo se ejercitan las **
|
|
64
|
+
En CI solo se ejercitan las **17 unit specs** → el contrato AMQP real (publish/
|
|
64
65
|
consume/confirms contra broker) **no se valida en pipeline**, solo localmente
|
|
65
66
|
con broker. Gap relevante.
|
|
66
67
|
- **Sin medición de cobertura:** no hay SimpleCov ni umbral → la cobertura no está
|
|
@@ -75,9 +76,18 @@ umbral de coverage declarado.
|
|
|
75
76
|
| **Errores RFC-020** (status→excepción, materia prima) | `raise_error_spec`, `remote_error_spec`, `communication_error_wrapping_spec`, `error_handling_spec` (integration) | **bien cubierto** (unit) |
|
|
76
77
|
| **Consumed RFC-018** (Bunny::Exception→`CommunicationError`) | `communication_error_wrapping_spec`, `client_session_pool_spec` | cubierto (unit) |
|
|
77
78
|
| **Config RFC-012** (validaciones de `Configuration`) | `configuration_spec` | cubierto |
|
|
79
|
+
| **Consumer#drain** (drenar y salir, #64) | `drain_tracker_spec` (unit, reloj inyectado), `drain_spec` (integration) | cubierto; la parte integration skipea sin broker. Validado por mutación: quitar la espera, el retorno temprano o el chequeo de `busy?` hace fallar el test correspondiente |
|
|
78
80
|
| **Confirms** (`PublishNacked`/`PublishUnroutable`) | `producer_spec`, `publisher_confirms_spec` (integration) | parcial en CI (la parte integration skipea sin broker) |
|
|
79
81
|
| **Operaciones/routing** (RFC-003, capa F2) | `route_spec`, `request_spec`, `controller_spec`, `controller_after_action_spec`, `resource_spec` | cubierto (unit) |
|
|
80
82
|
|
|
83
|
+
- **Aislamiento entre specs — corregido 2026-09-28 (#64):** el `after` de
|
|
84
|
+
`configuration_spec` reemplazaba la configuración global por una con defaults
|
|
85
|
+
(`guest`), así que los specs de integración que corrían después se conectaban
|
|
86
|
+
como `guest` y se **skipeaban como "RabbitMQ no disponible" aun con broker**.
|
|
87
|
+
Cuántos, dependía del seed (11 en `main` con `--seed 1`). Ahora un `around`
|
|
88
|
+
restaura la configuración original (`spec/unit/configuration_spec.rb:14-22`);
|
|
89
|
+
medido con broker local: 311 examples, 0 failures, 0 pending en los seeds 1, 2 y 3.
|
|
90
|
+
|
|
81
91
|
### g. Link a incidente → test de regresión
|
|
82
92
|
|
|
83
93
|
| incidente | test de regresión | ancla |
|
|
@@ -21,7 +21,7 @@ module BugBunny
|
|
|
21
21
|
# Claves soportadas:
|
|
22
22
|
# - `:type` — clase que debe responder `is_a?`
|
|
23
23
|
# - `:required` — si `true`, nil o string vacío lanzan ConfigurationError
|
|
24
|
-
# - `:range` — rango válido de valores (solo para
|
|
24
|
+
# - `:range` — rango válido de valores (solo para numéricos)
|
|
25
25
|
VALIDATIONS = {
|
|
26
26
|
host: { type: String, required: true },
|
|
27
27
|
port: { type: Integer, required: true, range: 1..65_535 },
|
|
@@ -33,7 +33,9 @@ module BugBunny
|
|
|
33
33
|
read_timeout: { type: Integer, range: 1..300 },
|
|
34
34
|
write_timeout: { type: Integer, range: 1..300 },
|
|
35
35
|
rpc_timeout: { type: Integer, range: 1..3_600 },
|
|
36
|
-
channel_prefetch: { type: Integer, range: 1..10_000 }
|
|
36
|
+
channel_prefetch: { type: Integer, range: 1..10_000 },
|
|
37
|
+
drain_idle_timeout: { type: Integer, range: 1..3_600 },
|
|
38
|
+
drain_poll_interval: { type: Numeric, range: 0.01..10 }
|
|
37
39
|
}.freeze
|
|
38
40
|
|
|
39
41
|
# @return [String] Host o IP del servidor RabbitMQ (ej: 'localhost').
|
|
@@ -94,6 +96,14 @@ module BugBunny
|
|
|
94
96
|
# @return [Integer] Intervalo en segundos para verificar la salud de la cola.
|
|
95
97
|
attr_accessor :health_check_interval
|
|
96
98
|
|
|
99
|
+
# @return [Integer] Segundos sin entregas tras los cuales {BugBunny::Consumer#drain}
|
|
100
|
+
# da la cola por vacía y retorna (default: 5).
|
|
101
|
+
attr_accessor :drain_idle_timeout
|
|
102
|
+
|
|
103
|
+
# @return [Numeric] Cada cuántos segundos {BugBunny::Consumer#drain} revisa si se
|
|
104
|
+
# cumplió la ventana de inactividad (default: 0.1).
|
|
105
|
+
attr_accessor :drain_poll_interval
|
|
106
|
+
|
|
97
107
|
# @return [String, nil] Ruta del archivo que se actualizará (touch) en cada health check exitoso.
|
|
98
108
|
# Ideal para sondas (probes) de orquestadores como Docker Swarm o Kubernetes.
|
|
99
109
|
# Si es `nil`, la funcionalidad de touchfile se desactiva.
|
|
@@ -217,6 +227,7 @@ module BugBunny
|
|
|
217
227
|
|
|
218
228
|
@consumer_middlewares = ConsumerMiddleware::Stack.new
|
|
219
229
|
init_callback_defaults
|
|
230
|
+
init_drain_defaults
|
|
220
231
|
end
|
|
221
232
|
|
|
222
233
|
# Construye la URL de conexión AMQP basada en los atributos configurados.
|
|
@@ -255,6 +266,15 @@ module BugBunny
|
|
|
255
266
|
@return_raise = true
|
|
256
267
|
end
|
|
257
268
|
|
|
269
|
+
# Defaults de {BugBunny::Consumer#drain}.
|
|
270
|
+
# Extraído de {#initialize} para mantener el ABC size dentro de los límites.
|
|
271
|
+
#
|
|
272
|
+
# @return [void]
|
|
273
|
+
def init_drain_defaults
|
|
274
|
+
@drain_idle_timeout = 5
|
|
275
|
+
@drain_poll_interval = 0.1
|
|
276
|
+
end
|
|
277
|
+
|
|
258
278
|
def validate_required!(attr, value, rules)
|
|
259
279
|
return unless rules[:required]
|
|
260
280
|
return unless value.nil? || (value.is_a?(String) && value.empty?)
|