bug_bunny 4.17.0 → 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 +35 -0
- data/CLAUDE.md +20 -4
- data/README.md +42 -26
- data/docs/behavior/behavior.md +179 -0
- data/docs/glossary/glossary.md +148 -0
- 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 +81 -23
- data/skills.yml +11 -5
- data/spec/unit/communication_error_wrapping_spec.rb +129 -0
- data/spec/unit/producer_spec.rb +10 -2
- 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: 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,40 @@
|
|
|
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
|
+
|
|
19
|
+
## [4.17.1] - 2026-05-19
|
|
20
|
+
|
|
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`.
|
|
22
|
+
|
|
23
|
+
### Nuevas funcionalidades
|
|
24
|
+
- Incorporar familia de skills `dev-*` (glosario + comportamiento) y recomponer composites (#47) — @Gabriel
|
|
25
|
+
|
|
26
|
+
### Correcciones
|
|
27
|
+
- Consistencia README: Quickstart Consumer, `rpc_timeout`, idioma, coexistencia — @Gabriel
|
|
28
|
+
- Escapar `;` en Note del diagrama Mermaid (flujo confirmed) — @Gabriel
|
|
29
|
+
|
|
30
|
+
### Documentación
|
|
31
|
+
- Marcar verificación humana de `behavior.md` completada (RFC-001 §3.3) — @Gabriel
|
|
32
|
+
- Rework `skill/SKILL.md` a RFC-008 endurecido (dev-compose 2.0.0) — @Gabriel
|
|
33
|
+
- Alinear nota de coexistencia skill/README — @Gabriel
|
|
34
|
+
|
|
35
|
+
### Otros cambios
|
|
36
|
+
- Migrar de `service-release` a `gem-release` — @Gabriel
|
|
37
|
+
|
|
3
38
|
## [4.17.0] - 2026-05-13
|
|
4
39
|
|
|
5
40
|
### Nuevas funcionalidades
|
data/CLAUDE.md
CHANGED
|
@@ -8,14 +8,30 @@ BugBunny es una gema Ruby que implementa una capa de enrutamiento RESTful sobre
|
|
|
8
8
|
|
|
9
9
|
## Documentación
|
|
10
10
|
|
|
11
|
-
- **
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
- **Modelo `dev-*` (RFC-001):** artefactos de detalle en `docs/<capa>/`
|
|
12
|
+
(`data/`, `glossary/`, `behavior/`); compuestos (`README.md` humano,
|
|
13
|
+
`skill/SKILL.md` agente version-locked) **indexan, no duplican**.
|
|
14
|
+
Artefactos generados por `dev-structure` / `dev-enrich`; compuestos por
|
|
15
|
+
`dev-compose`. Verificación humana antes de commitear.
|
|
16
|
+
- **Estado actual:** `docs/data` = n/a (gema sin DB, declarado solo en índice);
|
|
17
|
+
`docs/glossary` parcial (acreta por PR); `docs/behavior` completo (6 flujos,
|
|
18
|
+
backfill on-demand); operaciones/interfaz/topología = dev-structure F2 no
|
|
19
|
+
implementado.
|
|
20
|
+
- **Para agentes AI**: `skill/SKILL.md` (empaquetada en el `.gem`) +
|
|
21
|
+
`skill/references/`.
|
|
22
|
+
- **Coexistencia transitoria con destino pendiente (RFC-008 §2 — interim de
|
|
23
|
+
migración):** contrato/arquitectura sigue embebido en
|
|
24
|
+
`README.md`/`skill/SKILL.md` y las guías how-to viven en `skill/references/`
|
|
25
|
+
(pre-estándar) porque su capa destino (operaciones/interfaz/topología) es
|
|
26
|
+
dev-structure F2 no implementado. Por norma: no se fabrica la capa, no se
|
|
27
|
+
borra el contrato sin destino; migra cuando F2 entregue, mismo PR. Estado
|
|
28
|
+
transitorio declarado en el índice de artefactos. Origen del gap (resuelto,
|
|
29
|
+
normado): `sequre/ai_knowledge#95`.
|
|
14
30
|
|
|
15
31
|
## Knowledge Base
|
|
16
32
|
- Las skills en `.agents/skills/` incluyen conocimiento de dependencias.
|
|
17
33
|
- Leer la skill de una dependencia ANTES de responder sobre ella.
|
|
18
|
-
- Rebuild: `
|
|
34
|
+
- Rebuild: `wispro-agent sync`
|
|
19
35
|
|
|
20
36
|
### Entorno
|
|
21
37
|
- Versión de Ruby: leer `.ruby-version`
|
data/README.md
CHANGED
|
@@ -58,8 +58,8 @@ module BugBunny
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
# Worker entrypoint (dedicated thread or process)
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
BugBunny::Consumer.subscribe(
|
|
62
|
+
connection: BugBunny.create_connection,
|
|
63
63
|
queue_name: 'inventory_queue',
|
|
64
64
|
exchange_name: 'inventory',
|
|
65
65
|
routing_key: 'nodes'
|
|
@@ -124,7 +124,7 @@ BugBunny.configure do |config|
|
|
|
124
124
|
config.network_recovery_interval = 5 # seconds, base for exponential backoff
|
|
125
125
|
|
|
126
126
|
# Timeouts
|
|
127
|
-
config.rpc_timeout =
|
|
127
|
+
config.rpc_timeout = 10 # seconds (default 10), for synchronous RPC calls
|
|
128
128
|
config.connection_timeout = 10
|
|
129
129
|
config.read_timeout = 10
|
|
130
130
|
config.write_timeout = 10
|
|
@@ -339,9 +339,9 @@ BugBunny.consumer_middlewares.use TracingMiddleware
|
|
|
339
339
|
|
|
340
340
|
## Observability
|
|
341
341
|
|
|
342
|
-
BugBunny
|
|
342
|
+
BugBunny natively implements the [OpenTelemetry semantic conventions for messaging](https://opentelemetry.io/docs/specs/otel/trace/semantic-conventions/messaging/), automatically injecting fields like `messaging_system`, `messaging_operation`, `messaging_destination_name` and `messaging_message_id` into both the AMQP headers and the structured log events.
|
|
343
343
|
|
|
344
|
-
|
|
344
|
+
All internal events are emitted as `key=value` logs compatible with Datadog, CloudWatch, ELK and ExisRay.
|
|
345
345
|
|
|
346
346
|
```
|
|
347
347
|
component=bug_bunny event=producer.publish method=POST path=acct/publish messaging_destination_name=acct_x messaging_routing_key=acct.start.42
|
|
@@ -353,19 +353,19 @@ component=bug_bunny event=consumer.execution_error error_class=RuntimeError erro
|
|
|
353
353
|
component=bug_bunny event=consumer.connection_error attempt_count=2 retry_in_s=10 error_message="..."
|
|
354
354
|
```
|
|
355
355
|
|
|
356
|
-
###
|
|
356
|
+
### Internally measured durations
|
|
357
357
|
|
|
358
|
-
BugBunny
|
|
358
|
+
BugBunny measures and emits durations automatically — **there is no need to wrap `client.publish` calls with `Process.clock_gettime` in application code**. Units follow the [OpenTelemetry metric semantic conventions](https://opentelemetry.io/docs/specs/semconv/general/metrics/) (`s`, seconds as `Float`).
|
|
359
359
|
|
|
360
|
-
|
|
|
360
|
+
| Event | Duration | Measures |
|
|
361
361
|
|---|---|---|
|
|
362
|
-
| `producer.published` | `duration_s` |
|
|
363
|
-
| `producer.confirmed` | `publish_duration_s` + `confirm_duration_s` + `duration_s` (total) | Publish +
|
|
364
|
-
| `producer.rpc_response_received` | `duration_s` |
|
|
365
|
-
| `consumer.message_processed` | `duration_s` |
|
|
366
|
-
| `consumer.execution_error` | `duration_s` |
|
|
362
|
+
| `producer.published` | `duration_s` | Only the `basic_publish` (TCP enqueue to the broker). |
|
|
363
|
+
| `producer.confirmed` | `publish_duration_s` + `confirm_duration_s` + `duration_s` (total) | Publish + wait for broker ACK. |
|
|
364
|
+
| `producer.rpc_response_received` | `duration_s` | Full RPC round-trip (publish + remote processing + reply). |
|
|
365
|
+
| `consumer.message_processed` | `duration_s` | Message processing (router + controller + reply). |
|
|
366
|
+
| `consumer.execution_error` | `duration_s` | Elapsed time until the error. |
|
|
367
367
|
|
|
368
|
-
|
|
368
|
+
Sensitive keys (`password`, `token`, `secret`, `api_key`, `authorization`, etc.) are automatically filtered to `[FILTERED]` across all log output.
|
|
369
369
|
|
|
370
370
|
---
|
|
371
371
|
|
|
@@ -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)
|
|
@@ -426,17 +426,33 @@ end
|
|
|
426
426
|
|
|
427
427
|
---
|
|
428
428
|
|
|
429
|
-
##
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
429
|
+
## Índice de artefactos
|
|
430
|
+
|
|
431
|
+
Artefactos de detalle (modelo `dev-*`, RFC-001). El README indexa; no duplica.
|
|
432
|
+
|
|
433
|
+
| Capa | Artefacto | Estado |
|
|
434
|
+
|---|---|---|
|
|
435
|
+
| Datos | — | n/a — gema sin DB (sin schema/models) |
|
|
436
|
+
| Glosario | [docs/glossary/glossary.md](docs/glossary/glossary.md) | parcial, acreta por PR |
|
|
437
|
+
| Comportamiento | [docs/behavior/behavior.md](docs/behavior/behavior.md) | completa — 6 flujos (backfill on-demand) |
|
|
438
|
+
| Operaciones / Interfaz / Topología | — | F2 no implementado (dev-structure) — ver nota |
|
|
439
|
+
|
|
440
|
+
**Coexistencia transitoria con destino pendiente (RFC-008 §2 — interim de migración):** mientras la capa de detalle destino (operaciones/interfaz/topología) esté declarada pero **no implementada** (dev-structure F1, F2 del plan), permanecen embebidos/cruzados, bajo el interim normado:
|
|
441
|
+
|
|
442
|
+
- **En este README:** el contrato (jerarquía de excepciones, API de configuración, modos de entrega).
|
|
443
|
+
- **En `skill/SKILL.md`:** además el diagrama de arquitectura (flujo RPC).
|
|
444
|
+
- **Guías how-to** (`skill/references/*.md`, pre-estándar): el README las enlaza pese a la regla "no referenciar `skill/` desde el README" — destino futuro `docs/howto/`.
|
|
445
|
+
|
|
446
|
+
Por RFC-008 §2: no se fabrica la capa, no se borra contrato sin destino, no se duplica; migra cuando F2 entregue, mismo PR. Estado transitorio declarado, no excepción permanente. Origen del gap (resuelto, normado): [sequre/ai_knowledge#95](https://github.com/sequre/ai_knowledge/issues/95).
|
|
447
|
+
|
|
448
|
+
How-to (pre-estándar):
|
|
449
|
+
[Routing](skill/references/routing.md) ·
|
|
450
|
+
[Controllers](skill/references/controller.md) ·
|
|
451
|
+
[Resources](skill/references/resource.md) ·
|
|
452
|
+
[Client & Middleware](skill/references/client-middleware.md) ·
|
|
453
|
+
[Consumer](skill/references/consumer.md) ·
|
|
454
|
+
[Errores](skill/references/errores.md) ·
|
|
455
|
+
[Testing](skill/references/testing.md)
|
|
440
456
|
|
|
441
457
|
---
|
|
442
458
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Comportamiento — bug_bunny
|
|
2
|
+
|
|
3
|
+
> meta: artefacto `comportamiento` · RFC-007 (cadencia incremental default / completo on-demand) · generado dev-enrich 1.3.0 (backfill on-demand) · anclado a `94de2b4` · cobertura: completa (6 flujos) · verificado por humano 2026-05-18 (base) · 2026-05-26 (refresco scoped: contrato de error wrapping post-#49)
|
|
4
|
+
|
|
5
|
+
## 1. Resumen
|
|
6
|
+
|
|
7
|
+
Flujos de ejecución de la gema. Generado en **modo completo on-demand** (RFC-007 §2 / dev-enrich 1.3.0): backfill solicitado explícitamente, no incremental. Invariante de honestidad (RFC-001 §3.3): anclado a código real (`file:line`), lógica dispersa marcada como tal, cobertura declarada, **verificado por humano 2026-05-18** contra el código (el LLM extrajo las secuencias; humano confirmó).
|
|
8
|
+
|
|
9
|
+
## Cobertura (OBLIGATORIO)
|
|
10
|
+
|
|
11
|
+
| Flujo | Estado | Anclaje principal |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| RPC síncrono | **documentado** | `producer.rb:103-134`, `consumer.rb:152-293` |
|
|
14
|
+
| Fire-and-forget | **documentado** | `producer.rb:47-51,146-161` |
|
|
15
|
+
| Confirmed + basic.return bridge | **documentado** | `producer.rb:72-93,299-333`, `session.rb:204-250` |
|
|
16
|
+
| Consumer subscribe loop + reconnect + health | **documentado** | `consumer.rb:66-127,340-361` |
|
|
17
|
+
| Error handling / RemoteError | **documentado** | `consumer.rb:320-329`, `remote_error.rb`, `raise_error.rb:32-65` |
|
|
18
|
+
| Client middleware stack (onion) | **documentado** | `middleware/stack.rb:43-47`, `base.rb:35-43` |
|
|
19
|
+
|
|
20
|
+
Cobertura completa a `a5cdb10`. Acreta incremental en cada PR que toque un flujo (default RFC-007). Ausencia futura ≠ inexistencia.
|
|
21
|
+
|
|
22
|
+
## 2. Cuerpo
|
|
23
|
+
|
|
24
|
+
### Flujo: RPC síncrono
|
|
25
|
+
Request-response bloqueante; el hilo emisor espera en `Concurrent::IVar` correlacionado por `correlation_id` hasta reply o `RequestTimeout`.
|
|
26
|
+
|
|
27
|
+
```mermaid
|
|
28
|
+
sequenceDiagram
|
|
29
|
+
participant CL as Client
|
|
30
|
+
participant MW as Middleware Stack
|
|
31
|
+
participant P as Producer
|
|
32
|
+
participant BR as RabbitMQ
|
|
33
|
+
participant CO as Consumer
|
|
34
|
+
participant CT as Controller
|
|
35
|
+
CL->>MW: request (delivery_mode=:rpc)
|
|
36
|
+
MW->>P: call (onion → producer.rpc)
|
|
37
|
+
P->>P: ensure_reply_listener! · cid=UUID · reply_to=amq.rabbitmq.reply-to
|
|
38
|
+
P->>P: @pending_requests[cid]=IVar
|
|
39
|
+
P->>BR: publish (type=path, correlation_id, reply_to)
|
|
40
|
+
P-->>P: future.value(timeout) — BLOQUEA
|
|
41
|
+
BR->>CO: deliver
|
|
42
|
+
CO->>CO: consumer middlewares · recognize(method,path)
|
|
43
|
+
CO->>CT: Controller.call(headers, body)
|
|
44
|
+
CT->>CT: before/around/after + action → render
|
|
45
|
+
CT-->>CO: response (o handle_exception → 500)
|
|
46
|
+
CO->>BR: reply a reply_to (correlation_id)
|
|
47
|
+
BR->>P: reply (basic_consume listener)
|
|
48
|
+
P->>P: @pending_requests[cid].set → IVar resuelve
|
|
49
|
+
P->>P: on_rpc_reply · parse_response
|
|
50
|
+
P-->>CL: response hidratada
|
|
51
|
+
Note over P: bloqueo L122 · timeout → RequestTimeout L124
|
|
52
|
+
```
|
|
53
|
+
Contexto: `client.rb:97-101` → `producer.rb:103-134` (bloqueo L122) → reply listener `producer.rb:405-424` → `consumer.rb:247,272-293`.
|
|
54
|
+
|
|
55
|
+
### Flujo: Fire-and-forget
|
|
56
|
+
Publica y retorna `{ 'status' => 202 }` sin esperar broker ni consumer.
|
|
57
|
+
|
|
58
|
+
```mermaid
|
|
59
|
+
sequenceDiagram
|
|
60
|
+
participant CL as Client
|
|
61
|
+
participant MW as Middleware Stack
|
|
62
|
+
participant P as Producer
|
|
63
|
+
participant BR as RabbitMQ
|
|
64
|
+
CL->>MW: publish (delivery_mode=:publish)
|
|
65
|
+
MW->>P: call (onion → producer.fire)
|
|
66
|
+
P->>BR: publish_message (TCP enqueue, sin ACK)
|
|
67
|
+
P-->>CL: { status: 202, body: nil } (inmediato)
|
|
68
|
+
```
|
|
69
|
+
Contexto: `client.rb:129-134` → `producer.rb:47-51` → `publish_message producer.rb:146-161`.
|
|
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
|
+
|
|
73
|
+
### Flujo: Confirmed + basic.return bridge
|
|
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.
|
|
75
|
+
|
|
76
|
+
```mermaid
|
|
77
|
+
sequenceDiagram
|
|
78
|
+
participant CL as Client
|
|
79
|
+
participant P as Producer
|
|
80
|
+
participant S as Session
|
|
81
|
+
participant BR as RabbitMQ
|
|
82
|
+
participant RT as Bunny reader thread
|
|
83
|
+
CL->>P: publish confirmed:true [mandatory]
|
|
84
|
+
P->>S: register_return_listener(cid) → Event+slot
|
|
85
|
+
P->>BR: publish_message (mandatory)
|
|
86
|
+
P-->>P: wait_for_confirms! — BLOQUEA (IVar si hay timeout)
|
|
87
|
+
BR-->>RT: basic.return (si unroutable)
|
|
88
|
+
RT->>S: handle_broker_return → signal_return_listener
|
|
89
|
+
S->>S: slot[:info]=info · slot[:event].set
|
|
90
|
+
BR-->>P: confirms (ack/nack)
|
|
91
|
+
P->>P: handle_confirm_result — nack → PublishNacked
|
|
92
|
+
P->>P: handle_return_result → event.wait(RETURN_RACE_WINDOW_S=0.05s)
|
|
93
|
+
P->>P: slot[:info] presente → raise_unroutable!
|
|
94
|
+
P-->>CL: PublishUnroutable (o éxito si slot vacío)
|
|
95
|
+
Note over RT,S: on_return corre antes del raise · su excepción se loggea (no propaga)
|
|
96
|
+
```
|
|
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).
|
|
98
|
+
|
|
99
|
+
### Flujo: Consumer subscribe loop + reconnect + health
|
|
100
|
+
Loop bloqueante infinito con backoff exponencial + jitter en error; health check en TimerTask separado, **acoplado flojo** vía estado de session.
|
|
101
|
+
|
|
102
|
+
```mermaid
|
|
103
|
+
sequenceDiagram
|
|
104
|
+
participant C as Consumer
|
|
105
|
+
participant S as Session
|
|
106
|
+
participant BR as RabbitMQ
|
|
107
|
+
participant H as Health TimerTask
|
|
108
|
+
C->>S: exchange/queue declare · q.bind(rk)
|
|
109
|
+
C->>H: start_health_check (touch file)
|
|
110
|
+
C->>BR: q.subscribe(manual_ack, block:true) — LOOP
|
|
111
|
+
loop por mensaje
|
|
112
|
+
BR->>C: deliver → consumer_middlewares → process_message
|
|
113
|
+
end
|
|
114
|
+
H-->>S: cada interval: queue_declare(passive:true)
|
|
115
|
+
H-->>S: falla → cierra session (fuerza reconnect)
|
|
116
|
+
Note over C: rescue StandardError → attempt++ · backoff min(nri*2^(n-1), max) · sleep · retry (redeclara)
|
|
117
|
+
Note over C: max_reconnect_attempts alcanzado → raise (fatal) · ensure → shutdown
|
|
118
|
+
```
|
|
119
|
+
Contexto: `consumer.rb:66-127` (retry L106-124), `consumer.rb:340-361` (health). **Honestidad:** health check es thread aparte (TimerTask); no es parte del manejo de error del loop — se acoplan sólo vía cierre de session. Marcado, no fingido como un único flujo.
|
|
120
|
+
|
|
121
|
+
### Flujo: Error handling / RemoteError
|
|
122
|
+
Excepción no manejada en controller → serializada (clase/mensaje/backtrace[0..25]) → reply 500 → reconstruida client-side por `Middleware::RaiseError`.
|
|
123
|
+
|
|
124
|
+
```mermaid
|
|
125
|
+
sequenceDiagram
|
|
126
|
+
participant CT as Controller
|
|
127
|
+
participant CO as Consumer
|
|
128
|
+
participant BR as RabbitMQ
|
|
129
|
+
participant RE as Middleware::RaiseError
|
|
130
|
+
participant CL as Caller
|
|
131
|
+
CT->>CT: action raise → rescue → handle_exception
|
|
132
|
+
CT->>CT: rescue_from match? sí→handler · no→500 + RemoteError.serialize
|
|
133
|
+
CT-->>CO: response 500 (bug_bunny_exception)
|
|
134
|
+
CO->>CO: handle_fatal_error → RemoteError.serialize
|
|
135
|
+
CO->>BR: reply 500 a reply_to
|
|
136
|
+
BR->>RE: response (vía reply listener + parse_response)
|
|
137
|
+
RE->>RE: status 500..599 + bug_bunny_exception
|
|
138
|
+
RE-->>CL: raise BugBunny::RemoteError(class,message,backtrace)
|
|
139
|
+
```
|
|
140
|
+
Contexto: `controller.rb:200-234`, `consumer.rb:320-329`, `remote_error.rb:29-48`, `raise_error.rb:32-65`. **Honestidad:** backtrace truncado a 25 líneas en serialize; si el controller nunca llega a responder, el cliente expira por timeout en vez de recibir el error (no hay path de error garantizado).
|
|
141
|
+
|
|
142
|
+
### Flujo: Client middleware stack (onion)
|
|
143
|
+
`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).
|
|
144
|
+
|
|
145
|
+
```mermaid
|
|
146
|
+
sequenceDiagram
|
|
147
|
+
participant U as Caller
|
|
148
|
+
participant O as MW externo (primer use)
|
|
149
|
+
participant I as MW interno (último use)
|
|
150
|
+
participant P as Producer (final_action)
|
|
151
|
+
U->>O: call(env)
|
|
152
|
+
O->>O: on_request
|
|
153
|
+
O->>I: @app.call(env)
|
|
154
|
+
I->>I: on_request
|
|
155
|
+
I->>P: @app.call(env) → rpc/fire/confirmed
|
|
156
|
+
P-->>I: response
|
|
157
|
+
I->>I: on_complete
|
|
158
|
+
I-->>O: response
|
|
159
|
+
O->>O: on_complete (RaiseError: 4xx/5xx → raise)
|
|
160
|
+
O-->>U: response / excepción
|
|
161
|
+
```
|
|
162
|
+
Contexto: `middleware/stack.rb:31-47` (build L43-47, `reverse.inject`), `base.rb:35-43` (`call`: on_request → @app.call → on_complete), `client.rb:160-163` (final_action + `@stack.build(final_action).call(req)`). Orden: `use A; use B` → **A más externo** (corre primero), B envuelve al producer. `RaiseError` solo define `on_complete` (sin `on_request`).
|
|
163
|
+
|
|
164
|
+
## 3. Inferencias
|
|
165
|
+
|
|
166
|
+
| Inferencia | confidence | a verificar (humano) |
|
|
167
|
+
|---|---|---|
|
|
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) |
|
|
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 |
|
|
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` |
|
|
172
|
+
|
|
173
|
+
## 4. Cobertura y fronteras
|
|
174
|
+
|
|
175
|
+
- **Modo:** completo on-demand (RFC-007 §2). Default futuro = incremental por PR; este artefacto se actualiza en el mismo PR que toque cualquier flujo.
|
|
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`.)
|
|
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).
|
|
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).
|
|
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,148 @@
|
|
|
1
|
+
# Glosario — bug_bunny
|
|
2
|
+
|
|
3
|
+
> meta: artefacto `glosario` · RFC-009 (binding opcional, r: §2 materialización no-tabular) · generado dev-enrich (siembra) · anclado a `a5cdb10` · cobertura: parcial, acreta por PR
|
|
4
|
+
|
|
5
|
+
## 1. Resumen
|
|
6
|
+
|
|
7
|
+
Vocabulario de dominio (DDD ubiquitous language) del bounded context **bug_bunny**: capa de routing RESTful sobre AMQP. Siembra inicial (dev-enrich: glosario SE PUEDE sembrar = migración desde código + conocimiento). **Gema sin capa de datos** (`docs/data/datos.md` = n/a): por RFC-009 §2 el `Binding:` es **opcional** — se cita solo cuando un símbolo público estable *es* el concepto (clase/value object que lo nombra); concepto o patrón sin símbolo propio → `Binding: n/a`. Nunca binding sintético.
|
|
8
|
+
|
|
9
|
+
## 2. Cuerpo
|
|
10
|
+
|
|
11
|
+
## Request
|
|
12
|
+
Value object con toda la metadata del mensaje saliente: path, verbo, body, params, headers y opciones AMQP (exchange, routing_key, delivery_mode, persistent, mandatory).
|
|
13
|
+
|
|
14
|
+
**Binding:**
|
|
15
|
+
- `lib/bug_bunny/request.rb` — `BugBunny::Request` (la clase *es* el concepto)
|
|
16
|
+
|
|
17
|
+
## Delivery Mode
|
|
18
|
+
Modo de entrega de un mensaje en este contexto: `:rpc` (síncrono, bloquea esperando reply), `:publish` (fire-and-forget, retorna 202), `:confirmed` (async con ACK del broker).
|
|
19
|
+
|
|
20
|
+
**Binding:** n/a (modo, no símbolo que sea el concepto; se realiza vía `Client#delivery_mode` / `Producer#{rpc,fire,confirmed}`)
|
|
21
|
+
|
|
22
|
+
## RPC
|
|
23
|
+
Request-response síncrono sobre la pseudo-cola `amq.rabbitmq.reply-to` (Direct Reply-to). El hilo emisor bloquea en un `Concurrent::IVar` hasta el reply o `RequestTimeout`.
|
|
24
|
+
|
|
25
|
+
**Binding:** n/a (patrón; sin clase que sea "RPC")
|
|
26
|
+
|
|
27
|
+
## Fire-and-Forget
|
|
28
|
+
Publicación asíncrona sin espera de respuesta; el caller retorna `{ 'status' => 202 }` de inmediato.
|
|
29
|
+
|
|
30
|
+
**Binding:** n/a (patrón; sin clase propia)
|
|
31
|
+
|
|
32
|
+
## Publisher Confirms
|
|
33
|
+
Confirmación del broker (`basic.ack`) de recepción del mensaje, expuesta como `confirmed: true`. Dos señales asíncronas se convierten en excepciones raise-eables en el hilo de publish: `basic.nack` → `PublishNacked`; `basic.return` (mandatory unroutable) → `PublishUnroutable`.
|
|
34
|
+
|
|
35
|
+
**Binding:** n/a (extensión AMQP; se realiza en `Producer#confirmed`)
|
|
36
|
+
|
|
37
|
+
## Mandatory
|
|
38
|
+
Flag de `basic.publish` que pide al broker retornar el mensaje (`basic.return`) si no es ruteable a ninguna cola. Inerte sin `confirmed: true`.
|
|
39
|
+
|
|
40
|
+
**Binding:** n/a (flag AMQP)
|
|
41
|
+
|
|
42
|
+
## Route
|
|
43
|
+
Patrón compilado verbo+path → Controller#acción; extrae params nombrados por regex.
|
|
44
|
+
|
|
45
|
+
**Binding:**
|
|
46
|
+
- `lib/bug_bunny/routing/route.rb` — `BugBunny::Routing::Route`
|
|
47
|
+
|
|
48
|
+
## RouteSet
|
|
49
|
+
Registro central de rutas + DSL (`resources`, `namespace`, `member`, `collection`) y `recognize`.
|
|
50
|
+
|
|
51
|
+
**Binding:**
|
|
52
|
+
- `lib/bug_bunny/routing/route_set.rb` — `BugBunny::Routing::RouteSet`
|
|
53
|
+
|
|
54
|
+
## Controller
|
|
55
|
+
Base class tipo Rails que recibe el mensaje deserializado (body+headers) y produce una respuesta HTTP. Soporta `before/around/after_action`, `rescue_from`, `render`.
|
|
56
|
+
|
|
57
|
+
**Binding:**
|
|
58
|
+
- `lib/bug_bunny/controller.rb` — `BugBunny::Controller`
|
|
59
|
+
|
|
60
|
+
## Consumer
|
|
61
|
+
Worker bloqueante que escucha una cola, deserializa, rutea via `RouteSet` al controller y responde (RPC reply o ack). Incluye health check periódico.
|
|
62
|
+
|
|
63
|
+
**Binding:**
|
|
64
|
+
- `lib/bug_bunny/consumer.rb` — `BugBunny::Consumer`
|
|
65
|
+
|
|
66
|
+
## Producer
|
|
67
|
+
Publicador de bajo nivel: serialización, resolución de opciones AMQP, correlación RPC, sincronización de Publisher Confirms.
|
|
68
|
+
|
|
69
|
+
**Binding:**
|
|
70
|
+
- `lib/bug_bunny/producer.rb` — `BugBunny::Producer`
|
|
71
|
+
|
|
72
|
+
## Session
|
|
73
|
+
Wrapper de un canal Bunny con init perezoso, cascada de config (defaults gema → global → request) y resiliencia (double-checked locking, auto-reconnect). Correlaciona `basic.return` cross-thread.
|
|
74
|
+
|
|
75
|
+
**Binding:**
|
|
76
|
+
- `lib/bug_bunny/session.rb` — `BugBunny::Session`
|
|
77
|
+
|
|
78
|
+
## Client
|
|
79
|
+
API de alto nivel con arquitectura onion-middleware: construye `Request`, ejecuta el stack y delega en `Producer` via connection pool.
|
|
80
|
+
|
|
81
|
+
**Binding:**
|
|
82
|
+
- `lib/bug_bunny/client.rb` — `BugBunny::Client`
|
|
83
|
+
|
|
84
|
+
## Resource
|
|
85
|
+
ORM tipo ActiveModel: declara microservicios remotos como objetos Ruby con CRUD (`find`, `where`, `create`, `save`, `destroy`) que emiten RPC/async. `.with` da override de contexto thread-local.
|
|
86
|
+
|
|
87
|
+
**Binding:**
|
|
88
|
+
- `lib/bug_bunny/resource.rb` — `BugBunny::Resource`
|
|
89
|
+
|
|
90
|
+
## Exchange
|
|
91
|
+
Entidad AMQP que rutea mensajes a colas según binding. Declarada con `durable`/`auto_delete` por la cascada de config.
|
|
92
|
+
|
|
93
|
+
**Binding:** n/a (entidad del broker, no de la gema; se realiza en `Session#exchange`)
|
|
94
|
+
|
|
95
|
+
## Queue
|
|
96
|
+
Entidad AMQP que retiene mensajes hasta que un Consumer se suscribe. Default de la gema desde 4.16: compartida y durable.
|
|
97
|
+
|
|
98
|
+
**Binding:** n/a (entidad del broker; se realiza en `Session#queue`)
|
|
99
|
+
|
|
100
|
+
## Routing Key
|
|
101
|
+
Clave que el exchange usa para matchear mensaje→binding de cola. Por defecto = `path` del request salvo override explícito.
|
|
102
|
+
|
|
103
|
+
**Binding:** n/a (concepto AMQP; se computa en `Request#final_routing_key`)
|
|
104
|
+
|
|
105
|
+
## RemoteError
|
|
106
|
+
Error 500 que propaga clase, mensaje y backtrace originales de la excepción del worker remoto al caller RPC.
|
|
107
|
+
|
|
108
|
+
**Binding:**
|
|
109
|
+
- `lib/bug_bunny/remote_error.rb` — `BugBunny::RemoteError`
|
|
110
|
+
|
|
111
|
+
## PublishNacked
|
|
112
|
+
Excepción cuando el broker rechaza (`basic.nack`) un mensaje en modo `:confirmed`. Opt-out con `config.nack_raise = false`.
|
|
113
|
+
|
|
114
|
+
**Binding:**
|
|
115
|
+
- `lib/bug_bunny/exception.rb` — `BugBunny::PublishNacked`
|
|
116
|
+
|
|
117
|
+
## PublishUnroutable
|
|
118
|
+
Excepción cuando un mensaje `mandatory: true` en `:confirmed` no es ruteable a ninguna cola (`basic.return`). Opt-out con `config.return_raise = false`.
|
|
119
|
+
|
|
120
|
+
**Binding:**
|
|
121
|
+
- `lib/bug_bunny/exception.rb` — `BugBunny::PublishUnroutable`
|
|
122
|
+
|
|
123
|
+
## Consumer Middleware
|
|
124
|
+
Cadena transversal que corre antes del dispatch al controller (tracing, auth, logging); recibe `delivery_info`, `properties`, `body`; debe hacer yield.
|
|
125
|
+
|
|
126
|
+
**Binding:**
|
|
127
|
+
- `lib/bug_bunny/consumer_middleware.rb` — `BugBunny::ConsumerMiddleware`
|
|
128
|
+
|
|
129
|
+
## Observability
|
|
130
|
+
Mixin de logging estructurado `key=value` que implementa OTel semantic conventions for messaging; `safe_log` nunca lanza; filtra claves sensibles a `[FILTERED]`.
|
|
131
|
+
|
|
132
|
+
**Binding:**
|
|
133
|
+
- `lib/bug_bunny/observability.rb` — `BugBunny::Observability`
|
|
134
|
+
|
|
135
|
+
## 3. Inferencias
|
|
136
|
+
|
|
137
|
+
| Término | Inferencia | confidence | a verificar |
|
|
138
|
+
|---|---|---|---|
|
|
139
|
+
| "bounded context = bug_bunny" | El significado es local a la gema, no global del ecosistema | inferred | humano confirma encuadre DDD |
|
|
140
|
+
| Significado de cada término | Sembrado desde código + glosario ad-hoc migrado del `skill/SKILL.md`; el LLM redactó la prosa | inferred | humano aporta/corrige el significado de negocio (dev-enrich: el LLM infiere menos) |
|
|
141
|
+
| `Binding:` a clase que "es el concepto" | Criterio RFC-009 §2 aplicado por el LLM (qué símbolo *es* el término) | inferred | humano confirma que el símbolo materializa el término y no es sintético |
|
|
142
|
+
|
|
143
|
+
## 4. Cobertura y fronteras
|
|
144
|
+
|
|
145
|
+
- **Parcial y acreta:** glosario sembrado (RFC-009 §2; dev-enrich "se PUEDE sembrar"). Términos nuevos se agregan en el PR que los introduce. Ausencia ≠ inexistencia.
|
|
146
|
+
- **Binding opcional (RFC-009 §2, gap gema-sin-datos resuelto, RFC-009 §5 / issue ai_knowledge#91):** sin capa de datos el `Binding:` se omite/`n/a`; se cita símbolo solo cuando *es* el concepto.
|
|
147
|
+
- **Frontera (DAMA-DMBOK):** esto es Business Glossary (term-céntrico). El Data Dictionary (`definición` por columna, RFC-002 §2.c) no aplica — sin tablas.
|
|
148
|
+
- **Frescura:** si un PR renombra/elimina una clase con `Binding:`, el reviewer verifica que ningún binding quede colgado (RFC-009 §2).
|
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
|
@@ -1,28 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bug_bunny
|
|
3
|
+
description: >-
|
|
4
|
+
Routing RESTful sobre AMQP/RabbitMQ para microservicios Ruby/Rails: RPC
|
|
5
|
+
síncrono, fire-and-forget, Publisher Confirms, Resource ORM, controllers y
|
|
6
|
+
routing DSL tipo Rails. Usar cuando se integra/depura comunicación entre
|
|
7
|
+
servicios via RabbitMQ con la gema bug_bunny.
|
|
8
|
+
triggers:
|
|
9
|
+
- gema `bug_bunny` / módulo `BugBunny`
|
|
10
|
+
- "símbolos `BugBunny::{Client,Consumer,Resource,Controller}`"
|
|
11
|
+
- "excepciones `BugBunny::{PublishUnroutable,PublishNacked,RemoteError}`"
|
|
12
|
+
---
|
|
13
|
+
|
|
1
14
|
# BugBunny Expert
|
|
2
15
|
|
|
3
|
-
|
|
16
|
+
## Qué es / cuándo usar
|
|
4
17
|
|
|
5
|
-
|
|
18
|
+
Gema Ruby: capa de routing RESTful sobre AMQP/RabbitMQ. Microservicios se comunican via RabbitMQ con ergonomía tipo Rails (verbos, controllers, rutas, RPC síncrono, fire-and-forget, Publisher Confirms). Usar esta dependencia cuando integrás/depurás comunicación entre servicios con `bug_bunny`: publicar/consumir, ORM remoto, manejo de errores de broker.
|
|
19
|
+
|
|
20
|
+
## Contrato resumido (piso mínimo)
|
|
21
|
+
|
|
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
|
+
|
|
24
|
+
**Símbolos públicos clave**
|
|
25
|
+
|
|
26
|
+
| Símbolo | Uso típico |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `BugBunny::Client` | `client.request(url, method: :get)` (RPC sync) · `client.publish(url, body:)` (fire-and-forget, 202) · `client.publish(url, confirmed: true, mandatory: true)` (publisher confirms) |
|
|
29
|
+
| `BugBunny::Resource` | ORM tipo AR: `self.exchange=` / `self.resource_name=` / `connection_pool=`; `find/where/create/save/destroy` |
|
|
30
|
+
| `BugBunny::Consumer` | `BugBunny::Consumer.subscribe(connection: BugBunny.create_connection, queue_name:, exchange_name:, routing_key:)` (loop bloqueante) |
|
|
31
|
+
| `BugBunny::Controller` | `before/around/after_action`, `rescue_from`, `render status:, json:` |
|
|
32
|
+
| `BugBunny.routes.draw` | `resources :x` · `namespace` · `member`/`collection` |
|
|
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` · `CommunicationError` (envuelve cualquier `Bunny::Exception` — TCP/conn/canal; `.cause` preserva original) |
|
|
35
|
+
|
|
36
|
+
**Uso típico**
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# Servidor
|
|
40
|
+
BugBunny.routes.draw { resources :nodes }
|
|
41
|
+
BugBunny::Consumer.subscribe(connection: BugBunny.create_connection,
|
|
42
|
+
queue_name: 'inv_q', exchange_name: 'inventory', routing_key: 'nodes')
|
|
43
|
+
|
|
44
|
+
# Cliente
|
|
45
|
+
client = BugBunny::Client.new(pool: pool)
|
|
46
|
+
client.request('nodes/42', method: :get) # => { 'status' => 200, 'body' => {...} }
|
|
47
|
+
client.publish('events', body: { type: 'x' }) # => { 'status' => 202 }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Gotchas/breaking críticos**
|
|
51
|
+
|
|
52
|
+
- URL es **posicional**, no kwarg `path:` — `client.publish(**args)` con `path:` → `ArgumentError`.
|
|
53
|
+
- `confirmed: true` ≠ persistente: para sobrevivir restart hace falta `persistent: true` (+ queue `durable`).
|
|
54
|
+
- `confirmed:true + mandatory:true` con `return_raise` (default `true`) → `PublishUnroutable` si no rutea.
|
|
55
|
+
- `BugBunny::Consumer.subscribe` requiere `connection:`. No correr el Consumer en threads de Puma (loop bloqueante).
|
|
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`.
|
|
58
|
+
|
|
59
|
+
## Índice de artefactos (fuente de verdad)
|
|
60
|
+
|
|
61
|
+
El detalle vive en `docs/<capa>/` (modelo `dev-*`); esta skill **indexa y resume**, no duplica. Links relativos = version-locked (mismo tag del release; `gemspec.files` incluye `docs/**`).
|
|
62
|
+
|
|
63
|
+
| Capa | Artefacto | Estado |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| Glosario de dominio | [docs/glossary/glossary.md](../docs/glossary/glossary.md) | parcial, acreta por PR |
|
|
66
|
+
| Comportamiento (flujos) | [docs/behavior/behavior.md](../docs/behavior/behavior.md) | completa — 6 flujos |
|
|
67
|
+
| Datos | — | n/a — gema sin DB |
|
|
68
|
+
| Operaciones / Interfaz / Topología | — | F2 no implementado — ver Cobertura y fronteras |
|
|
69
|
+
|
|
70
|
+
> **Glosario:** migrado a [docs/glossary/glossary.md](../docs/glossary/glossary.md)
|
|
71
|
+
> (RFC-008 §2 — el compuesto referencia, no copia). Términos AMQP base
|
|
72
|
+
> (Exchange, Queue, Routing Key, RPC, Publisher Confirms, Mandatory, etc.) y su
|
|
73
|
+
> binding físico están ahí.
|
|
74
|
+
|
|
75
|
+
## Cobertura y fronteras
|
|
76
|
+
|
|
77
|
+
**Coexistencia transitoria con destino pendiente (RFC-008 §2 — interim de migración):** mientras la capa de detalle destino (operaciones/interfaz/topología) esté declarada pero **no implementada** (dev-structure F1, F2 del plan), permanecen embebidos bajo el interim normado:
|
|
78
|
+
|
|
79
|
+
- **En esta skill (abajo):** el contrato detallado (jerarquía de excepciones, API de config, modos de entrega) **y** el diagrama de arquitectura (flujo RPC). El *Contrato resumido* de arriba es el piso mínimo (RFC-008 §2); lo de abajo es el detalle interim hasta que exista `docs/api|interface|topology`.
|
|
80
|
+
- **En `README.md`:** el contrato (sin el diagrama de arquitectura).
|
|
81
|
+
- **Guías how-to** (`references/*.md`, pre-estándar): destino futuro `docs/howto/`.
|
|
6
82
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
**AMQP** — Advanced Message Queuing Protocol. Protocolo binario que implementa RabbitMQ.
|
|
10
|
-
**Bunny** — Cliente Ruby para AMQP. BugBunny lo usa internamente para conexiones, canales y publicación.
|
|
11
|
-
**Exchange** — Recibe mensajes del producer y los enruta a queues según reglas. Tipos: `direct` (match exacto), `topic` (wildcards), `fanout` (broadcast).
|
|
12
|
-
**Queue** — Almacena mensajes hasta que un consumer los consume. Las queues durables sobreviven reinicios del broker.
|
|
13
|
-
**Routing Key** — String que el producer adjunta al mensaje. El exchange lo usa para decidir a qué queues enrutar.
|
|
14
|
-
**Binding** — Enlace entre un exchange y una queue, opcionalmente con un patrón de routing key.
|
|
15
|
-
**Session** — `BugBunny::Session` envuelve canales de Bunny con thread-safety y double-checked locking.
|
|
16
|
-
**RPC** — Patrón síncrono que usa la pseudo-cola `amq.rabbitmq.reply-to` para respuestas sin crear queues temporales.
|
|
17
|
-
**Fire-and-Forget** — Patrón asíncrono donde el producer publica y continúa sin esperar respuesta. Retorna `{ 'status' => 202 }`.
|
|
18
|
-
**Publisher Confirms** — Extensión de RabbitMQ que confirma al publisher que el broker recibió el mensaje. BugBunny lo expone como `confirmed: true` en `Client#publish`: el publish bloquea hasta `wait_for_confirms`. Dos señales asíncronas del broker, ambas convertidas en excepciones raise-eables en el publish thread:
|
|
19
|
-
- **NACK** (rechazo explícito) → `BugBunny::PublishNacked` (configurable via `config.nack_raise = false`).
|
|
20
|
-
- **basic.return** (mandatory unroutable) → `BugBunny::PublishUnroutable` (configurable via `config.return_raise = false`).
|
|
21
|
-
**Mandatory** — Flag de `basic.publish` que pide al broker retornar el mensaje al publisher si no es ruteable a ninguna cola. Se procesa via `basic.return` (no es respuesta del request).
|
|
22
|
-
**basic.return** — Evento asincrónico que Bunny dispatcha por exchange (`Bunny::Exchange#on_return`). BugBunny registra un handler único por nombre de exchange al resolverlo en `Session#exchange` y lo delega a `Configuration#on_return` o al default que logea.
|
|
23
|
-
**Resource** — ORM tipo ActiveRecord que mapea operaciones CRUD a llamadas AMQP.
|
|
24
|
-
**Consumer** — Worker bloqueante que despacha mensajes a controladores mediante un Router.
|
|
25
|
-
**Connection Pool** — Pool de conexiones (`connection_pool` gem) que comparte sessions entre threads. Cada slot cachea su `Session` y `Producer`.
|
|
83
|
+
Por RFC-008 §2: no se fabrica la capa, no se borra contrato sin destino, no se duplica; migra cuando F2 entregue, mismo PR. Estado transitorio declarado, no excepción permanente. Origen del gap (resuelto, normado): [sequre/ai_knowledge#95](https://github.com/sequre/ai_knowledge/issues/95).
|
|
26
84
|
|
|
27
85
|
---
|
|
28
86
|
|
|
@@ -374,8 +432,8 @@ Limitación de RSpec: `instance_double` valida que el método exista pero **no**
|
|
|
374
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`).
|
|
375
433
|
|
|
376
434
|
### BugBunny::CommunicationError
|
|
377
|
-
**Causa:** Fallo de
|
|
378
|
-
**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.
|
|
379
437
|
|
|
380
438
|
Ver catálogo completo en [Errores](references/errores.md).
|
|
381
439
|
|
data/skills.yml
CHANGED
|
@@ -8,7 +8,17 @@ skills:
|
|
|
8
8
|
repo: sequre/ai_knowledge
|
|
9
9
|
gem-release:
|
|
10
10
|
repo: sequre/ai_knowledge
|
|
11
|
-
|
|
11
|
+
dev-structure:
|
|
12
|
+
repo: sequre/ai_knowledge
|
|
13
|
+
dev-compose:
|
|
14
|
+
repo: sequre/ai_knowledge
|
|
15
|
+
dev-enrich:
|
|
16
|
+
repo: sequre/ai_knowledge
|
|
17
|
+
skill-feedback:
|
|
18
|
+
repo: sequre/ai_knowledge
|
|
19
|
+
agent-issue:
|
|
20
|
+
repo: sequre/ai_knowledge
|
|
21
|
+
dev-flow:
|
|
12
22
|
repo: sequre/ai_knowledge
|
|
13
23
|
matrix-element:
|
|
14
24
|
repo: sequre/ai_knowledge
|
|
@@ -17,10 +27,6 @@ skills:
|
|
|
17
27
|
auth_token: "${MATRIX_AUTH_TOKEN}"
|
|
18
28
|
rooms:
|
|
19
29
|
agents: "!VCHwQXgmXdyhhhPhoz:matrix.cloud.wispro.co"
|
|
20
|
-
skill-feedback:
|
|
21
|
-
repo: sequre/ai_knowledge
|
|
22
|
-
agent-issue:
|
|
23
|
-
repo: sequre/ai_knowledge
|
|
24
30
|
documentation-writer:
|
|
25
31
|
repo: github/awesome-copilot
|
|
26
32
|
path: skills/documentation-writer
|
|
@@ -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
|
|
@@ -233,6 +233,8 @@ files:
|
|
|
233
233
|
- CLAUDE.md
|
|
234
234
|
- README.md
|
|
235
235
|
- Rakefile
|
|
236
|
+
- docs/behavior/behavior.md
|
|
237
|
+
- docs/glossary/glossary.md
|
|
236
238
|
- initializer_example.rb
|
|
237
239
|
- lib/bug_bunny.rb
|
|
238
240
|
- lib/bug_bunny/client.rb
|
|
@@ -281,6 +283,7 @@ files:
|
|
|
281
283
|
- spec/support/bunny_mocks.rb
|
|
282
284
|
- spec/support/integration_helper.rb
|
|
283
285
|
- spec/unit/client_session_pool_spec.rb
|
|
286
|
+
- spec/unit/communication_error_wrapping_spec.rb
|
|
284
287
|
- spec/unit/configuration_spec.rb
|
|
285
288
|
- spec/unit/consumer_middleware_spec.rb
|
|
286
289
|
- spec/unit/consumer_spec.rb
|
|
@@ -304,7 +307,7 @@ metadata:
|
|
|
304
307
|
homepage_uri: https://github.com/gedera/bug_bunny
|
|
305
308
|
source_code_uri: https://github.com/gedera/bug_bunny
|
|
306
309
|
changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
|
|
307
|
-
documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.
|
|
310
|
+
documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.18.0/skill
|
|
308
311
|
post_install_message:
|
|
309
312
|
rdoc_options: []
|
|
310
313
|
require_paths:
|