bug_bunny 4.16.0 → 4.17.1
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 +57 -0
- data/CLAUDE.md +20 -4
- data/README.md +108 -25
- data/docs/behavior/behavior.md +176 -0
- data/docs/glossary/glossary.md +148 -0
- data/lib/bug_bunny/client.rb +29 -4
- data/lib/bug_bunny/version.rb +1 -1
- data/skill/SKILL.md +116 -21
- data/skill/references/client-middleware.md +12 -0
- data/skills.yml +11 -5
- data/spec/unit/client_session_pool_spec.rb +102 -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: 50bb70f17d783b52abeb208bc362953c9494b61853f836da84f73552e3a76966
|
|
4
|
+
data.tar.gz: 37f9550b64942b1e544a6e7d593c1695f1293c6a1b488e1b0bec3179d1f3b7a4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4733c50dcb6824c4e7d53bc0842e823b0674c13bf6cd9d963f99b6fa090166d083ca64cc6e65eba2fc29bff09be840772a6404fae19e62f3dd9b6dbcf56fc2d3
|
|
7
|
+
data.tar.gz: e9613f65b4bce0ad19aa8c19f259c6a3069ba4dfe185e4146a5992ddfbd20b3428816ed882e9d637f2b4a85d1d5d307094763bf8f3082900e8c6d9d3fe8a1fb7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.17.1] - 2026-05-19
|
|
4
|
+
|
|
5
|
+
> 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`.
|
|
6
|
+
|
|
7
|
+
### Nuevas funcionalidades
|
|
8
|
+
- Incorporar familia de skills `dev-*` (glosario + comportamiento) y recomponer composites (#47) — @Gabriel
|
|
9
|
+
|
|
10
|
+
### Correcciones
|
|
11
|
+
- Consistencia README: Quickstart Consumer, `rpc_timeout`, idioma, coexistencia — @Gabriel
|
|
12
|
+
- Escapar `;` en Note del diagrama Mermaid (flujo confirmed) — @Gabriel
|
|
13
|
+
|
|
14
|
+
### Documentación
|
|
15
|
+
- Marcar verificación humana de `behavior.md` completada (RFC-001 §3.3) — @Gabriel
|
|
16
|
+
- Rework `skill/SKILL.md` a RFC-008 endurecido (dev-compose 2.0.0) — @Gabriel
|
|
17
|
+
- Alinear nota de coexistencia skill/README — @Gabriel
|
|
18
|
+
|
|
19
|
+
### Otros cambios
|
|
20
|
+
- Migrar de `service-release` a `gem-release` — @Gabriel
|
|
21
|
+
|
|
22
|
+
## [4.17.0] - 2026-05-13
|
|
23
|
+
|
|
24
|
+
### Nuevas funcionalidades
|
|
25
|
+
- **`Client::REQUEST_ATTRS` extendido con metadata AMQP estándar (#45):** Los siguientes atributos del Request ahora pueden pasarse como kwargs directos en `client.publish` / `client.request` sin necesidad del block API:
|
|
26
|
+
- `persistent` (Boolean) — `delivery_mode: 2` AMQP. Critical: por default es `false`; `confirmed: true` **NO** lo implica.
|
|
27
|
+
- `correlation_id` (String) — Tracing explícito (sobreescribe el auto-asignado por RPC y `return_raise`).
|
|
28
|
+
- `priority` (Integer 0-255).
|
|
29
|
+
- `app_id` (String).
|
|
30
|
+
- `content_type` (String, default `'application/json'`).
|
|
31
|
+
- `content_encoding` (String).
|
|
32
|
+
- `expiration` (String, TTL en ms).
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
# Antes (4.16): requería block API
|
|
36
|
+
client.publish('evt', exchange: 'x', confirmed: true) do |req|
|
|
37
|
+
req.persistent = true
|
|
38
|
+
req.correlation_id = 'cid-123'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Ahora (4.17): kwargs directos
|
|
42
|
+
client.publish('evt', exchange: 'x', confirmed: true,
|
|
43
|
+
persistent: true, correlation_id: 'cid-123')
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
El block API sigue funcionando para overrides puntuales o para atributos no expuestos (`timestamp`, `type`, `reply_to`).
|
|
47
|
+
|
|
48
|
+
### Correcciones
|
|
49
|
+
- **`apply_args` usa `args.key?` en lugar de truthy check:** Permite pasar valores falsy explícitos (ej. `persistent: false`, `priority: 0`) que antes se filtraban silenciosamente como si no se hubieran pasado.
|
|
50
|
+
|
|
51
|
+
### Documentación (motivado por #45 — adopción real en sequre/box_radius_manager#18)
|
|
52
|
+
- README + SKILL.md cubren cuatro gotchas detectados solo en integration tests:
|
|
53
|
+
1. `url` es positional, no kwarg `:path`. Splatear un hash con `path:` rompe.
|
|
54
|
+
2. `confirmed: true` no implica `persistent: true` — son flags ortogonales.
|
|
55
|
+
3. Default `exchange_options` es `{ durable: false }` — publishers a exchange compartido deben pasar `exchange_options: { durable: true }` explícito.
|
|
56
|
+
4. `instance_double` no detecta arity mismatch con splat de kwargs — recomendación de smoke test integration para cada publisher nuevo.
|
|
57
|
+
- Nueva sección "Production publisher recipe" en README y receta canónica en SKILL.md con la combinación recomendada para auditoría/billing/accounting.
|
|
58
|
+
- `skill/references/client-middleware.md` con tabla completa de kwargs de Request actualizada.
|
|
59
|
+
|
|
3
60
|
## [4.16.0] - 2026-05-13
|
|
4
61
|
|
|
5
62
|
### Cambios de comportamiento (semi-breaking)
|
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
|
|
@@ -210,6 +210,73 @@ client.publish('events', body: { type: 'user.signed_in', user_id: 42 })
|
|
|
210
210
|
client.request('users', method: :get, params: { role: 'admin', page: 2 })
|
|
211
211
|
```
|
|
212
212
|
|
|
213
|
+
### Gotchas
|
|
214
|
+
|
|
215
|
+
**URL is positional, not a kwarg.** The first argument of `client.request` / `client.publish` is positional. There is **no** `path:` kwarg, splatting a hash with `path:` will fail silently or raise `ArgumentError`:
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
args = { exchange: 'ingest.x', body: payload }
|
|
219
|
+
client.publish(**args) # ❌ ArgumentError: wrong number of arguments
|
|
220
|
+
client.publish('event.name', **args) # ✅
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Block runs after kwargs.** Keyword args are applied first; the block (if given) can override them. Use kwargs for the common case and block for atypical setup:
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
client.publish('evt', exchange: 'x', persistent: true) do |req|
|
|
227
|
+
req.timestamp = some_past_time # only via block — not in REQUEST_ATTRS
|
|
228
|
+
end
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Production publisher recipe
|
|
232
|
+
|
|
233
|
+
Defaults aimed at sane microservices — declare durable exchanges, persistent messages, confirmed delivery with mandatory routing, explicit correlation id:
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
client.publish('acct.start',
|
|
237
|
+
exchange: 'ingest.radius',
|
|
238
|
+
exchange_type: :topic,
|
|
239
|
+
exchange_options: { durable: true }, # match consumer-declared exchange
|
|
240
|
+
body: payload,
|
|
241
|
+
confirmed: true,
|
|
242
|
+
mandatory: true, # raise PublishUnroutable if no binding
|
|
243
|
+
persistent: true, # delivery_mode: 2 — survives broker restart
|
|
244
|
+
correlation_id: SecureRandom.uuid,
|
|
245
|
+
app_id: 'radius_manager')
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
| AMQP property | Kwarg | Reason it matters for critical publishers |
|
|
249
|
+
|---|---|---|
|
|
250
|
+
| `delivery_mode` | `persistent: true` | Without it, the message lives only in broker RAM (lost on restart). Default `false`. |
|
|
251
|
+
| `confirmation` | `confirmed: true` | Block until the broker acks. Without it, `client.publish` returns 202 before the broker sees the message. |
|
|
252
|
+
| `mandatory` | `mandatory: true` | Catches misrouted publishes. Combined with `return_raise` (default `true`), raises `PublishUnroutable` instead of silently dropping. |
|
|
253
|
+
| `exchange durable` | `exchange_options: { durable: true }` | Match the exchange definition that consumers declare. Mismatch raises `Bunny::PreconditionFailed`. |
|
|
254
|
+
| `correlation_id` | `correlation_id:` | Tracing. Auto-generated when missing for RPC and for `confirmed + mandatory + return_raise`, but explicit is preferred. |
|
|
255
|
+
|
|
256
|
+
### Testing publishers
|
|
257
|
+
|
|
258
|
+
Mocks of `Client` (via `instance_double`) **do not catch arity mismatches** when the caller does splat (`**args`). Signature errors like passing `path:` as kwarg or unknown keys won't surface in unit tests with mocks. **Add a smoke integration test** for new publishers — declare an exclusive queue, bind to the exchange, publish, `queue.pop`, assert `correlation_id`, `headers`, `routing_key`, `delivery_mode`:
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
RSpec.describe 'MyPublisher', :integration do
|
|
262
|
+
it 'publishes with correct AMQP metadata' do
|
|
263
|
+
conn = BugBunny.create_connection
|
|
264
|
+
ch = conn.create_channel
|
|
265
|
+
x = ch.topic('ingest.radius', durable: true)
|
|
266
|
+
q = ch.queue('', exclusive: true).bind(x, routing_key: 'acct.#')
|
|
267
|
+
|
|
268
|
+
MyPublisher.call(payload)
|
|
269
|
+
|
|
270
|
+
_delivery, props, body = q.pop(manual_ack: false)
|
|
271
|
+
expect(props.correlation_id).not_to be_nil
|
|
272
|
+
expect(props.delivery_mode).to eq(2) # persistent
|
|
273
|
+
expect(JSON.parse(body)).to include(...)
|
|
274
|
+
ensure
|
|
275
|
+
conn&.close
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
```
|
|
279
|
+
|
|
213
280
|
### Publisher Confirms (delivery-critical events)
|
|
214
281
|
|
|
215
282
|
For events where you need a delivery guarantee from the broker (auditing, billing, accounting) without the cost of a full RPC, use `publish` with `confirmed: true`. The call blocks until the broker acknowledges receipt:
|
|
@@ -272,9 +339,9 @@ BugBunny.consumer_middlewares.use TracingMiddleware
|
|
|
272
339
|
|
|
273
340
|
## Observability
|
|
274
341
|
|
|
275
|
-
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.
|
|
276
343
|
|
|
277
|
-
|
|
344
|
+
All internal events are emitted as `key=value` logs compatible with Datadog, CloudWatch, ELK and ExisRay.
|
|
278
345
|
|
|
279
346
|
```
|
|
280
347
|
component=bug_bunny event=producer.publish method=POST path=acct/publish messaging_destination_name=acct_x messaging_routing_key=acct.start.42
|
|
@@ -286,19 +353,19 @@ component=bug_bunny event=consumer.execution_error error_class=RuntimeError erro
|
|
|
286
353
|
component=bug_bunny event=consumer.connection_error attempt_count=2 retry_in_s=10 error_message="..."
|
|
287
354
|
```
|
|
288
355
|
|
|
289
|
-
###
|
|
356
|
+
### Internally measured durations
|
|
290
357
|
|
|
291
|
-
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`).
|
|
292
359
|
|
|
293
|
-
|
|
|
360
|
+
| Event | Duration | Measures |
|
|
294
361
|
|---|---|---|
|
|
295
|
-
| `producer.published` | `duration_s` |
|
|
296
|
-
| `producer.confirmed` | `publish_duration_s` + `confirm_duration_s` + `duration_s` (total) | Publish +
|
|
297
|
-
| `producer.rpc_response_received` | `duration_s` |
|
|
298
|
-
| `consumer.message_processed` | `duration_s` |
|
|
299
|
-
| `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. |
|
|
300
367
|
|
|
301
|
-
|
|
368
|
+
Sensitive keys (`password`, `token`, `secret`, `api_key`, `authorization`, etc.) are automatically filtered to `[FILTERED]` across all log output.
|
|
302
369
|
|
|
303
370
|
---
|
|
304
371
|
|
|
@@ -359,17 +426,33 @@ end
|
|
|
359
426
|
|
|
360
427
|
---
|
|
361
428
|
|
|
362
|
-
##
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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)
|
|
373
456
|
|
|
374
457
|
---
|
|
375
458
|
|
|
@@ -0,0 +1,176 @@
|
|
|
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 `a5cdb10` · cobertura: completa (6 flujos) · verificado por humano 2026-05-18
|
|
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
|
+
### Flujo: Confirmed + basic.return bridge
|
|
72
|
+
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
|
+
|
|
74
|
+
```mermaid
|
|
75
|
+
sequenceDiagram
|
|
76
|
+
participant CL as Client
|
|
77
|
+
participant P as Producer
|
|
78
|
+
participant S as Session
|
|
79
|
+
participant BR as RabbitMQ
|
|
80
|
+
participant RT as Bunny reader thread
|
|
81
|
+
CL->>P: publish confirmed:true [mandatory]
|
|
82
|
+
P->>S: register_return_listener(cid) → Event+slot
|
|
83
|
+
P->>BR: publish_message (mandatory)
|
|
84
|
+
P-->>P: wait_for_confirms! — BLOQUEA (IVar si hay timeout)
|
|
85
|
+
BR-->>RT: basic.return (si unroutable)
|
|
86
|
+
RT->>S: handle_broker_return → signal_return_listener
|
|
87
|
+
S->>S: slot[:info]=info · slot[:event].set
|
|
88
|
+
BR-->>P: confirms (ack/nack)
|
|
89
|
+
P->>P: handle_confirm_result — nack → PublishNacked
|
|
90
|
+
P->>P: handle_return_result → event.wait(RETURN_RACE_WINDOW_S=0.05s)
|
|
91
|
+
P->>P: slot[:info] presente → raise_unroutable!
|
|
92
|
+
P-->>CL: PublishUnroutable (o éxito si slot vacío)
|
|
93
|
+
Note over RT,S: on_return corre antes del raise · su excepción se loggea (no propaga)
|
|
94
|
+
```
|
|
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`).
|
|
96
|
+
|
|
97
|
+
### Flujo: Consumer subscribe loop + reconnect + health
|
|
98
|
+
Loop bloqueante infinito con backoff exponencial + jitter en error; health check en TimerTask separado, **acoplado flojo** vía estado de session.
|
|
99
|
+
|
|
100
|
+
```mermaid
|
|
101
|
+
sequenceDiagram
|
|
102
|
+
participant C as Consumer
|
|
103
|
+
participant S as Session
|
|
104
|
+
participant BR as RabbitMQ
|
|
105
|
+
participant H as Health TimerTask
|
|
106
|
+
C->>S: exchange/queue declare · q.bind(rk)
|
|
107
|
+
C->>H: start_health_check (touch file)
|
|
108
|
+
C->>BR: q.subscribe(manual_ack, block:true) — LOOP
|
|
109
|
+
loop por mensaje
|
|
110
|
+
BR->>C: deliver → consumer_middlewares → process_message
|
|
111
|
+
end
|
|
112
|
+
H-->>S: cada interval: queue_declare(passive:true)
|
|
113
|
+
H-->>S: falla → cierra session (fuerza reconnect)
|
|
114
|
+
Note over C: rescue StandardError → attempt++ · backoff min(nri*2^(n-1), max) · sleep · retry (redeclara)
|
|
115
|
+
Note over C: max_reconnect_attempts alcanzado → raise (fatal) · ensure → shutdown
|
|
116
|
+
```
|
|
117
|
+
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.
|
|
118
|
+
|
|
119
|
+
### Flujo: Error handling / RemoteError
|
|
120
|
+
Excepción no manejada en controller → serializada (clase/mensaje/backtrace[0..25]) → reply 500 → reconstruida client-side por `Middleware::RaiseError`.
|
|
121
|
+
|
|
122
|
+
```mermaid
|
|
123
|
+
sequenceDiagram
|
|
124
|
+
participant CT as Controller
|
|
125
|
+
participant CO as Consumer
|
|
126
|
+
participant BR as RabbitMQ
|
|
127
|
+
participant RE as Middleware::RaiseError
|
|
128
|
+
participant CL as Caller
|
|
129
|
+
CT->>CT: action raise → rescue → handle_exception
|
|
130
|
+
CT->>CT: rescue_from match? sí→handler · no→500 + RemoteError.serialize
|
|
131
|
+
CT-->>CO: response 500 (bug_bunny_exception)
|
|
132
|
+
CO->>CO: handle_fatal_error → RemoteError.serialize
|
|
133
|
+
CO->>BR: reply 500 a reply_to
|
|
134
|
+
BR->>RE: response (vía reply listener + parse_response)
|
|
135
|
+
RE->>RE: status 500..599 + bug_bunny_exception
|
|
136
|
+
RE-->>CL: raise BugBunny::RemoteError(class,message,backtrace)
|
|
137
|
+
```
|
|
138
|
+
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).
|
|
139
|
+
|
|
140
|
+
### Flujo: Client middleware stack (onion)
|
|
141
|
+
`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).
|
|
142
|
+
|
|
143
|
+
```mermaid
|
|
144
|
+
sequenceDiagram
|
|
145
|
+
participant U as Caller
|
|
146
|
+
participant O as MW externo (primer use)
|
|
147
|
+
participant I as MW interno (último use)
|
|
148
|
+
participant P as Producer (final_action)
|
|
149
|
+
U->>O: call(env)
|
|
150
|
+
O->>O: on_request
|
|
151
|
+
O->>I: @app.call(env)
|
|
152
|
+
I->>I: on_request
|
|
153
|
+
I->>P: @app.call(env) → rpc/fire/confirmed
|
|
154
|
+
P-->>I: response
|
|
155
|
+
I->>I: on_complete
|
|
156
|
+
I-->>O: response
|
|
157
|
+
O->>O: on_complete (RaiseError: 4xx/5xx → raise)
|
|
158
|
+
O-->>U: response / excepción
|
|
159
|
+
```
|
|
160
|
+
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`).
|
|
161
|
+
|
|
162
|
+
## 3. Inferencias
|
|
163
|
+
|
|
164
|
+
| Inferencia | confidence | a verificar (humano) |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| 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
|
+
| 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
|
+
| Health check acoplado flojo al loop vía cierre de session | inferred | confirmar `consumer.rb:340-361` vs `106-124` |
|
|
169
|
+
|
|
170
|
+
## 4. Cobertura y fronteras
|
|
171
|
+
|
|
172
|
+
- **Modo:** completo on-demand (RFC-007 §2). Default futuro = incremental por PR; este artefacto se actualiza en el mismo PR que toque cualquier flujo.
|
|
173
|
+
- **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
|
+
- **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
|
+
- **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).
|
|
@@ -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
|
@@ -29,9 +29,17 @@ module BugBunny
|
|
|
29
29
|
attr_accessor :delivery_mode
|
|
30
30
|
|
|
31
31
|
# Argumentos del cliente que se mapean 1:1 a setters del Request.
|
|
32
|
+
#
|
|
33
|
+
# Incluye opciones de enrutamiento (`delivery_mode`, `method`, `body`, `exchange`,
|
|
34
|
+
# `exchange_type`, `routing_key`, `timeout`, `params`), de infraestructura
|
|
35
|
+
# (`exchange_options`, `queue_options`) y de metadata AMQP estándar
|
|
36
|
+
# (`persistent`, `correlation_id`, `priority`, `app_id`, `content_type`,
|
|
37
|
+
# `content_encoding`, `expiration`). Lo que no esté acá debe setearse via
|
|
38
|
+
# block API (`do |req| ... end`).
|
|
32
39
|
REQUEST_ATTRS = %i[
|
|
33
40
|
delivery_mode method body exchange exchange_type routing_key
|
|
34
41
|
timeout exchange_options queue_options params
|
|
42
|
+
persistent correlation_id priority app_id content_type content_encoding expiration
|
|
35
43
|
].freeze
|
|
36
44
|
|
|
37
45
|
# Inicializa un nuevo cliente.
|
|
@@ -64,7 +72,8 @@ module BugBunny
|
|
|
64
72
|
#
|
|
65
73
|
# Envía un mensaje y bloquea la ejecución del hilo actual hasta recibir respuesta.
|
|
66
74
|
#
|
|
67
|
-
# @param url [String] La ruta del recurso (ej: 'users/1').
|
|
75
|
+
# @param url [String] La ruta del recurso (ej: 'users/1'). **Argumento posicional** —
|
|
76
|
+
# no existe el kwarg `:path`. Splatear un hash con `path:` falla silencioso.
|
|
68
77
|
# @param args [Hash] Opciones de configuración.
|
|
69
78
|
# @option args [Symbol] :method El verbo HTTP (:get, :post, :put, :delete). Default: :get.
|
|
70
79
|
# @option args [Object] :body El cuerpo del mensaje.
|
|
@@ -72,7 +81,18 @@ module BugBunny
|
|
|
72
81
|
# @option args [Integer] :timeout Tiempo máximo de espera.
|
|
73
82
|
# @option args [Hash] :exchange_options Opciones específicas para la declaración del Exchange.
|
|
74
83
|
# @option args [Hash] :queue_options Opciones específicas para la declaración de la Cola.
|
|
75
|
-
# @
|
|
84
|
+
# @option args [Boolean] :persistent Si `true`, `delivery_mode: 2` (mensaje persiste en disco
|
|
85
|
+
# del broker). Default `false`. Útil para mensajes críticos sobre queues durables.
|
|
86
|
+
# @option args [String] :correlation_id ID de correlación AMQP. Útil para tracing custom.
|
|
87
|
+
# Si no se setea, `Producer#rpc` y `Producer#confirmed` (con return_raise) auto-asignan UUID.
|
|
88
|
+
# @option args [Integer] :priority Prioridad del mensaje (0-255). Requiere queue declarada con
|
|
89
|
+
# `x-max-priority` para que el broker la respete.
|
|
90
|
+
# @option args [String] :app_id Identificador del publisher.
|
|
91
|
+
# @option args [String] :content_type MIME type. Default `'application/json'`.
|
|
92
|
+
# @option args [String] :content_encoding Encoding del payload (ej: 'gzip').
|
|
93
|
+
# @option args [String] :expiration TTL del mensaje en ms (formato AMQP).
|
|
94
|
+
# @yield [req] Bloque para configurar el objeto Request directamente. Necesario para
|
|
95
|
+
# atributos no mapeados como kwarg (ej: `req.timestamp = ...`).
|
|
76
96
|
# @return [Hash] La respuesta del servidor.
|
|
77
97
|
def request(url, **args)
|
|
78
98
|
send(url, **args) do |req|
|
|
@@ -87,7 +107,8 @@ module BugBunny
|
|
|
87
107
|
# que el broker confirme la recepción del mensaje. Útil para eventos críticos (auditoría,
|
|
88
108
|
# billing) donde se requiere garantía de entrega sin el overhead de un RPC completo.
|
|
89
109
|
#
|
|
90
|
-
# @param url [String] La ruta del evento/recurso.
|
|
110
|
+
# @param url [String] La ruta del evento/recurso. **Argumento posicional** — no
|
|
111
|
+
# existe el kwarg `:path`. Splatear un hash con `path:` falla silencioso.
|
|
91
112
|
# @param args [Hash] Mismas opciones que {#request}, excepto `:timeout`. Adicionales:
|
|
92
113
|
# @option args [Boolean] :confirmed Si `true`, espera `wait_for_confirms` del broker.
|
|
93
114
|
# @option args [Boolean] :mandatory Si `true`, el broker retorna el mensaje si no es ruteable.
|
|
@@ -158,12 +179,16 @@ module BugBunny
|
|
|
158
179
|
|
|
159
180
|
# Mapea los argumentos generales (no específicos de Publisher Confirms) sobre el Request.
|
|
160
181
|
#
|
|
182
|
+
# Usa `args.key?` en lugar de truthy check para que `persistent: false`,
|
|
183
|
+
# `priority: 0` u otros valores falsy explícitos del caller sean honrados
|
|
184
|
+
# (no se filtran como si no hubieran sido pasados).
|
|
185
|
+
#
|
|
161
186
|
# @param req [BugBunny::Request]
|
|
162
187
|
# @param args [Hash]
|
|
163
188
|
# @return [void]
|
|
164
189
|
def apply_args(req, args)
|
|
165
190
|
REQUEST_ATTRS.each do |key|
|
|
166
|
-
req.public_send("#{key}=", args[key]) if args
|
|
191
|
+
req.public_send("#{key}=", args[key]) if args.key?(key)
|
|
167
192
|
end
|
|
168
193
|
req.headers.merge!(args[:headers]) if args[:headers]
|
|
169
194
|
end
|
data/lib/bug_bunny/version.rb
CHANGED
data/skill/SKILL.md
CHANGED
|
@@ -1,28 +1,85 @@
|
|
|
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.17.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` |
|
|
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
|
+
|
|
58
|
+
## Índice de artefactos (fuente de verdad)
|
|
59
|
+
|
|
60
|
+
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/**`).
|
|
6
61
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
**
|
|
22
|
-
|
|
23
|
-
**
|
|
24
|
-
**
|
|
25
|
-
**
|
|
62
|
+
| Capa | Artefacto | Estado |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| Glosario de dominio | [docs/glossary/glossary.md](../docs/glossary/glossary.md) | parcial, acreta por PR |
|
|
65
|
+
| Comportamiento (flujos) | [docs/behavior/behavior.md](../docs/behavior/behavior.md) | completa — 6 flujos |
|
|
66
|
+
| Datos | — | n/a — gema sin DB |
|
|
67
|
+
| Operaciones / Interfaz / Topología | — | F2 no implementado — ver Cobertura y fronteras |
|
|
68
|
+
|
|
69
|
+
> **Glosario:** migrado a [docs/glossary/glossary.md](../docs/glossary/glossary.md)
|
|
70
|
+
> (RFC-008 §2 — el compuesto referencia, no copia). Términos AMQP base
|
|
71
|
+
> (Exchange, Queue, Routing Key, RPC, Publisher Confirms, Mandatory, etc.) y su
|
|
72
|
+
> binding físico están ahí.
|
|
73
|
+
|
|
74
|
+
## Cobertura y fronteras
|
|
75
|
+
|
|
76
|
+
**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:
|
|
77
|
+
|
|
78
|
+
- **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`.
|
|
79
|
+
- **En `README.md`:** el contrato (sin el diagrama de arquitectura).
|
|
80
|
+
- **Guías how-to** (`references/*.md`, pre-estándar): destino futuro `docs/howto/`.
|
|
81
|
+
|
|
82
|
+
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
83
|
|
|
27
84
|
---
|
|
28
85
|
|
|
@@ -245,6 +302,21 @@ client.publish('acct.start', exchange: 'acct_x', body: payload,
|
|
|
245
302
|
# → { 'status' => 202, 'body' => nil } # broker ACK confirmado
|
|
246
303
|
```
|
|
247
304
|
|
|
305
|
+
**Receta canónica de publisher productivo (auditoría / billing / accounting):**
|
|
306
|
+
```ruby
|
|
307
|
+
client.publish('acct.start',
|
|
308
|
+
exchange: 'ingest.radius',
|
|
309
|
+
exchange_type: :topic,
|
|
310
|
+
exchange_options: { durable: true }, # matchear declaración del consumer
|
|
311
|
+
body: payload,
|
|
312
|
+
confirmed: true, # broker ACK síncrono
|
|
313
|
+
mandatory: true, # raise PublishUnroutable si no hay binding
|
|
314
|
+
persistent: true, # delivery_mode: 2 — sobrevive restart
|
|
315
|
+
correlation_id: SecureRandom.uuid, # tracing explícito
|
|
316
|
+
app_id: 'radius_manager')
|
|
317
|
+
```
|
|
318
|
+
A partir de 4.17, `persistent`, `correlation_id`, `priority`, `app_id`, `content_type`, `content_encoding` y `expiration` están en `Client::REQUEST_ATTRS` y se aceptan como kwargs. El block API sigue funcionando para overrides puntuales o para atributos no expuestos (`timestamp`, `type`).
|
|
319
|
+
|
|
248
320
|
**Dos señales del broker, dos excepciones simétricas:**
|
|
249
321
|
|
|
250
322
|
| Señal | Default | Excepción | Campos |
|
|
@@ -311,6 +383,29 @@ No guardar el resultado de `Order.with(...)` en una variable para múltiples lla
|
|
|
311
383
|
### Registrar middleware durante call()
|
|
312
384
|
No registrar consumer middlewares durante la ejecución de `call()`. El stack toma un snapshot al inicio; los registros concurrentes no afectan la ejecución actual.
|
|
313
385
|
|
|
386
|
+
### Pasar `path:` como kwarg a `Client#publish` / `#request`
|
|
387
|
+
El primer argumento es **posicional** (`url`). No hay kwarg `:path`. Splatear un hash que tenga `path:` falla con `ArgumentError: wrong number of arguments`. Construir args sin path y pasar la URL aparte:
|
|
388
|
+
```ruby
|
|
389
|
+
args = { exchange: 'x', body: payload }
|
|
390
|
+
client.publish('event.name', **args) # ✅
|
|
391
|
+
client.publish(**args.merge(path: 'event.name')) # ❌
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Asumir que `confirmed: true` implica persistencia
|
|
395
|
+
`confirmed: true` solo activa Publisher Confirms (broker ACK síncrono). **NO** setea `delivery_mode: 2`. El default de `Request#persistent` es `false` — el mensaje vive en RAM del broker y se pierde si reinicia. Para eventos críticos sobre queue durable hay que pasar `persistent: true` (a partir de 4.17) o setearlo via block. Tabla de decisión:
|
|
396
|
+
|
|
397
|
+
| Necesitás | Pasar |
|
|
398
|
+
|---|---|
|
|
399
|
+
| Broker confirma recepción | `confirmed: true` |
|
|
400
|
+
| Mensaje sobrevive restart | `persistent: true` (requiere queue `durable: true`) |
|
|
401
|
+
| Raise si no rutea | `mandatory: true` (+ `return_raise: true` default) |
|
|
402
|
+
|
|
403
|
+
### Olvidar `exchange_options: { durable: true }` en publishers a exchange compartido
|
|
404
|
+
`DEFAULT_EXCHANGE_OPTIONS = { durable: false, auto_delete: false }`. Si un consumer previamente declaró el exchange como durable (caso normal en producción), un publisher que use el default va a recibir `Bunny::PreconditionFailed - inequivalent arg 'durable'` al re-declarar. Solución: pasar `exchange_options: { durable: true }` en el publisher, o setear global `BugBunny.configure { |c| c.exchange_options = { durable: true } }`.
|
|
405
|
+
|
|
406
|
+
### Confiar en `instance_double(BugBunny::Client)` para detectar errores de signature
|
|
407
|
+
Limitación de RSpec: `instance_double` valida que el método exista pero **no** valida arity estricta cuando el caller hace `**args` splat. Tests con mocks pasan, integration con broker real rompe. Mitigación: para cada publisher nuevo, sumar un smoke spec `:integration` que declare queue exclusiva con binding, publique, haga `queue.pop`, y verifique `correlation_id`, `delivery_mode`, `headers`, `routing_key`.
|
|
408
|
+
|
|
314
409
|
---
|
|
315
410
|
|
|
316
411
|
## Errores Comunes
|
|
@@ -47,6 +47,18 @@ end
|
|
|
47
47
|
| `mandatory` | Boolean | `false` | Pide al broker retornar el mensaje si no es ruteable. Solo útil con `confirmed: true`. |
|
|
48
48
|
| `confirm_timeout` | Float | `nil` | Segundos máximos a esperar el ACK. `nil` = espera indefinida. Excedido → `BugBunny::RequestTimeout`. |
|
|
49
49
|
| `nack_raise` | Boolean | `nil` | Override per-request de `config.nack_raise`. `nil` = usa flag global. |
|
|
50
|
+
| `return_raise` | Boolean | `nil` | Override per-request de `config.return_raise`. Requiere `confirmed: true` y `mandatory: true`. |
|
|
51
|
+
| `persistent` | Boolean | `false` | `delivery_mode: 2` AMQP. Mensaje sobrevive restart del broker. Requiere queue durable. |
|
|
52
|
+
| `correlation_id` | String | nil | ID de correlación AMQP. Auto-asignado UUID en RPC y en `confirmed + mandatory + return_raise`. |
|
|
53
|
+
| `priority` | Integer | nil | Prioridad 0-255. Requiere queue con `x-max-priority`. |
|
|
54
|
+
| `app_id` | String | nil | Identificador del publisher (AMQP `app-id`). |
|
|
55
|
+
| `content_type` | String | `'application/json'` | MIME type del payload. |
|
|
56
|
+
| `content_encoding` | String | nil | Encoding del payload (`'gzip'`, `'deflate'`, etc.). |
|
|
57
|
+
| `expiration` | String | nil | TTL del mensaje en ms (formato AMQP). |
|
|
58
|
+
|
|
59
|
+
**Gotcha:** el primer argumento de `Client#publish` / `#request` es **posicional** (`url`). No existe el kwarg `:path`. Splatear un hash con `path:` falla con `ArgumentError` o se ignora silencioso.
|
|
60
|
+
|
|
61
|
+
**Atributos no expuestos como kwarg** (solo via block API): `timestamp` (default `Time.now.to_i`), `type` (default `full_path`), `reply_to` (RPC interno).
|
|
50
62
|
|
|
51
63
|
## Producer (bajo nivel)
|
|
52
64
|
|
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
|
|
@@ -287,6 +287,108 @@ RSpec.describe BugBunny::Client, 'session pooling' do
|
|
|
287
287
|
end
|
|
288
288
|
end
|
|
289
289
|
|
|
290
|
+
describe 'AMQP metadata kwargs (REQUEST_ATTRS extension)' do
|
|
291
|
+
let(:fake_exchange) { double('exchange', publish: nil) }
|
|
292
|
+
|
|
293
|
+
def stub_producer_capture
|
|
294
|
+
captured = nil
|
|
295
|
+
allow_any_instance_of(BugBunny::Producer).to receive(:fire) do |_prod, req|
|
|
296
|
+
captured = req
|
|
297
|
+
{ 'status' => 202, 'body' => nil }
|
|
298
|
+
end
|
|
299
|
+
allow_any_instance_of(BugBunny::Producer).to receive(:confirmed) do |_prod, req|
|
|
300
|
+
captured = req
|
|
301
|
+
{ 'status' => 202, 'body' => nil }
|
|
302
|
+
end
|
|
303
|
+
[-> { captured }]
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
it 'persistent: true se propaga al Request via kwarg' do
|
|
307
|
+
client = described_class.new(pool: fake_pool(fake_conn))
|
|
308
|
+
get_req, = stub_producer_capture
|
|
309
|
+
|
|
310
|
+
client.publish('evt', exchange: 'x', exchange_type: 'direct', persistent: true)
|
|
311
|
+
|
|
312
|
+
expect(get_req.call.persistent).to be(true)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
it 'persistent: false explícito se honra (no se filtra como falsy)' do
|
|
316
|
+
client = described_class.new(pool: fake_pool(fake_conn))
|
|
317
|
+
get_req, = stub_producer_capture
|
|
318
|
+
|
|
319
|
+
client.publish('evt', exchange: 'x', exchange_type: 'direct', persistent: false)
|
|
320
|
+
|
|
321
|
+
expect(get_req.call.persistent).to be(false)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it 'correlation_id: se propaga al Request via kwarg' do
|
|
325
|
+
client = described_class.new(pool: fake_pool(fake_conn))
|
|
326
|
+
get_req, = stub_producer_capture
|
|
327
|
+
|
|
328
|
+
client.publish('evt', exchange: 'x', exchange_type: 'direct', correlation_id: 'cid-123')
|
|
329
|
+
|
|
330
|
+
expect(get_req.call.correlation_id).to eq('cid-123')
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
it 'priority: se propaga al Request via kwarg' do
|
|
334
|
+
client = described_class.new(pool: fake_pool(fake_conn))
|
|
335
|
+
get_req, = stub_producer_capture
|
|
336
|
+
|
|
337
|
+
client.publish('evt', exchange: 'x', exchange_type: 'direct', priority: 9)
|
|
338
|
+
|
|
339
|
+
expect(get_req.call.priority).to eq(9)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
it 'app_id, content_type, content_encoding, expiration se propagan via kwargs' do
|
|
343
|
+
client = described_class.new(pool: fake_pool(fake_conn))
|
|
344
|
+
get_req, = stub_producer_capture
|
|
345
|
+
|
|
346
|
+
client.publish('evt',
|
|
347
|
+
exchange: 'x', exchange_type: 'direct',
|
|
348
|
+
app_id: 'radius_manager',
|
|
349
|
+
content_type: 'application/x-protobuf',
|
|
350
|
+
content_encoding: 'gzip',
|
|
351
|
+
expiration: '60000')
|
|
352
|
+
|
|
353
|
+
req = get_req.call
|
|
354
|
+
expect(req.app_id).to eq('radius_manager')
|
|
355
|
+
expect(req.content_type).to eq('application/x-protobuf')
|
|
356
|
+
expect(req.content_encoding).to eq('gzip')
|
|
357
|
+
expect(req.expiration).to eq('60000')
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
it 'block API sigue funcionando para atributos no expuestos como kwarg (ej: timestamp, type)' do
|
|
361
|
+
client = described_class.new(pool: fake_pool(fake_conn))
|
|
362
|
+
get_req, = stub_producer_capture
|
|
363
|
+
|
|
364
|
+
ts = Time.now.to_i - 100
|
|
365
|
+
client.publish('evt', exchange: 'x', exchange_type: 'direct') do |req|
|
|
366
|
+
req.timestamp = ts
|
|
367
|
+
req.type = 'custom.type'
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
req = get_req.call
|
|
371
|
+
expect(req.timestamp).to eq(ts)
|
|
372
|
+
expect(req.type).to eq('custom.type')
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
it 'kwargs y block API coexisten — block corre después y puede sobrescribir' do
|
|
376
|
+
client = described_class.new(pool: fake_pool(fake_conn))
|
|
377
|
+
get_req, = stub_producer_capture
|
|
378
|
+
|
|
379
|
+
client.publish('evt',
|
|
380
|
+
exchange: 'x', exchange_type: 'direct',
|
|
381
|
+
persistent: false, priority: 1) do |req|
|
|
382
|
+
req.persistent = true
|
|
383
|
+
req.priority = 9
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
req = get_req.call
|
|
387
|
+
expect(req.persistent).to be(true)
|
|
388
|
+
expect(req.priority).to eq(9)
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
290
392
|
describe 'Session no se cierra entre requests' do
|
|
291
393
|
it 'no invoca close en la Session al terminar el request' do
|
|
292
394
|
conn = fake_conn
|
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.17.1
|
|
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-19 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
|
|
@@ -304,7 +306,7 @@ metadata:
|
|
|
304
306
|
homepage_uri: https://github.com/gedera/bug_bunny
|
|
305
307
|
source_code_uri: https://github.com/gedera/bug_bunny
|
|
306
308
|
changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
|
|
307
|
-
documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.
|
|
309
|
+
documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.17.1/skill
|
|
308
310
|
post_install_message:
|
|
309
311
|
rdoc_options: []
|
|
310
312
|
require_paths:
|