bug_bunny 4.17.1 → 4.18.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/CHANGELOG.md +16 -0
- data/README.md +1 -1
- data/docs/behavior/behavior.md +6 -3
- data/lib/bug_bunny/client.rb +23 -2
- data/lib/bug_bunny/exception.rb +18 -2
- data/lib/bug_bunny/producer.rb +2 -2
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +8 -1
- data/skill/SKILL.md +5 -4
- data/spec/unit/communication_error_wrapping_spec.rb +129 -0
- data/spec/unit/producer_spec.rb +10 -2
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 030517babaaafccb4ac6b067e1c11ac1cf6d4a6a711919c8455c3dcfe6a79637
|
|
4
|
+
data.tar.gz: fb9a551e05882e05efc3cd23215773507ae6d5d2baab70681360c04100724d29
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5c4fbaade4883c361a20e705155d2b60dc6b3e6e64905afe34b02a7a7016cd7913bfd9f1f2de09979d357f72b026dbdc3129ad871d128a1b9ee901628a096b6
|
|
7
|
+
data.tar.gz: fb7b8dd6f7b74dee3c6d13b789dd07ff3abaeda535680a1ab7ea4276269e40a238588403e5fd7c8d2cdb3188acd3b7889d867d4dffed6a19af45d91eefa68591
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.18.0] - 2026-05-26
|
|
4
|
+
|
|
5
|
+
> Behavior change menor: cualquier `Bunny::Exception` que escape en la frontera del gem (TCP fail, conn rota, canal cerrado, auth fail) ahora se envuelve como `BugBunny::CommunicationError`. La excepción original queda accesible vía `.cause`. Callers que rescatan `Bunny::TCPConnectionFailed` directo deben migrar a `BugBunny::CommunicationError`. Callers que ya rescatan `BugBunny::Error` / `CommunicationError`: sin cambios.
|
|
6
|
+
|
|
7
|
+
### Correcciones
|
|
8
|
+
- **`Client#publish`/`#request`/`#send` leak de `Bunny::TCPConnectionFailedForAllHosts` (#49):** el TCP fail nacía en `ConnectionPool::TimedStack#try_create` dentro de `@pool.with`, antes de entrar al `Producer#confirmed` que ya envolvía con `rescue StandardError`. `Client#run_in_pool` ahora envuelve cualquier `Bunny::Exception` → `BugBunny::CommunicationError` (cubre también `Bunny::ConnectionClosedError` in-flight sobre conn rota mid-publish). `BugBunny.create_connection` también envuelve (Railtie, scripts, tests). YARD `@raise` actualizado. — @Gabriel
|
|
9
|
+
|
|
10
|
+
### Mejoras internas
|
|
11
|
+
- `Producer#confirmed` rescue estrechado de `rescue StandardError` a `rescue Bunny::Exception` — no traga bugs internos (`NoMethodError`, `KeyError`) bajo la etiqueta de fallo de transporte. — @Gabriel
|
|
12
|
+
- `CommunicationError` docstring expandido: declara que envuelve cualquier `Bunny::Exception`, no solo TCP, y que `.cause` preserva la original. — @Gabriel
|
|
13
|
+
- `Client#run_in_pool` refactor: extraído `execute_in_pool` privado para mantener `Metrics/AbcSize`. — @Gabriel
|
|
14
|
+
|
|
15
|
+
### Documentación
|
|
16
|
+
- `docs/behavior/behavior.md` refrescado scoped (RFC-001 §3.3.3 vía quality-code Paso 5): nota de contrato de error wrapping en flujos Fire-and-forget y Confirmed; nueva entrada en §3 Inferencias; verificación humana 2026-05-26. — @Gabriel
|
|
17
|
+
- `README.md` y `skill/SKILL.md` re-indexados vía `dev-compose`: jerarquía de excepciones refinada, gotcha nuevo sobre errores de transporte 4.18+, descripción de `BugBunny::CommunicationError` en errores comunes. — @Gabriel
|
|
18
|
+
|
|
3
19
|
## [4.17.1] - 2026-05-19
|
|
4
20
|
|
|
5
21
|
> Release docs-only sobre 4.17.0 — sin cambios en `lib/` ni API pública. Incorpora la familia de skills `dev-*` (RFC-001) y los artefactos de detalle version-locked (`docs/glossary`, `docs/behavior`) que viajan en el `.gem`.
|
data/README.md
CHANGED
|
@@ -377,7 +377,7 @@ BugBunny maps RabbitMQ responses to a semantic exception hierarchy, similar to h
|
|
|
377
377
|
|
|
378
378
|
```
|
|
379
379
|
BugBunny::Error
|
|
380
|
-
├── CommunicationError (
|
|
380
|
+
├── CommunicationError (wraps any Bunny::Exception — TCP/auth/channel — at the gem boundary; .cause preserves the original)
|
|
381
381
|
├── ConfigurationError (invalid config attribute)
|
|
382
382
|
├── SecurityError (unauthorized controller resolution)
|
|
383
383
|
├── PublishNacked (broker basic.nack on :confirmed publish)
|
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 `94de2b4` · cobertura: completa (6 flujos) · verificado por humano 2026-05-18 (base) · 2026-05-26 (refresco scoped: contrato de error wrapping post-#49)
|
|
4
4
|
|
|
5
5
|
## 1. Resumen
|
|
6
6
|
|
|
@@ -68,6 +68,8 @@ sequenceDiagram
|
|
|
68
68
|
```
|
|
69
69
|
Contexto: `client.rb:129-134` → `producer.rb:47-51` → `publish_message producer.rb:146-161`.
|
|
70
70
|
|
|
71
|
+
**Errores (contrato cliente):** cualquier `Bunny::Exception` que escape durante `@pool.with` (TCP fail en `try_create`, `ConnectionClosedError` in-flight, `ChannelAlreadyClosed`, etc.) se envuelve como `BugBunny::CommunicationError` en `Client#run_in_pool` (`client.rb:155-167`). La excepción original queda en `.cause`. Los callers del `Client` no rescatan tipos `Bunny::*`.
|
|
72
|
+
|
|
71
73
|
### Flujo: Confirmed + basic.return bridge
|
|
72
74
|
ACK del broker síncrono; `basic.return` (mandatory unroutable) llega en el reader thread de Bunny y se puentea al hilo de publish vía `@pending_returns` + `Concurrent::Event` con ventana de tolerancia GVL.
|
|
73
75
|
|
|
@@ -92,7 +94,7 @@ sequenceDiagram
|
|
|
92
94
|
P-->>CL: PublishUnroutable (o éxito si slot vacío)
|
|
93
95
|
Note over RT,S: on_return corre antes del raise · su excepción se loggea (no propaga)
|
|
94
96
|
```
|
|
95
|
-
Contexto: `producer.rb:72-93,200-216,281-345`; `session.rb:70-74,204-250`. Branches: ack→ok · nack→`PublishNacked` (`producer.rb:235`) · return→`PublishUnroutable` (`producer.rb:325`) · timeout→`RequestTimeout` (`producer.rb:214-215`).
|
|
97
|
+
Contexto: `producer.rb:72-93,200-216,281-345`; `session.rb:70-74,204-250`. Branches: ack→ok · nack→`PublishNacked` (`producer.rb:235`) · return→`PublishUnroutable` (`producer.rb:325`) · timeout→`RequestTimeout` (`producer.rb:214-215`) · canal/conn AMQP rota (`Bunny::Exception`)→`CommunicationError` (envuelta por `Producer#confirmed` `producer.rb:87-90` y/o `Client#run_in_pool` `client.rb:155-167`; `.cause` preserva la original).
|
|
96
98
|
|
|
97
99
|
### Flujo: Consumer subscribe loop + reconnect + health
|
|
98
100
|
Loop bloqueante infinito con backoff exponencial + jitter en error; health check en TimerTask separado, **acoplado flojo** vía estado de session.
|
|
@@ -166,6 +168,7 @@ Contexto: `middleware/stack.rb:31-47` (build L43-47, `reverse.inject`), `base.rb
|
|
|
166
168
|
| 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) |
|
|
167
169
|
| 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 |
|
|
168
170
|
| Health check acoplado flojo al loop vía cierre de session | inferred | confirmar `consumer.rb:340-361` vs `106-124` |
|
|
171
|
+
| 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` |
|
|
169
172
|
|
|
170
173
|
## 4. Cobertura y fronteras
|
|
171
174
|
|
|
@@ -173,4 +176,4 @@ Contexto: `middleware/stack.rb:31-47` (build L43-47, `reverse.inject`), `base.rb
|
|
|
173
176
|
- **Lógica dispersa marcada (no fingida):** health check (thread aparte vía `Concurrent::TimerTask`, acople flojo con el loop sólo vía cierre de session). RFC-007: marcada, no diagramada como flujo limpio falso. (El orden onion del middleware NO es lógica dispersa: es mecanismo limpio y documentado en `stack.rb:37-39`.)
|
|
174
177
|
- **Fuera de alcance:** estructura (operaciones/interfaz/topología) → dev-structure F2 (no implementado). Significado de términos → `glossary.md`. Datos → n/a (sin DB).
|
|
175
178
|
- **Frescura:** PR que renombre/mueva un método citado → reviewer actualiza el diagrama y `file:line` en el mismo PR (RFC-001 §3.3).
|
|
176
|
-
- **Verificación humana:** completada 2026-05-18 (invariante RFC-001 §3.3 satisfecho). Re-verificar en cada PR que toque un flujo citado (frescura).
|
|
179
|
+
- **Verificación humana:** base completada 2026-05-18; refresco scoped 2026-05-26 (contrato de error wrapping post-#49 en Fire-and-forget y Confirmed; invariante RFC-001 §3.3 satisfecho). Re-verificar en cada PR que toque un flujo citado (frescura).
|
data/lib/bug_bunny/client.rb
CHANGED
|
@@ -138,9 +138,19 @@ module BugBunny
|
|
|
138
138
|
# Ejecuta la lógica de envío dentro del contexto del Pool.
|
|
139
139
|
# Mapea los argumentos al objeto Request y ejecuta la cadena de middlewares.
|
|
140
140
|
#
|
|
141
|
+
# Cualquier `Bunny::Exception` que nazca durante la adquisición de la conexión
|
|
142
|
+
# (`@pool.with` → `try_create`) o durante operaciones sobre una conexión rota
|
|
143
|
+
# in-flight (canal cerrado, conn perdida) se envuelve en
|
|
144
|
+
# {BugBunny::CommunicationError}. Esto preserva el contrato de abstracción del
|
|
145
|
+
# gem: los callers del `Client` no deberían tener que rescatar tipos de
|
|
146
|
+
# `Bunny::*`. La excepción original queda accesible vía `.cause` (Ruby la
|
|
147
|
+
# preserva automáticamente al re-raisear dentro del `rescue`).
|
|
148
|
+
#
|
|
141
149
|
# @param url [String] La ruta destino.
|
|
142
150
|
# @param args [Hash] Argumentos pasados a los métodos públicos.
|
|
143
151
|
# @yield [req] Bloque para configuración adicional del Request.
|
|
152
|
+
# @raise [BugBunny::CommunicationError] Si la conexión TCP/AMQP falla durante
|
|
153
|
+
# la adquisición del slot del pool o durante la operación.
|
|
144
154
|
def run_in_pool(url, args)
|
|
145
155
|
req = build_request(url, args)
|
|
146
156
|
|
|
@@ -151,8 +161,19 @@ module BugBunny
|
|
|
151
161
|
# de los keyword args. Evaluamos el warning sobre el estado final del Request.
|
|
152
162
|
warn_return_raise_misuse(req)
|
|
153
163
|
|
|
154
|
-
|
|
155
|
-
|
|
164
|
+
execute_in_pool(req)
|
|
165
|
+
rescue BugBunny::Error
|
|
166
|
+
raise
|
|
167
|
+
rescue Bunny::Exception => e
|
|
168
|
+
raise BugBunny::CommunicationError, "AMQP failure on path=#{req&.path}: #{e.class}: #{e.message}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Ejecuta el Request dentro del Pool. Session y Producer se reutilizan por
|
|
172
|
+
# slot de conexión (ver #session_for / #producer_for).
|
|
173
|
+
#
|
|
174
|
+
# @param req [BugBunny::Request]
|
|
175
|
+
# @return [Hash] Respuesta del producer.
|
|
176
|
+
def execute_in_pool(req)
|
|
156
177
|
@pool.with do |conn|
|
|
157
178
|
session = session_for(conn)
|
|
158
179
|
producer = producer_for(conn, session)
|
data/lib/bug_bunny/exception.rb
CHANGED
|
@@ -7,8 +7,24 @@ module BugBunny
|
|
|
7
7
|
# Permite capturar cualquier error de la librería con un `rescue BugBunny::Error`.
|
|
8
8
|
class Error < ::StandardError; end
|
|
9
9
|
|
|
10
|
-
# Error lanzado cuando ocurren problemas de red o
|
|
11
|
-
#
|
|
10
|
+
# Error lanzado cuando ocurren problemas de red, conexión o protocolo AMQP con RabbitMQ.
|
|
11
|
+
#
|
|
12
|
+
# Envuelve cualquier `Bunny::Exception` (TCP fail, auth fail, canal cerrado,
|
|
13
|
+
# `PreconditionFailed`, `ConnectionClosedError`, etc.) en las fronteras de
|
|
14
|
+
# abstracción del gem — `BugBunny.create_connection`, `BugBunny::Client#publish` /
|
|
15
|
+
# `#request` / `#send`, y `BugBunny::Producer#confirmed`. Los callers no deberían
|
|
16
|
+
# rescatar tipos de `Bunny::*` directamente: con `rescue BugBunny::CommunicationError`
|
|
17
|
+
# alcanza para cubrir cualquier fallo de transporte/broker.
|
|
18
|
+
#
|
|
19
|
+
# La excepción original queda accesible vía `.cause` (Ruby la preserva
|
|
20
|
+
# automáticamente al re-raisear dentro del `rescue`).
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# begin
|
|
24
|
+
# client.publish('evt', exchange: 'x', body: payload)
|
|
25
|
+
# rescue BugBunny::CommunicationError => e
|
|
26
|
+
# logger.error("publish failed: #{e.message} cause=#{e.cause&.class}")
|
|
27
|
+
# end
|
|
12
28
|
class CommunicationError < Error; end
|
|
13
29
|
|
|
14
30
|
# Error lanzado cuando la configuración de la gema es inválida.
|
data/lib/bug_bunny/producer.rb
CHANGED
|
@@ -86,8 +86,8 @@ module BugBunny
|
|
|
86
86
|
{ 'status' => 202, 'body' => nil }
|
|
87
87
|
rescue BugBunny::Error
|
|
88
88
|
raise
|
|
89
|
-
rescue
|
|
90
|
-
raise BugBunny::CommunicationError, "Publisher confirms failed: #{e.message}"
|
|
89
|
+
rescue Bunny::Exception => e
|
|
90
|
+
raise BugBunny::CommunicationError, "Publisher confirms failed: #{e.class}: #{e.message}"
|
|
91
91
|
ensure
|
|
92
92
|
teardown_return_listener(request, return_listener)
|
|
93
93
|
end
|
data/lib/bug_bunny/version.rb
CHANGED
data/lib/bug_bunny.rb
CHANGED
|
@@ -80,7 +80,10 @@ module BugBunny
|
|
|
80
80
|
# @option options [Integer] :continuation_timeout (15000) Timeout para operaciones RPC internas.
|
|
81
81
|
#
|
|
82
82
|
# @return [Bunny::Session] Una sesión de Bunny ya iniciada (`start` ya invocado).
|
|
83
|
-
# @raise [
|
|
83
|
+
# @raise [BugBunny::CommunicationError] Si no se puede establecer la conexión
|
|
84
|
+
# (TCP fail, auth fail, vhost inválido, etc.). Envuelve cualquier
|
|
85
|
+
# `Bunny::Exception` para preservar la frontera de abstracción del gem.
|
|
86
|
+
# La excepción original queda en `.cause`.
|
|
84
87
|
def self.create_connection(**options)
|
|
85
88
|
conn_options = merge_connection_options(options)
|
|
86
89
|
Bunny.new(conn_options).tap do |conn|
|
|
@@ -89,6 +92,10 @@ module BugBunny
|
|
|
89
92
|
end
|
|
90
93
|
conn.start
|
|
91
94
|
end
|
|
95
|
+
rescue Bunny::Exception => e
|
|
96
|
+
raise BugBunny::CommunicationError,
|
|
97
|
+
"Failed to establish AMQP connection to #{conn_options[:host]}:#{conn_options[:port]}: " \
|
|
98
|
+
"#{e.class}: #{e.message}"
|
|
92
99
|
end
|
|
93
100
|
|
|
94
101
|
# Cierra la conexión global si existe.
|
data/skill/SKILL.md
CHANGED
|
@@ -19,7 +19,7 @@ Gema Ruby: capa de routing RESTful sobre AMQP/RabbitMQ. Microservicios se comuni
|
|
|
19
19
|
|
|
20
20
|
## Contrato resumido (piso mínimo)
|
|
21
21
|
|
|
22
|
-
> Resume el contrato de **`bug_bunny` 4.
|
|
22
|
+
> Resume el contrato de **`bug_bunny` 4.18.0**. Suficiente para el uso típico sin abrir el detalle; el detalle version-locked está en [`../docs/behavior/behavior.md`](../docs/behavior/behavior.md) (6 flujos) y [`../docs/glossary/glossary.md`](../docs/glossary/glossary.md) (símbolos→significado). Antipatrones/API completa: más abajo (embebido interim, ver Cobertura y fronteras).
|
|
23
23
|
|
|
24
24
|
**Símbolos públicos clave**
|
|
25
25
|
|
|
@@ -31,7 +31,7 @@ Gema Ruby: capa de routing RESTful sobre AMQP/RabbitMQ. Microservicios se comuni
|
|
|
31
31
|
| `BugBunny::Controller` | `before/around/after_action`, `rescue_from`, `render status:, json:` |
|
|
32
32
|
| `BugBunny.routes.draw` | `resources :x` · `namespace` · `member`/`collection` |
|
|
33
33
|
| `BugBunny.configure` | `host/port/username/password` · `rpc_timeout` (default 10) · `nack_raise`/`return_raise` (default `true`) · `on_return` |
|
|
34
|
-
| Excepciones | `RemoteError` (500 con backtrace remoto) · `PublishNacked` · `PublishUnroutable` · `RequestTimeout` · `NotFound` · `UnprocessableEntity` |
|
|
34
|
+
| Excepciones | `RemoteError` (500 con backtrace remoto) · `PublishNacked` · `PublishUnroutable` · `RequestTimeout` · `NotFound` · `UnprocessableEntity` · `CommunicationError` (envuelve cualquier `Bunny::Exception` — TCP/conn/canal; `.cause` preserva original) |
|
|
35
35
|
|
|
36
36
|
**Uso típico**
|
|
37
37
|
|
|
@@ -54,6 +54,7 @@ client.publish('events', body: { type: 'x' }) # => { 'status' => 202 }
|
|
|
54
54
|
- `confirmed:true + mandatory:true` con `return_raise` (default `true`) → `PublishUnroutable` si no rutea.
|
|
55
55
|
- `BugBunny::Consumer.subscribe` requiere `connection:`. No correr el Consumer en threads de Puma (loop bloqueante).
|
|
56
56
|
- `exchange_options: { durable: true }` debe matchear la declaración del consumer, o `Bunny::PreconditionFailed`.
|
|
57
|
+
- **Errores de transporte (4.18+):** TCP fail, conn rota, canal cerrado → siempre `BugBunny::CommunicationError`. No rescatar `Bunny::TCPConnectionFailed`/`ConnectionClosedError` directo — quedó atrás de la frontera. La original sigue accesible vía `.cause`.
|
|
57
58
|
|
|
58
59
|
## Índice de artefactos (fuente de verdad)
|
|
59
60
|
|
|
@@ -431,8 +432,8 @@ Limitación de RSpec: `instance_double` valida que el método exista pero **no**
|
|
|
431
432
|
**Resolución:** `rescue BugBunny::RemoteError => e` y acceder a `e.original_class`, `e.original_message`, `e.original_backtrace`. Revisar logs del consumer (`event=controller.unhandled_exception`).
|
|
432
433
|
|
|
433
434
|
### BugBunny::CommunicationError
|
|
434
|
-
**Causa:** Fallo de
|
|
435
|
-
**Resolución:** Verificar conectividad a RabbitMQ. Revisar `max_reconnect_attempts` y logs de reconexión.
|
|
435
|
+
**Causa:** Fallo de transporte AMQP — envuelve cualquier `Bunny::Exception` que escape en la frontera del gem (`Client#publish`/`#request`/`#send`, `Producer#confirmed`, `BugBunny.create_connection`). Cubre TCP fail (`Bunny::TCPConnectionFailed`), conn rota in-flight (`ConnectionClosedError`), canal cerrado (`ChannelAlreadyClosed`), auth fail, etc. La excepción original queda en `.cause`.
|
|
436
|
+
**Resolución:** Verificar conectividad a RabbitMQ (host/port/auth/vhost). Inspeccionar `e.cause` para clasificar el fallo concreto. Revisar `max_reconnect_attempts` y logs de reconexión.
|
|
436
437
|
|
|
437
438
|
Ver catálogo completo en [Errores](references/errores.md).
|
|
438
439
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'support/bunny_mocks'
|
|
5
|
+
|
|
6
|
+
# Specs para el contrato de envoltura definido en issue #49:
|
|
7
|
+
# cualquier `Bunny::Exception` que escape de las fronteras de abstracción del
|
|
8
|
+
# gem (`BugBunny.create_connection`, `BugBunny::Client#publish`/`#request`/`#send`,
|
|
9
|
+
# `BugBunny::Producer#confirmed`) debe re-raisearse como
|
|
10
|
+
# `BugBunny::CommunicationError`, preservando la excepción original en `.cause`.
|
|
11
|
+
RSpec.describe 'Bunny::Exception → BugBunny::CommunicationError wrapping' do
|
|
12
|
+
include BunnyMocks
|
|
13
|
+
|
|
14
|
+
describe 'BugBunny.create_connection' do
|
|
15
|
+
before { BugBunny.configuration ||= BugBunny::Configuration.new }
|
|
16
|
+
|
|
17
|
+
it 'envuelve Bunny::TCPConnectionFailedForAllHosts en CommunicationError' do
|
|
18
|
+
fake_session = instance_double(Bunny::Session)
|
|
19
|
+
allow(fake_session).to receive(:after_recovery_completed)
|
|
20
|
+
allow(fake_session).to receive(:start).and_raise(Bunny::TCPConnectionFailedForAllHosts)
|
|
21
|
+
allow(Bunny).to receive(:new).and_return(fake_session)
|
|
22
|
+
|
|
23
|
+
err = capture_error { BugBunny.create_connection(host: 'broker.invalid', port: 5672) }
|
|
24
|
+
expect(err).to be_a(BugBunny::CommunicationError)
|
|
25
|
+
expect(err.message).to match(/broker.invalid:5672/)
|
|
26
|
+
expect(err.cause).to be_a(Bunny::TCPConnectionFailed)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'envuelve cualquier Bunny::Exception, no solo TCP' do
|
|
30
|
+
fake_session = instance_double(Bunny::Session)
|
|
31
|
+
allow(fake_session).to receive(:after_recovery_completed)
|
|
32
|
+
allow(fake_session).to receive(:start).and_raise(Bunny::AuthenticationFailureError.new('guest', '/', 0))
|
|
33
|
+
allow(Bunny).to receive(:new).and_return(fake_session)
|
|
34
|
+
|
|
35
|
+
err = capture_error { BugBunny.create_connection }
|
|
36
|
+
expect(err).to be_a(BugBunny::CommunicationError)
|
|
37
|
+
expect(err.cause).to be_a(Bunny::AuthenticationFailureError)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe 'Client#publish — TCP fail en try_create (issue #49 caso original)' do
|
|
42
|
+
it 'envuelve Bunny::TCPConnectionFailedForAllHosts levantado dentro de @pool.with' do
|
|
43
|
+
# Pool falso que levanta la excepción raw de bunny al adquirir slot —
|
|
44
|
+
# simula exactamente lo observado en producción en el stack trace del issue.
|
|
45
|
+
raising_pool = Object.new
|
|
46
|
+
raising_pool.define_singleton_method(:with) { |&_block| raise Bunny::TCPConnectionFailedForAllHosts }
|
|
47
|
+
|
|
48
|
+
client = BugBunny::Client.new(pool: raising_pool)
|
|
49
|
+
|
|
50
|
+
err = capture_error { client.publish('evt', exchange: 'x', exchange_type: 'direct', body: { a: 1 }) }
|
|
51
|
+
expect(err).to be_a(BugBunny::CommunicationError)
|
|
52
|
+
expect(err.message).to match(/AMQP failure on path=evt/)
|
|
53
|
+
expect(err.cause).to be_a(Bunny::TCPConnectionFailed)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe 'Client — in-flight ConnectionClosedError' do
|
|
58
|
+
it 'envuelve Bunny::ConnectionClosedError levantado por el Producer' do
|
|
59
|
+
conn = BunnyMocks::FakeConnection.new(true, BunnyMocks::FakeChannel.new(true))
|
|
60
|
+
pool = Object.new.tap { |p| p.define_singleton_method(:with) { |&blk| blk.call(conn) } }
|
|
61
|
+
client = BugBunny::Client.new(pool: pool)
|
|
62
|
+
|
|
63
|
+
allow_any_instance_of(BugBunny::Producer).to receive(:fire)
|
|
64
|
+
.and_raise(Bunny::ConnectionClosedError.new('connection lost'))
|
|
65
|
+
|
|
66
|
+
err = capture_error { client.publish('evt', exchange: 'x', exchange_type: 'direct', body: { a: 1 }) }
|
|
67
|
+
expect(err).to be_a(BugBunny::CommunicationError)
|
|
68
|
+
expect(err.message).to match(/AMQP failure/)
|
|
69
|
+
expect(err.cause).to be_a(Bunny::ConnectionClosedError)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe 'Client — pass-through de BugBunny::Error' do
|
|
74
|
+
it 'no re-envuelve errores propios del gem (RequestTimeout, PublishNacked, etc.)' do
|
|
75
|
+
conn = BunnyMocks::FakeConnection.new(true, BunnyMocks::FakeChannel.new(true))
|
|
76
|
+
pool = Object.new.tap { |p| p.define_singleton_method(:with) { |&blk| blk.call(conn) } }
|
|
77
|
+
client = BugBunny::Client.new(pool: pool)
|
|
78
|
+
|
|
79
|
+
allow_any_instance_of(BugBunny::Producer).to receive(:rpc)
|
|
80
|
+
.and_raise(BugBunny::RequestTimeout.new('timeout'))
|
|
81
|
+
|
|
82
|
+
expect { client.request('foo', method: :get, exchange: 'x', exchange_type: 'direct') }
|
|
83
|
+
.to raise_error(BugBunny::RequestTimeout)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe 'Producer#confirmed — rescue limitado a Bunny::Exception' do
|
|
88
|
+
it 'no traga errores genéricos de Ruby (NoMethodError) como CommunicationError' do
|
|
89
|
+
producer = BugBunny::Producer.new(instance_double(BugBunny::Session))
|
|
90
|
+
allow(producer).to receive(:setup_return_listener).and_return(nil)
|
|
91
|
+
allow(producer).to receive(:teardown_return_listener)
|
|
92
|
+
allow(producer).to receive(:publish_message).and_raise(NoMethodError.new('bug interno'))
|
|
93
|
+
|
|
94
|
+
request = BugBunny::Request.new('evt').tap do |r|
|
|
95
|
+
r.exchange = 'x'
|
|
96
|
+
r.exchange_type = 'direct'
|
|
97
|
+
r.delivery_mode = :confirmed
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
expect { producer.confirmed(request) }.to raise_error(NoMethodError, 'bug interno')
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'envuelve Bunny::Exception como CommunicationError' do
|
|
104
|
+
producer = BugBunny::Producer.new(instance_double(BugBunny::Session))
|
|
105
|
+
allow(producer).to receive(:setup_return_listener).and_return(nil)
|
|
106
|
+
allow(producer).to receive(:teardown_return_listener)
|
|
107
|
+
allow(producer).to receive(:publish_message)
|
|
108
|
+
.and_raise(Bunny::ChannelAlreadyClosed.new('closed', double(id: 1)))
|
|
109
|
+
|
|
110
|
+
request = BugBunny::Request.new('evt').tap do |r|
|
|
111
|
+
r.exchange = 'x'
|
|
112
|
+
r.exchange_type = 'direct'
|
|
113
|
+
r.delivery_mode = :confirmed
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
err = capture_error { producer.confirmed(request) }
|
|
117
|
+
expect(err).to be_a(BugBunny::CommunicationError)
|
|
118
|
+
expect(err.message).to match(/Publisher confirms failed/)
|
|
119
|
+
expect(err.cause).to be_a(Bunny::ChannelAlreadyClosed)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def capture_error
|
|
124
|
+
yield
|
|
125
|
+
nil
|
|
126
|
+
rescue StandardError => e
|
|
127
|
+
e
|
|
128
|
+
end
|
|
129
|
+
end
|
data/spec/unit/producer_spec.rb
CHANGED
|
@@ -377,13 +377,21 @@ RSpec.describe BugBunny::Producer do
|
|
|
377
377
|
expect { confirmed_producer.confirmed(req) }.to raise_error(BugBunny::RequestTimeout, /Timeout/)
|
|
378
378
|
end
|
|
379
379
|
|
|
380
|
-
it 'envuelve
|
|
381
|
-
allow(mock_channel).to receive(:wait_for_confirms)
|
|
380
|
+
it 'envuelve Bunny::Exception del canal como BugBunny::CommunicationError' do
|
|
381
|
+
allow(mock_channel).to receive(:wait_for_confirms)
|
|
382
|
+
.and_raise(Bunny::ChannelAlreadyClosed.new('boom', double(id: 1)))
|
|
382
383
|
|
|
383
384
|
expect { confirmed_producer.confirmed(build_request) }
|
|
384
385
|
.to raise_error(BugBunny::CommunicationError, /boom/)
|
|
385
386
|
end
|
|
386
387
|
|
|
388
|
+
it 'no traga errores genéricos de Ruby (rescue es Bunny::Exception, no StandardError)' do
|
|
389
|
+
allow(mock_channel).to receive(:wait_for_confirms).and_raise(NoMethodError, 'bug interno')
|
|
390
|
+
|
|
391
|
+
expect { confirmed_producer.confirmed(build_request) }
|
|
392
|
+
.to raise_error(NoMethodError, 'bug interno')
|
|
393
|
+
end
|
|
394
|
+
|
|
387
395
|
it 'propaga BugBunny::Error sin envolver' do
|
|
388
396
|
allow(fake_exchange).to receive(:publish).and_raise(BugBunny::CommunicationError, 'chan dead')
|
|
389
397
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bug_bunny
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.18.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gabix
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|
|
@@ -283,6 +283,7 @@ files:
|
|
|
283
283
|
- spec/support/bunny_mocks.rb
|
|
284
284
|
- spec/support/integration_helper.rb
|
|
285
285
|
- spec/unit/client_session_pool_spec.rb
|
|
286
|
+
- spec/unit/communication_error_wrapping_spec.rb
|
|
286
287
|
- spec/unit/configuration_spec.rb
|
|
287
288
|
- spec/unit/consumer_middleware_spec.rb
|
|
288
289
|
- spec/unit/consumer_spec.rb
|
|
@@ -306,7 +307,7 @@ metadata:
|
|
|
306
307
|
homepage_uri: https://github.com/gedera/bug_bunny
|
|
307
308
|
source_code_uri: https://github.com/gedera/bug_bunny
|
|
308
309
|
changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
|
|
309
|
-
documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.
|
|
310
|
+
documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.18.0/skill
|
|
310
311
|
post_install_message:
|
|
311
312
|
rdoc_options: []
|
|
312
313
|
require_paths:
|