bug_bunny 4.17.1 → 4.19.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 +30 -0
- data/README.md +1 -1
- data/docs/behavior/behavior.md +6 -3
- data/docs/release/release.md +108 -0
- data/lib/bug_bunny/client.rb +23 -2
- data/lib/bug_bunny/exception.rb +51 -5
- data/lib/bug_bunny/middleware/raise_error.rb +97 -43
- 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 +10 -4
- data/skill/references/errores.md +33 -4
- data/skills.yml +5 -21
- data/spec/unit/communication_error_wrapping_spec.rb +129 -0
- data/spec/unit/producer_spec.rb +10 -2
- data/spec/unit/raise_error_spec.rb +176 -0
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c751d42f963af2dc145005dba7b6245b0dc9fa549cacecb824e6a5cc2c3232d
|
|
4
|
+
data.tar.gz: c7c602f3f96a47a85080b287fd87435bb161abcab6b5358f51b45ed05fd1428a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e8e1e583486fec890b5ccdedb77f99f1954f37e9a4217afe1aee0085ca8483f242a966643bcce5da5a7a180d0223850039a883ae9ce92b8be91a02c7beaac7b7
|
|
7
|
+
data.tar.gz: 71305eaf4dec9b4bf3fae7bf19bbb3fd997d75c409c6d5239d52030c1c7b5833f48f675f5bbcf187ef16334c0286f7a780675add36980c5b927ca592c68ab9d7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.19.0] - 2026-06-25
|
|
4
|
+
|
|
5
|
+
> Capa de errores transport-agnostic (#52). Cambio **aditivo y retrocompatible**: `.message` no se degrada y se suman dos accesores. Bump minor.
|
|
6
|
+
|
|
7
|
+
### Nuevas funcionalidades
|
|
8
|
+
- **`status` + `raw_response` uniformes en toda la jerarquía (#52):** subidos a la base `BugBunny::Error` (`attr_accessor`) y poblados por `RaiseError.on_complete` en **todas** las clases de error, no solo `UnprocessableEntity`. El consumidor accede a `e.status` / `e.raw_response` de forma uniforme; la gema queda agnóstica al payload y el boundary del servicio interpreta el envelope de dominio. — @Gabriel
|
|
9
|
+
|
|
10
|
+
### Correcciones
|
|
11
|
+
- **`.message` ya no vuelca un `Hash#inspect` con el envelope anidado (#52):** ante `{ error: { code, message, details } }`, `format_error_message` extrae `error.message` best-effort en vez de interpolar el Hash; mantiene el shape plano histórico (incluido `error: ""`) y cae a JSON. Es solo el string humano para logs/Sentry, no contrato. — @Gabriel
|
|
12
|
+
|
|
13
|
+
### Documentación
|
|
14
|
+
- `skill/references/errores.md` y `skill/SKILL.md`: nueva sección "Materia prima del error" (`status`/`raw_response`), formato de `.message` endurecido y advertencia de sanitización (no loguear `raw_response` crudo). — @Gabriel
|
|
15
|
+
- Piloto RFC-014: `docs/release/release.md` (suplemento de release de la gema) (#51) — @Gabriel
|
|
16
|
+
|
|
17
|
+
## [4.18.0] - 2026-05-26
|
|
18
|
+
|
|
19
|
+
> 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.
|
|
20
|
+
|
|
21
|
+
### Correcciones
|
|
22
|
+
- **`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
|
|
23
|
+
|
|
24
|
+
### Mejoras internas
|
|
25
|
+
- `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
|
|
26
|
+
- `CommunicationError` docstring expandido: declara que envuelve cualquier `Bunny::Exception`, no solo TCP, y que `.cause` preserva la original. — @Gabriel
|
|
27
|
+
- `Client#run_in_pool` refactor: extraído `execute_in_pool` privado para mantener `Metrics/AbcSize`. — @Gabriel
|
|
28
|
+
|
|
29
|
+
### Documentación
|
|
30
|
+
- `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
|
|
31
|
+
- `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
|
|
32
|
+
|
|
3
33
|
## [4.17.1] - 2026-05-19
|
|
4
34
|
|
|
5
35
|
> 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).
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Release — bug_bunny
|
|
2
|
+
|
|
3
|
+
> meta: artefacto release · RFC-014 (shape v2.0, `proposed`) · generado
|
|
4
|
+
> manualmente (**piloto RFC-014** — suplemento gema, valida patrón 1 +
|
|
5
|
+
> per-repo-visible) · anclado a `.github/workflows/release.yml`, `*.gemspec`,
|
|
6
|
+
> `lib/bug_bunny/version.rb`, `CHANGELOG.md`, git · fecha 2026-06-01 · cobertura:
|
|
7
|
+
> completa (régimen gema: build→publish, §e/f/g n/a).
|
|
8
|
+
|
|
9
|
+
## 1. Resumen
|
|
10
|
+
|
|
11
|
+
Cómo se libera esta gema. **Patrón 1 (gema-tag):** push de tag `vX.X.X` → un
|
|
12
|
+
**GitHub Actions workflow que vive en el repo** (`.github/workflows/release.yml`)
|
|
13
|
+
→ `gem build` + `gem push` a **RubyGems público**. A diferencia de un servicio,
|
|
14
|
+
el pipeline **no es opaco**: vive en `.github/`, es auditable y versionado con el
|
|
15
|
+
código (`dueño: per-repo-visible`). Una gema **no despliega — publica**: §e
|
|
16
|
+
(deploy), §f (ambientes), §g (rollback) son **n/a** (rollback = `gem yank`,
|
|
17
|
+
out-of-repo).
|
|
18
|
+
|
|
19
|
+
## 2. Cuerpo
|
|
20
|
+
|
|
21
|
+
### a. Hecho verificable
|
|
22
|
+
|
|
23
|
+
- **Convención de versión:** SemVer `vX.X.X`. Actual: **4.18.0**.
|
|
24
|
+
- **Source of truth:** tag remoto (`v4.18.0`) + **triple mirror**
|
|
25
|
+
`lib/bug_bunny/version.rb` (`VERSION = '4.18.0'`) ← `bug_bunny.gemspec:7`
|
|
26
|
+
(`spec.version = BugBunny::VERSION`).
|
|
27
|
+
- **Changelog canónico:** `CHANGELOG.md` único.
|
|
28
|
+
- **Patrón de trigger:** `gema-tag` (patrón 1).
|
|
29
|
+
- **Salida:** gema publicada en **RubyGems público**.
|
|
30
|
+
- **Deploy / ambientes / rollback:** **n/a** (gema publica, no despliega).
|
|
31
|
+
|
|
32
|
+
### b. Versionado
|
|
33
|
+
|
|
34
|
+
- **Convención:** SemVer `vX.X.X` (**con `v`** — distinto al servicio).
|
|
35
|
+
- **Source of truth:** tag remoto canónico (`git tag --sort=-v:refname` →
|
|
36
|
+
`v4.18.0`).
|
|
37
|
+
- **Mirror:** `lib/bug_bunny/version.rb` (`VERSION`), leído por
|
|
38
|
+
`bug_bunny.gemspec:7` (`spec.version = BugBunny::VERSION`).
|
|
39
|
+
`required_ruby_version >= 2.6.0` (`bug_bunny.gemspec:17`).
|
|
40
|
+
- **Política de divergencia:** `gem-release` lee el tag remoto; si `version.rb`
|
|
41
|
+
difiere → warn (posible release sin taguear). El bump se calcula sobre el tag.
|
|
42
|
+
|
|
43
|
+
### c. Changelog
|
|
44
|
+
|
|
45
|
+
- **Canónico único:** `CHANGELOG.md` ✓.
|
|
46
|
+
- **Formato:** `## [X.X.X] - YYYY-MM-DD` + nota de behavior-change si aplica +
|
|
47
|
+
categorías (`### Correcciones`), atribución `— @autor`, **link al issue**
|
|
48
|
+
(ej. `(#49)`). Cruza RFC-013 §h: el changelog ya referencia el incidente que
|
|
49
|
+
motivó el fix.
|
|
50
|
+
|
|
51
|
+
### d. Trigger → pipeline → salida
|
|
52
|
+
|
|
53
|
+
| Patrón | Trigger (señal per-repo) | Pipeline | Salida | Dueño |
|
|
54
|
+
|---|---|---|---|---|
|
|
55
|
+
| `gema-tag` | push tag `vX.X.X` | GH Actions `.github/workflows/release.yml` | gema en **RubyGems público** | **`per-repo-visible`** |
|
|
56
|
+
|
|
57
|
+
- **Pipeline NO opaco:** `.github/workflows/release.yml` —
|
|
58
|
+
`on: push: tags: ['v*']` → `ruby/setup-ruby@v1` → `gem build *.gemspec` +
|
|
59
|
+
`gem push *.gem` (auth `secrets.RUBYGEMS_API_KEY`). Auditable y versionado con
|
|
60
|
+
el código; se ancla a `file:line`, no se referencia como caja negra.
|
|
61
|
+
- **Consumo:** los servicios la pinnean por versión (`gem "bug_bunny", "~> 4.18.0"`)
|
|
62
|
+
desde RubyGems — **no** git-source.
|
|
63
|
+
|
|
64
|
+
### e. Deploy / publish
|
|
65
|
+
|
|
66
|
+
**n/a (deploy)** — una gema no despliega. El "release" es **publish** a RubyGems,
|
|
67
|
+
cubierto en §d. No hay ambiente de runtime propio.
|
|
68
|
+
|
|
69
|
+
### f. Ambientes
|
|
70
|
+
|
|
71
|
+
**n/a** — no hay `dev`/`staging`/`prod`. La única salida es la gema publicada.
|
|
72
|
+
|
|
73
|
+
### g. Rollback
|
|
74
|
+
|
|
75
|
+
**n/a en el repo** — el rollback de una gema es **`gem yank <version>`** en
|
|
76
|
+
RubyGems (admin out-of-repo), no un cambio de código. No se documenta como
|
|
77
|
+
procedimiento per-repo porque no vive acá. Una versión yankeada se anotaría en
|
|
78
|
+
`CHANGELOG.md` (con su razón) si pasara.
|
|
79
|
+
|
|
80
|
+
### h. Dependencias de deploy inter-servicio
|
|
81
|
+
|
|
82
|
+
- **Consumidores** (cruza RFC-018): servicios del fleet la pinnean
|
|
83
|
+
`~> 4.18.0` (semántica minor-compatible). Un cambio de contrato del gem
|
|
84
|
+
(ej. el behavior-change de `4.18.0` — `Bunny::Exception` → `CommunicationError`)
|
|
85
|
+
obliga a los consumidores a migrar; el `CHANGELOG.md` lo documenta como
|
|
86
|
+
breaking note. **Orden de deploy:** los consumidores adoptan al hacer `bundle
|
|
87
|
+
update bug_bunny` — no hay deploy coordinado (cada servicio elige cuándo).
|
|
88
|
+
|
|
89
|
+
### i. Contrato con la skill productora
|
|
90
|
+
|
|
91
|
+
- **Skill:** `gem-release` (tag-based, **aplica** — patrón 1 emite tag).
|
|
92
|
+
- **Qué espera del repo:** `lib/bug_bunny/version.rb` como source, `*.gemspec`
|
|
93
|
+
que lo lee, `CHANGELOG.md` canónico, tag `vX.X.X`, gate `quality-code` verde,
|
|
94
|
+
y el `.github/workflows/release.yml` que publica en el tag.
|
|
95
|
+
- **Estado:** conforma — layout estándar de gema, workflow per-repo presente.
|
|
96
|
+
|
|
97
|
+
## 3. Inferencias
|
|
98
|
+
|
|
99
|
+
- Ninguna relevante: el pipeline de gema es **visible** (`.github/`), así que el
|
|
100
|
+
artefacto se ancla a archivos reales, no a inferencia sobre cajas negras.
|
|
101
|
+
|
|
102
|
+
## 4. Cobertura y fronteras
|
|
103
|
+
|
|
104
|
+
- Cobertura **completa** del régimen gema (build→publish). §e/f/g `n/a` honesto.
|
|
105
|
+
- Contraste validado con el piloto servicio (`box_radius_manager`): allá el
|
|
106
|
+
pipeline es **opaco** (`fleet-link` Codefresh); acá es **per-repo-visible**
|
|
107
|
+
(GH Actions en el repo). Misma RFC, dos loci de dueño — el valor de la columna
|
|
108
|
+
`dueño` de §d.
|
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
|
@@ -5,10 +5,55 @@ require 'json'
|
|
|
5
5
|
module BugBunny
|
|
6
6
|
# Clase base para todas las excepciones lanzadas por la gema BugBunny.
|
|
7
7
|
# Permite capturar cualquier error de la librería con un `rescue BugBunny::Error`.
|
|
8
|
-
|
|
8
|
+
#
|
|
9
|
+
# Para los errores derivados de una respuesta RPC (los que levanta
|
|
10
|
+
# {BugBunny::Middleware::RaiseError}), expone de forma uniforme la **materia
|
|
11
|
+
# prima** del error: el `status` y el `raw_response` (cuerpo crudo). La gema es
|
|
12
|
+
# **agnóstica al payload**: no interpreta la estructura del cuerpo de error: es
|
|
13
|
+
# el boundary de cada servicio quien lee `raw_response` y decide la semántica
|
|
14
|
+
# de dominio (códigos, detalles de validación, etc.).
|
|
15
|
+
#
|
|
16
|
+
# @example Leer la materia prima en el boundary del servicio
|
|
17
|
+
# rescue BugBunny::Error => e
|
|
18
|
+
# e.status # => 409
|
|
19
|
+
# e.raw_response # => { "error" => { "code" => "...", "message" => "...", "details" => {} } }
|
|
20
|
+
class Error < ::StandardError
|
|
21
|
+
# @return [Hash, String, nil] El cuerpo crudo de la respuesta de error, tal
|
|
22
|
+
# como llegó por el wire. `nil` para errores que no provienen de una
|
|
23
|
+
# respuesta RPC (ej: {CommunicationError}, {ConfigurationError}).
|
|
24
|
+
#
|
|
25
|
+
# @note **No loguear ni enviar a sinks (Sentry/logs) sin sanitizar.** El
|
|
26
|
+
# cuerpo crudo puede contener datos sensibles (p. ej. en `details`). Antes
|
|
27
|
+
# de cualquier sink, filtrar las claves sensibles del fleet
|
|
28
|
+
# (`password|pass|passwd|secret|token|api_key|auth`) → `[FILTERED]`. La
|
|
29
|
+
# gema entrega el cuerpo crudo a propósito; sanitizarlo es responsabilidad
|
|
30
|
+
# del consumidor.
|
|
31
|
+
attr_accessor :raw_response
|
|
32
|
+
|
|
33
|
+
# @return [Integer, nil] El código de estado de la respuesta que originó el
|
|
34
|
+
# error (ej: 400, 404, 409, 422, 500). `nil` para errores que no provienen
|
|
35
|
+
# de una respuesta RPC.
|
|
36
|
+
attr_accessor :status
|
|
37
|
+
end
|
|
9
38
|
|
|
10
|
-
# Error lanzado cuando ocurren problemas de red o
|
|
11
|
-
#
|
|
39
|
+
# Error lanzado cuando ocurren problemas de red, conexión o protocolo AMQP con RabbitMQ.
|
|
40
|
+
#
|
|
41
|
+
# Envuelve cualquier `Bunny::Exception` (TCP fail, auth fail, canal cerrado,
|
|
42
|
+
# `PreconditionFailed`, `ConnectionClosedError`, etc.) en las fronteras de
|
|
43
|
+
# abstracción del gem — `BugBunny.create_connection`, `BugBunny::Client#publish` /
|
|
44
|
+
# `#request` / `#send`, y `BugBunny::Producer#confirmed`. Los callers no deberían
|
|
45
|
+
# rescatar tipos de `Bunny::*` directamente: con `rescue BugBunny::CommunicationError`
|
|
46
|
+
# alcanza para cubrir cualquier fallo de transporte/broker.
|
|
47
|
+
#
|
|
48
|
+
# La excepción original queda accesible vía `.cause` (Ruby la preserva
|
|
49
|
+
# automáticamente al re-raisear dentro del `rescue`).
|
|
50
|
+
#
|
|
51
|
+
# @example
|
|
52
|
+
# begin
|
|
53
|
+
# client.publish('evt', exchange: 'x', body: payload)
|
|
54
|
+
# rescue BugBunny::CommunicationError => e
|
|
55
|
+
# logger.error("publish failed: #{e.message} cause=#{e.cause&.class}")
|
|
56
|
+
# end
|
|
12
57
|
class CommunicationError < Error; end
|
|
13
58
|
|
|
14
59
|
# Error lanzado cuando la configuración de la gema es inválida.
|
|
@@ -175,8 +220,9 @@ module BugBunny
|
|
|
175
220
|
# @return [Hash, Array, String] Los mensajes de error listos para ser iterados.
|
|
176
221
|
attr_reader :error_messages
|
|
177
222
|
|
|
178
|
-
#
|
|
179
|
-
|
|
223
|
+
# `raw_response` y `status` se heredan de {BugBunny::Error} (poblados por
|
|
224
|
+
# {BugBunny::Middleware::RaiseError}); `raw_response` además se setea acá en
|
|
225
|
+
# el constructor para mantener el comportamiento histórico.
|
|
180
226
|
|
|
181
227
|
# Inicializa la excepción procesando el cuerpo de la respuesta.
|
|
182
228
|
#
|
|
@@ -34,71 +34,125 @@ module BugBunny
|
|
|
34
34
|
body = response['body']
|
|
35
35
|
|
|
36
36
|
case status
|
|
37
|
-
when 200..299
|
|
38
|
-
|
|
39
|
-
when
|
|
40
|
-
|
|
41
|
-
when
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
when
|
|
46
|
-
|
|
47
|
-
when 409
|
|
48
|
-
raise BugBunny::Conflict, format_error_message(body)
|
|
49
|
-
when 422
|
|
50
|
-
# Pasamos el body crudo; UnprocessableEntity lo procesará en exception.rb
|
|
51
|
-
raise BugBunny::UnprocessableEntity, body
|
|
52
|
-
when 500..599
|
|
53
|
-
if body.is_a?(Hash) && body['bug_bunny_exception']
|
|
54
|
-
exception_data = body['bug_bunny_exception']
|
|
55
|
-
raise BugBunny::RemoteError.new(
|
|
56
|
-
exception_data['class'],
|
|
57
|
-
exception_data['message'],
|
|
58
|
-
exception_data['backtrace'] || []
|
|
59
|
-
)
|
|
60
|
-
end
|
|
61
|
-
raise BugBunny::InternalServerError, format_error_message(body)
|
|
62
|
-
else
|
|
63
|
-
handle_unknown_error(status, body)
|
|
37
|
+
when 200..299 then nil # Flujo normal (Success)
|
|
38
|
+
when 400 then raise_typed(BugBunny::BadRequest.new(format_error_message(body)), status, body)
|
|
39
|
+
when 404 then raise_not_found(status, body)
|
|
40
|
+
when 406 then raise_typed(BugBunny::NotAcceptable.new, status, body)
|
|
41
|
+
when 408 then raise_typed(BugBunny::RequestTimeout.new, status, body)
|
|
42
|
+
when 409 then raise_typed(BugBunny::Conflict.new(format_error_message(body)), status, body)
|
|
43
|
+
# Pasamos el body crudo; UnprocessableEntity lo procesará en exception.rb
|
|
44
|
+
when 422 then raise_typed(BugBunny::UnprocessableEntity.new(body), status, body)
|
|
45
|
+
when 500..599 then raise_server_error(status, body)
|
|
46
|
+
else handle_unknown_error(status, body)
|
|
64
47
|
end
|
|
65
48
|
end
|
|
66
49
|
|
|
67
50
|
private
|
|
68
51
|
|
|
69
|
-
#
|
|
52
|
+
# Levanta el error de servidor (5xx). Si el worker remoto serializó su
|
|
53
|
+
# excepción en `bug_bunny_exception`, propaga un {BugBunny::RemoteError} con
|
|
54
|
+
# la traza original; si no, un {BugBunny::InternalServerError} genérico.
|
|
70
55
|
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
56
|
+
# @param status [Integer] El código de estado (rango 500..599).
|
|
57
|
+
# @param body [Hash, String, nil] El cuerpo crudo de la respuesta.
|
|
58
|
+
# @raise [BugBunny::RemoteError] Si el cuerpo trae `bug_bunny_exception`.
|
|
59
|
+
# @raise [BugBunny::InternalServerError] Para 5xx genéricos.
|
|
60
|
+
def raise_server_error(status, body)
|
|
61
|
+
if body.is_a?(Hash) && body['bug_bunny_exception']
|
|
62
|
+
data = body['bug_bunny_exception']
|
|
63
|
+
remote = BugBunny::RemoteError.new(data['class'], data['message'], data['backtrace'] || [])
|
|
64
|
+
raise_typed(remote, status, body)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
raise_typed(BugBunny::InternalServerError.new(format_error_message(body)), status, body)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Puebla la materia prima del error (`status` + `raw_response`) en la
|
|
71
|
+
# excepción y la levanta. Aplica de forma uniforme a **todas** las clases de
|
|
72
|
+
# error derivadas de una respuesta RPC, no solo a {BugBunny::UnprocessableEntity}.
|
|
73
|
+
#
|
|
74
|
+
# La gema es agnóstica al payload: entrega el cuerpo crudo tal cual para que
|
|
75
|
+
# el boundary del servicio lo interprete; no parsea su estructura.
|
|
76
|
+
#
|
|
77
|
+
# @param error [BugBunny::Error] La excepción ya construida.
|
|
78
|
+
# @param status [Integer] El código de estado de la respuesta.
|
|
79
|
+
# @param body [Hash, String, nil] El cuerpo crudo de la respuesta.
|
|
80
|
+
# @raise [BugBunny::Error] Siempre levanta la excepción recibida.
|
|
81
|
+
# @return [void]
|
|
82
|
+
def raise_typed(error, status, body)
|
|
83
|
+
error.status = status
|
|
84
|
+
error.raw_response = body
|
|
85
|
+
raise error
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Formatea el cuerpo de la respuesta de error en un **string humano**
|
|
89
|
+
# best-effort para logs/Sentry. Es de mejor esfuerzo, **no un contrato**: la
|
|
90
|
+
# gema no expone `code`/`details` como API; quien los necesite los lee desde
|
|
91
|
+
# `raw_response` en el boundary del servicio.
|
|
92
|
+
#
|
|
93
|
+
# Soporta dos shapes del cuerpo, en orden de prioridad:
|
|
94
|
+
#
|
|
95
|
+
# 1. **Envelope anidado** `{ "error": { "message": "...", ... } }`: extrae
|
|
96
|
+
# `error.message`.
|
|
97
|
+
# 2. **Shape plano** `{ "error": "texto", "detail": "..." }`: concatena
|
|
98
|
+
# `error` + `detail`.
|
|
99
|
+
#
|
|
100
|
+
# Si ninguno aplica, cae a `body.to_json` para no volcar un `Hash#inspect`
|
|
101
|
+
# ilegible en los logs.
|
|
102
|
+
#
|
|
103
|
+
# @note Esta función **solo** arma el mensaje humano y no interpreta el
|
|
104
|
+
# detalle estructurado del cuerpo (claves como `code`/`details`/`detail`).
|
|
105
|
+
# Ese contenido vive en `raw_response` y lo interpreta el boundary del
|
|
106
|
+
# servicio; la gema se mantiene agnóstica al payload.
|
|
73
107
|
#
|
|
74
108
|
# @param body [Hash, String, nil] El cuerpo de la respuesta.
|
|
75
|
-
# @return [String] Un mensaje de error limpio y
|
|
109
|
+
# @return [String] Un mensaje de error limpio y legible.
|
|
76
110
|
def format_error_message(body)
|
|
77
111
|
return 'Unknown Error' if body.nil? || (body.respond_to?(:empty?) && body.empty?)
|
|
78
112
|
return body if body.is_a?(String)
|
|
113
|
+
return body.to_json unless body.is_a?(Hash)
|
|
114
|
+
|
|
115
|
+
human_message_from(body) || body.to_json
|
|
116
|
+
end
|
|
79
117
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
118
|
+
# Extrae el string humano del cuerpo según el shape, sin volcar Hashes.
|
|
119
|
+
#
|
|
120
|
+
# @param body [Hash] El cuerpo de la respuesta.
|
|
121
|
+
# @return [String, nil] El mensaje humano, o `nil` si el shape no lo provee.
|
|
122
|
+
# @api private
|
|
123
|
+
def human_message_from(body)
|
|
124
|
+
err = body['error']
|
|
125
|
+
|
|
126
|
+
# Envelope canónico anidado: { error: { message: "..." } }
|
|
127
|
+
if err.is_a?(Hash)
|
|
128
|
+
nested = err['message']
|
|
129
|
+
return nested if nested.is_a?(String) && !nested.empty?
|
|
130
|
+
|
|
131
|
+
return nil
|
|
87
132
|
end
|
|
133
|
+
|
|
134
|
+
# Shape plano histórico: { error: "texto", detail: "..." }.
|
|
135
|
+
# Cualquier String (incluido "") entra por acá para preservar el
|
|
136
|
+
# comportamiento histórico (la key 'error' siempre fue truthy).
|
|
137
|
+
return nil unless err.is_a?(String)
|
|
138
|
+
|
|
139
|
+
detail = body['detail'] ? " - #{body['detail']}" : ''
|
|
140
|
+
"#{err}#{detail}"
|
|
88
141
|
end
|
|
89
142
|
|
|
90
143
|
# Distingue un error de routing (ruta/controller no existe en el servicio remoto)
|
|
91
144
|
# de un 404 genérico de recurso no encontrado.
|
|
92
145
|
#
|
|
146
|
+
# @param status [Integer] El código de estado (404).
|
|
93
147
|
# @param body [Hash, String, nil] El cuerpo de la respuesta 404.
|
|
94
148
|
# @raise [BugBunny::RoutingError] Si el consumer marcó `error_type: 'routing_error'`.
|
|
95
149
|
# @raise [BugBunny::NotFound] Para 404 genéricos.
|
|
96
|
-
def raise_not_found(body)
|
|
150
|
+
def raise_not_found(status, body)
|
|
97
151
|
if body.is_a?(Hash) && body['error_type'] == 'routing_error'
|
|
98
|
-
|
|
152
|
+
raise_typed(BugBunny::RoutingError.new(format_error_message(body)), status, body)
|
|
99
153
|
end
|
|
100
154
|
|
|
101
|
-
|
|
155
|
+
raise_typed(BugBunny::NotFound.new(format_error_message(body)), status, body)
|
|
102
156
|
end
|
|
103
157
|
|
|
104
158
|
# Maneja códigos de error genéricos no mapeados explícitamente en el `case`.
|
|
@@ -111,9 +165,9 @@ module BugBunny
|
|
|
111
165
|
msg = format_error_message(body)
|
|
112
166
|
|
|
113
167
|
if status >= 500
|
|
114
|
-
|
|
168
|
+
raise_typed(BugBunny::ServerError.new("Server Error (#{status}): #{msg}"), status, body)
|
|
115
169
|
elsif status >= 400
|
|
116
|
-
|
|
170
|
+
raise_typed(BugBunny::ClientError.new("Client Error (#{status}): #{msg}"), status, body)
|
|
117
171
|
end
|
|
118
172
|
end
|
|
119
173
|
end
|
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,13 @@ 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.
|
|
437
|
+
|
|
438
|
+
**Materia prima (desde 4.19):** todo `BugBunny::Error` de respuesta RPC expone
|
|
439
|
+
`e.status` y `e.raw_response` (cuerpo crudo) de forma uniforme — no solo 422. La
|
|
440
|
+
gema es agnóstica al payload: el envelope de dominio se parsea en el boundary del
|
|
441
|
+
servicio. No loguear `raw_response` sin sanitizar.
|
|
436
442
|
|
|
437
443
|
Ver catálogo completo en [Errores](references/errores.md).
|
|
438
444
|
|
data/skill/references/errores.md
CHANGED
|
@@ -22,6 +22,30 @@ StandardError
|
|
|
22
22
|
└── BugBunny::RemoteError (500)
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## Materia prima del error (`status` + `raw_response`) — desde 4.19
|
|
26
|
+
|
|
27
|
+
Todos los errores derivados de una respuesta RPC exponen de forma **uniforme**
|
|
28
|
+
el `status` y el `raw_response` (cuerpo crudo), no solo `UnprocessableEntity`.
|
|
29
|
+
La gema es **agnóstica al payload**: entrega el cuerpo crudo tal cual y el
|
|
30
|
+
boundary del servicio decide la semántica de dominio (códigos, detalles, etc.).
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
begin
|
|
34
|
+
client.request('clusters/42', method: :get)
|
|
35
|
+
rescue BugBunny::Error => e
|
|
36
|
+
e.status # => 409 (Integer, nil si el error no viene de una respuesta RPC)
|
|
37
|
+
e.raw_response # => cuerpo crudo tal como llegó por el wire (Hash/String/nil)
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
> ⚠️ **No loguear `raw_response` crudo.** Puede contener datos sensibles. Filtrar
|
|
42
|
+
> claves (`password|pass|passwd|secret|token|api_key|auth`) → `[FILTERED]` antes
|
|
43
|
+
> de Sentry/logs. La gema lo entrega a propósito; sanitizar es del consumidor.
|
|
44
|
+
|
|
45
|
+
Para consumir un envelope estructurado de dominio (ej. `{ error: { code,
|
|
46
|
+
message, details } }`), parsealo en el boundary del servicio desde
|
|
47
|
+
`e.raw_response` — la gema **no** expone `code`/`details` como contrato.
|
|
48
|
+
|
|
25
49
|
## Errores de Infraestructura
|
|
26
50
|
|
|
27
51
|
### BugBunny::CommunicationError
|
|
@@ -133,10 +157,15 @@ end
|
|
|
133
157
|
|
|
134
158
|
## Formato de Mensajes de Error
|
|
135
159
|
|
|
136
|
-
El middleware `RaiseError` construye el
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
160
|
+
El middleware `RaiseError` construye el `.message` humano (best-effort, para
|
|
161
|
+
logs/Sentry — **no es contrato**) así:
|
|
162
|
+
1. Envelope anidado `{ "error": { "message": "..." } }` → extrae `error.message`.
|
|
163
|
+
2. Shape plano `{ "error": "...", "detail": "..." }` → concatena `error` + `detail`.
|
|
164
|
+
3. Si ninguno aplica, usa el cuerpo completo como JSON (nunca un `Hash#inspect`).
|
|
165
|
+
4. Si el body está vacío, usa `"Unknown Error"`.
|
|
166
|
+
|
|
167
|
+
Para detalle estructurado (`code`/`details`), leé `e.raw_response` en el
|
|
168
|
+
boundary del servicio (ver "Materia prima del error").
|
|
140
169
|
|
|
141
170
|
## Connection Pool Missing
|
|
142
171
|
|
data/skills.yml
CHANGED
|
@@ -2,34 +2,18 @@ mcps:
|
|
|
2
2
|
- github
|
|
3
3
|
- clickup
|
|
4
4
|
skills:
|
|
5
|
+
multi-vendor-feedback:
|
|
5
6
|
yard:
|
|
6
|
-
repo: sequre/ai_knowledge
|
|
7
7
|
quality-code:
|
|
8
|
-
repo: sequre/ai_knowledge
|
|
9
8
|
gem-release:
|
|
10
|
-
repo: sequre/ai_knowledge
|
|
11
9
|
dev-structure:
|
|
12
|
-
repo: sequre/ai_knowledge
|
|
13
10
|
dev-compose:
|
|
14
|
-
repo: sequre/ai_knowledge
|
|
15
11
|
dev-enrich:
|
|
16
|
-
repo: sequre/ai_knowledge
|
|
17
12
|
skill-feedback:
|
|
18
|
-
repo: sequre/ai_knowledge
|
|
19
13
|
agent-issue:
|
|
20
|
-
|
|
14
|
+
bug-report:
|
|
21
15
|
dev-flow:
|
|
22
|
-
repo: sequre/ai_knowledge
|
|
23
16
|
matrix-element:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
auth_token: "${MATRIX_AUTH_TOKEN}"
|
|
28
|
-
rooms:
|
|
29
|
-
agents: "!VCHwQXgmXdyhhhPhoz:matrix.cloud.wispro.co"
|
|
30
|
-
documentation-writer:
|
|
31
|
-
repo: github/awesome-copilot
|
|
32
|
-
path: skills/documentation-writer
|
|
33
|
-
rabbitmq-expert:
|
|
34
|
-
repo: martinholovsky/claude-skills-generator
|
|
35
|
-
path: skills/rabbitmq-expert
|
|
17
|
+
# rabbitmq-expert:
|
|
18
|
+
# repo: martinholovsky/claude-skills-generator
|
|
19
|
+
# path: skills/rabbitmq-expert
|
|
@@ -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
|
|
|
@@ -55,5 +55,181 @@ RSpec.describe BugBunny::Middleware::RaiseError do
|
|
|
55
55
|
expect { middleware.on_complete(response) }.to raise_error(BugBunny::NotFound)
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
|
+
|
|
59
|
+
# Alcance issue #52: status + raw_response como materia prima en TODAS las
|
|
60
|
+
# clases de error, no solo en 422.
|
|
61
|
+
describe 'raw_response and status on every error class' do
|
|
62
|
+
def captured_error(response)
|
|
63
|
+
middleware.on_complete(response)
|
|
64
|
+
nil
|
|
65
|
+
rescue BugBunny::Error => e
|
|
66
|
+
e
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
400 => BugBunny::BadRequest,
|
|
71
|
+
409 => BugBunny::Conflict,
|
|
72
|
+
500 => BugBunny::InternalServerError
|
|
73
|
+
}.each do |status, klass|
|
|
74
|
+
context "when status is #{status}" do
|
|
75
|
+
let(:body) { { 'error' => 'boom' } }
|
|
76
|
+
let(:error) { captured_error('status' => status, 'body' => body) }
|
|
77
|
+
|
|
78
|
+
it "raises #{klass} with status and raw_response populated" do
|
|
79
|
+
expect(error).to be_a(klass)
|
|
80
|
+
expect(error.status).to eq(status)
|
|
81
|
+
expect(error.raw_response).to eq(body)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
context 'when status is 404 (NotFound)' do
|
|
87
|
+
let(:body) { { 'error' => 'missing' } }
|
|
88
|
+
let(:error) { captured_error('status' => 404, 'body' => body) }
|
|
89
|
+
|
|
90
|
+
it 'populates status and raw_response' do
|
|
91
|
+
expect(error.status).to eq(404)
|
|
92
|
+
expect(error.raw_response).to eq(body)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
context 'when status is 422 (UnprocessableEntity)' do
|
|
97
|
+
let(:body) { { 'errors' => { 'name' => ['blank'] } } }
|
|
98
|
+
let(:error) { captured_error('status' => 422, 'body' => body) }
|
|
99
|
+
|
|
100
|
+
it 'keeps raw_response/error_messages and adds status from base' do
|
|
101
|
+
expect(error).to be_a(BugBunny::UnprocessableEntity)
|
|
102
|
+
expect(error.status).to eq(422)
|
|
103
|
+
expect(error.raw_response).to eq(body)
|
|
104
|
+
expect(error.error_messages).to eq('name' => ['blank'])
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
context 'when status is unmapped (418)' do
|
|
109
|
+
let(:body) { { 'error' => 'teapot' } }
|
|
110
|
+
let(:error) { captured_error('status' => 418, 'body' => body) }
|
|
111
|
+
|
|
112
|
+
it 'populates status and raw_response on the generic ClientError' do
|
|
113
|
+
expect(error).to be_a(BugBunny::ClientError)
|
|
114
|
+
expect(error.status).to eq(418)
|
|
115
|
+
expect(error.raw_response).to eq(body)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Alcance issue #52: hardening de format_error_message contra el envelope
|
|
121
|
+
# anidado para no volcar un Hash#inspect en .message.
|
|
122
|
+
describe 'message hardening against the nested canonical envelope' do
|
|
123
|
+
def captured_error(response)
|
|
124
|
+
middleware.on_complete(response)
|
|
125
|
+
nil
|
|
126
|
+
rescue BugBunny::Error => e
|
|
127
|
+
e
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
context 'when error is the nested canonical envelope { error: { message } }' do
|
|
131
|
+
let(:body) { { 'error' => { 'code' => 'shortname_taken', 'message' => 'shortname already taken', 'details' => {} } } }
|
|
132
|
+
let(:error) { captured_error('status' => 409, 'body' => body) }
|
|
133
|
+
|
|
134
|
+
it 'extracts the human message and does not dump the Hash' do
|
|
135
|
+
expect(error.message).to eq('shortname already taken')
|
|
136
|
+
expect(error.message).not_to include('=>')
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
context 'when the nested envelope lacks a usable message' do
|
|
141
|
+
let(:body) { { 'error' => { 'code' => 'x', 'details' => {} } } }
|
|
142
|
+
let(:error) { captured_error('status' => 409, 'body' => body) }
|
|
143
|
+
|
|
144
|
+
it 'falls back to JSON instead of Hash#inspect' do
|
|
145
|
+
expect(error.message).to eq(body.to_json)
|
|
146
|
+
expect(error.message).not_to include('=>')
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
context 'when error is the flat historical shape { error: string, detail: string }' do
|
|
151
|
+
let(:body) { { 'error' => 'boom', 'detail' => 'because reasons' } }
|
|
152
|
+
let(:error) { captured_error('status' => 400, 'body' => body) }
|
|
153
|
+
|
|
154
|
+
it 'keeps concatenating error and detail (backward compatible)' do
|
|
155
|
+
expect(error.message).to eq('boom - because reasons')
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
context 'when error is an empty string (backward-compat edge case)' do
|
|
160
|
+
let(:body) { { 'error' => '', 'detail' => 'x' } }
|
|
161
|
+
let(:error) { captured_error('status' => 400, 'body' => body) }
|
|
162
|
+
|
|
163
|
+
it 'preserves the historical concatenation instead of dumping JSON' do
|
|
164
|
+
expect(error.message).to eq(' - x')
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Alcance issue #52: casos borde y paths no cubiertos arriba.
|
|
170
|
+
describe 'edge cases and uncovered paths' do
|
|
171
|
+
def captured_error(response)
|
|
172
|
+
middleware.on_complete(response)
|
|
173
|
+
nil
|
|
174
|
+
rescue BugBunny::Error => e
|
|
175
|
+
e
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
context 'when 5xx carries a serialized remote exception (bug_bunny_exception)' do
|
|
179
|
+
let(:body) do
|
|
180
|
+
{ 'bug_bunny_exception' => { 'class' => 'ActiveRecord::RecordNotFound',
|
|
181
|
+
'message' => 'User not found',
|
|
182
|
+
'backtrace' => ['app.rb:1'] } }
|
|
183
|
+
end
|
|
184
|
+
let(:error) { captured_error('status' => 500, 'body' => body) }
|
|
185
|
+
|
|
186
|
+
it 'raises RemoteError with status and raw_response populated' do
|
|
187
|
+
expect(error).to be_a(BugBunny::RemoteError)
|
|
188
|
+
expect(error.status).to eq(500)
|
|
189
|
+
expect(error.raw_response).to eq(body)
|
|
190
|
+
expect(error.original_class).to eq('ActiveRecord::RecordNotFound')
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
context 'when 422 carries the nested envelope (consumer parses from raw_response)' do
|
|
195
|
+
let(:body) { { 'error' => { 'code' => 'taken', 'message' => 'shortname already taken', 'details' => {} } } }
|
|
196
|
+
let(:error) { captured_error('status' => 422, 'body' => body) }
|
|
197
|
+
|
|
198
|
+
it 'keeps raw_response intact for the service boundary' do
|
|
199
|
+
expect(error).to be_a(BugBunny::UnprocessableEntity)
|
|
200
|
+
expect(error.status).to eq(422)
|
|
201
|
+
expect(error.raw_response).to eq(body)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
context 'when status is 406 (no-arg constructor)' do
|
|
206
|
+
let(:body) { { 'error' => 'nope' } }
|
|
207
|
+
let(:error) { captured_error('status' => 406, 'body' => body) }
|
|
208
|
+
|
|
209
|
+
it 'still populates status and raw_response' do
|
|
210
|
+
expect(error).to be_a(BugBunny::NotAcceptable)
|
|
211
|
+
expect(error.status).to eq(406)
|
|
212
|
+
expect(error.raw_response).to eq(body)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
context 'when status is 408 (no-arg constructor)' do
|
|
217
|
+
let(:error) { captured_error('status' => 408, 'body' => nil) }
|
|
218
|
+
|
|
219
|
+
it 'still populates status' do
|
|
220
|
+
expect(error).to be_a(BugBunny::RequestTimeout)
|
|
221
|
+
expect(error.status).to eq(408)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
context 'when body is not a Hash nor a String (e.g. Array)' do
|
|
226
|
+
let(:body) { [1, 2, 3] }
|
|
227
|
+
let(:error) { captured_error('status' => 400, 'body' => body) }
|
|
228
|
+
|
|
229
|
+
it 'falls back to JSON without raising in the formatter' do
|
|
230
|
+
expect(error.message).to eq(body.to_json)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
58
234
|
end
|
|
59
235
|
end
|
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.19.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-
|
|
11
|
+
date: 2026-06-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|
|
@@ -235,6 +235,7 @@ files:
|
|
|
235
235
|
- Rakefile
|
|
236
236
|
- docs/behavior/behavior.md
|
|
237
237
|
- docs/glossary/glossary.md
|
|
238
|
+
- docs/release/release.md
|
|
238
239
|
- initializer_example.rb
|
|
239
240
|
- lib/bug_bunny.rb
|
|
240
241
|
- lib/bug_bunny/client.rb
|
|
@@ -283,6 +284,7 @@ files:
|
|
|
283
284
|
- spec/support/bunny_mocks.rb
|
|
284
285
|
- spec/support/integration_helper.rb
|
|
285
286
|
- spec/unit/client_session_pool_spec.rb
|
|
287
|
+
- spec/unit/communication_error_wrapping_spec.rb
|
|
286
288
|
- spec/unit/configuration_spec.rb
|
|
287
289
|
- spec/unit/consumer_middleware_spec.rb
|
|
288
290
|
- spec/unit/consumer_spec.rb
|
|
@@ -306,7 +308,7 @@ metadata:
|
|
|
306
308
|
homepage_uri: https://github.com/gedera/bug_bunny
|
|
307
309
|
source_code_uri: https://github.com/gedera/bug_bunny
|
|
308
310
|
changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
|
|
309
|
-
documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.
|
|
311
|
+
documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.19.0/skill
|
|
310
312
|
post_install_message:
|
|
311
313
|
rdoc_options: []
|
|
312
314
|
require_paths:
|