bug_bunny 5.2.0 → 5.3.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/.rspec +2 -0
- data/AGENTS.md +3 -2
- data/CHANGELOG.md +17 -0
- data/CLAUDE.md +1 -1
- data/README.md +42 -3
- data/Rakefile +9 -1
- data/docs/behavior/behavior.md +25 -3
- data/docs/config/configuracion.md +51 -43
- data/docs/errors/errors.md +4 -2
- data/docs/release/release.md +12 -9
- data/docs/test/testing.md +15 -6
- data/lib/bug_bunny/configuration.rb +11 -0
- data/lib/bug_bunny/consumer_middleware/rails_executor.rb +33 -0
- data/lib/bug_bunny/consumer_middleware.rb +13 -0
- data/lib/bug_bunny/railtie.rb +18 -2
- data/lib/bug_bunny/version.rb +1 -1
- data/skill/SKILL.md +12 -6
- data/skill/references/consumer.md +9 -0
- data/skill/references/testing.md +4 -1
- data/spec/rails/app.rb +70 -0
- data/spec/rails/consumer_rails_executor_spec.rb +165 -0
- data/spec/rails/rails_helper.rb +6 -0
- data/spec/unit/configuration_spec.rb +6 -0
- data/spec/unit/consumer_middleware_spec.rb +13 -0
- data/spec/unit/consumer_rails_executor_outside_rails_spec.rb +19 -0
- metadata +51 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a28c7b99a99dc128e597b0dd3c6d128ab0abcf634410603f1dbeee78321ff33
|
|
4
|
+
data.tar.gz: 176ef53af6d5b7de0d6d10426aad13dbce548c2e894fd06db43550146791c216
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed8f25adc078770d9009b145cae463a772d0bae7234dcbae31e398f65896c7a055f230952c626becab97f235cbf23c66cd590938189ef4176c4bcecd94db092e
|
|
7
|
+
data.tar.gz: 0e5a151b21aee8a2647c41a8407df889d36582eb45026c4ca8a6a1d04b3bd0109a0e371e1a1f00d7625247e8be9ed6baf9b14c8a7ef28c9b50b5f6a0262207f2
|
data/.rspec
ADDED
data/AGENTS.md
CHANGED
|
@@ -12,7 +12,7 @@ proyecto y convenciones de equipo, ver `CLAUDE.md`. Para la entrada humana, ver
|
|
|
12
12
|
|
|
13
13
|
| capa | artefacto | estado | qué responde |
|
|
14
14
|
|---|---|---|---|
|
|
15
|
-
| Comportamiento | `docs/behavior/behavior.md` | completo (
|
|
15
|
+
| Comportamiento | `docs/behavior/behavior.md` | completo (8 flujos) | secuencias de publish/RPC/consume/confirms, contrato de error-wrapping |
|
|
16
16
|
| Glosario | `docs/glossary/glossary.md` | parcial (acreta por PR) | término de negocio → binding físico en `lib/` |
|
|
17
17
|
| Errores | `docs/errors/errors.md` | completo (§a/b/d estructura + §c política inferida) | jerarquía de excepciones públicas, mapeo `status→excepción`, shape del payload, política retry |
|
|
18
18
|
| Configuración | `docs/config/configuracion.md` | completo (estructura + enrich §f/g/h) | opciones de `Configuration`, defaults, failure-mode/threading, inyecciones del `Railtie`, ENV sugeridas |
|
|
@@ -38,6 +38,7 @@ a YARD/specs/CHANGELOG. Pendiente de **verificación humana** (inferencias):
|
|
|
38
38
|
|
|
39
39
|
- Ruby: `.ruby-version` · gemas: `Gemfile.lock` · manager: chruby + Bundler.
|
|
40
40
|
- Antes de commitear: `bundle exec rubocop -a` (base `rubocop-rails-omakase`),
|
|
41
|
-
`bundle exec
|
|
41
|
+
`bundle exec rake` (las dos suites: `spec/` y `spec/rails/`, cada una en su proceso; `bundle exec rspec` solo
|
|
42
|
+
corre `spec/`, sin Rails), YARD incremental (`bundle exec yard stats --list-undoc`).
|
|
42
43
|
- Releases: `/gem-release` (el GitHub Action publica a RubyGems al pushear tag `v*`).
|
|
43
44
|
- Doc por-PR: si tocás una capa, mové su artefacto en el mismo PR (régimen RFC-001).
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [5.3.0] - 2026-10-16
|
|
4
|
+
|
|
5
|
+
### Nuevas funcionalidades
|
|
6
|
+
- **Dentro de Rails, cada mensaje del Consumer corre dentro del executor de Rails (#70):** el railtie registra `BugBunny::ConsumerMiddleware::RailsExecutor`, que envuelve cada mensaje con `Rails.application.executor.wrap`, como Sidekiq con cada job. El Consumer procesa en el hilo de trabajo de Bunny, que Rails no conoce, así que hasta ahora lo que Rails guarda por hilo pasaba de un mensaje al siguiente: los `CurrentAttributes` (tags de Sentry, contexto de traza, métricas por ejecución) y la conexión de ActiveRecord (el hilo la retenía en vez de devolverla al pool). Fuera de Rails no se registra nada y la gema se comporta como antes. (#70, #71) — @Pslp
|
|
7
|
+
- **`config.consumer_rails_executor`** (default `true`): con `false`, el railtie no registra el middleware. Se lee al terminar de inicializar la app, así que se apaga desde `config/initializers`. Si la app no llamó a `BugBunny.configure` antes de ese momento, el railtie usa la configuración por default en vez de caerse al bootear. — @Pslp
|
|
8
|
+
- **`ConsumerMiddleware::Stack#prepend`:** registra un middleware al principio de la cadena. El del executor va primero así, para envolver también a los middlewares que cargan estado por hilo (el de tracing de ExisRay se registra en su propio `after_initialize`, en un orden que no controlamos). — @Pslp
|
|
9
|
+
|
|
10
|
+
### ⚠️ Cambio de conducta (dentro de Rails)
|
|
11
|
+
- **Rails ≥ 7.0 — un error que escapa del procesamiento de un mensaje ahora además se reporta a `Rails.error`** con `handled: false`, porque es lo que hace `executor.wrap` antes de relanzar. El camino del mensaje no cambia: se rechaza sin requeue, como desde 5.2.0. Los errores dentro de las acciones del controller los sigue rescatando el Consumer y no se ven afectados. **A mirar si consumís:** si tenés un subscriber de `Rails.error` (el de Sentry, por ejemplo), esos errores ahora aparecen ahí. Para volver a la conducta anterior: `config.consumer_rails_executor = false`. — @Pslp
|
|
12
|
+
- **La caché de queries de ActiveRecord queda prendida durante cada mensaje** y se limpia al terminarlo: es lo que hace el executor (`ActiveRecord::QueryCache::ExecutorHooks`). En el hilo de Bunny estaba apagada. **A mirar si consumís:** un handler que relee la misma fila dentro de un mensaje esperando que otro proceso la cambie (un poll) ahora recibe el valor cacheado; usá `ActiveRecord::Base.uncached { … }` ahí. Se vuelve a la conducta anterior con `config.consumer_rails_executor = false`. — @Pslp
|
|
13
|
+
|
|
14
|
+
### Tests
|
|
15
|
+
- `spec/rails/`: una app Rails mínima (ActiveRecord sobre SQLite en memoria) prueba el railtie con Rails de verdad. Corre en su propio proceso (`rake spec:rails`), para que el resto de la suite siga probando la gema fuera de Rails; `rake` sin argumentos corre las dos, y `rspec` pelado corre `spec/` (un `.rspec` excluye `spec/rails/` salvo que se lo pida: `rspec spec/rails` la corre). Suma `railties`, `activerecord` y `sqlite3` como dependencias de desarrollo. — @Pslp
|
|
16
|
+
|
|
17
|
+
### Documentación
|
|
18
|
+
- README, `skill/` y los artefactos de `docs/` (configuración, comportamiento, tests, errores) describen el executor, la opción y los dos cambios de conducta; las metas que tocó #70 se re-anclaron a `67cca83`. — @Pslp
|
|
19
|
+
|
|
3
20
|
## [5.2.0] - 2026-09-29
|
|
4
21
|
|
|
5
22
|
### Nuevas funcionalidades
|
data/CLAUDE.md
CHANGED
|
@@ -61,7 +61,7 @@ BugBunny es una gema Ruby que implementa una capa de enrutamiento RESTful sobre
|
|
|
61
61
|
|
|
62
62
|
### Testing
|
|
63
63
|
- Framework: RSpec
|
|
64
|
-
- Correr: `bundle exec rspec`
|
|
64
|
+
- Correr: `bundle exec rake` (las dos suites: `spec/` y `spec/rails/`, cada una en su proceso; `bundle exec rspec` solo corre `spec/`, sin Rails)
|
|
65
65
|
- Todo código nuevo debe tener tests.
|
|
66
66
|
|
|
67
67
|
### Releases o Nuevas versiones
|
data/README.md
CHANGED
|
@@ -173,6 +173,10 @@ BugBunny.configure do |config|
|
|
|
173
173
|
config.drain_idle_timeout = 5
|
|
174
174
|
config.drain_poll_interval = 0.1
|
|
175
175
|
|
|
176
|
+
# Rails only — wrap every consumed message in Rails.application.executor (default: true).
|
|
177
|
+
# See "Rails executor around each message" below. Set it from config/initializers.
|
|
178
|
+
config.consumer_rails_executor = true
|
|
179
|
+
|
|
176
180
|
# Publisher Confirms — fail-loud defaults (both flags default to true).
|
|
177
181
|
# Set to false to restore legacy log-only behavior.
|
|
178
182
|
config.nack_raise = true # broker NACK → raise BugBunny::PublishNacked
|
|
@@ -362,6 +366,41 @@ end
|
|
|
362
366
|
BugBunny.consumer_middlewares.use TracingMiddleware
|
|
363
367
|
```
|
|
364
368
|
|
|
369
|
+
`use` appends to the chain; `prepend` puts a middleware first, wrapping every other one.
|
|
370
|
+
|
|
371
|
+
### Rails executor around each message
|
|
372
|
+
|
|
373
|
+
Inside Rails, the railtie registers `BugBunny::ConsumerMiddleware::RailsExecutor` **first** in the chain. It wraps every
|
|
374
|
+
message in `Rails.application.executor.wrap`, the same way Sidekiq wraps each job. The consumer processes messages on
|
|
375
|
+
Bunny's worker thread, which Rails does not know about; without the executor, what Rails keeps per thread survives from
|
|
376
|
+
one message to the next:
|
|
377
|
+
|
|
378
|
+
- `ActiveSupport::CurrentAttributes` (and whatever lives in them: Sentry tags, trace context, per-run metrics);
|
|
379
|
+
- the ActiveRecord connection, which the thread keeps until it dies instead of returning it to the pool.
|
|
380
|
+
|
|
381
|
+
It goes first (`prepend`, not `use`) so that it also wraps the middlewares that load per-thread state, like ExisRay's
|
|
382
|
+
tracing middleware. Outside Rails nothing is registered and the consumer behaves as before.
|
|
383
|
+
|
|
384
|
+
#### ⚠️ Behavior change
|
|
385
|
+
|
|
386
|
+
**Errors that escape a message (Rails ≥ 7.0).**
|
|
387
|
+
An error that escapes the processing of a message (from a middleware, or from `handle_fatal_error`) still ends the same
|
|
388
|
+
way: the delivery is rejected without requeue. **It is now also reported to `Rails.error`** with `handled: false`, because that is what `executor.wrap` does before re-raising. If you have a `Rails.error` subscriber
|
|
389
|
+
(e.g. Sentry's), that error shows up there. Errors inside your controller actions are rescued by the consumer as
|
|
390
|
+
before and are not affected.
|
|
391
|
+
|
|
392
|
+
**The ActiveRecord query cache is on during each message**, and cleared when it ends: that is what the executor does
|
|
393
|
+
(`ActiveRecord::QueryCache::ExecutorHooks`). On Bunny's thread it used to be off. A handler that re-reads the same row
|
|
394
|
+
within one message, waiting for another process to change it (a poll), now gets the cached value; wrap that read in
|
|
395
|
+
`ActiveRecord::Base.uncached { … }`.
|
|
396
|
+
|
|
397
|
+
To turn it off (for example, if your consumer already wraps each message itself):
|
|
398
|
+
|
|
399
|
+
```ruby
|
|
400
|
+
# config/initializers/bug_bunny.rb
|
|
401
|
+
BugBunny.configure { |config| config.consumer_rails_executor = false }
|
|
402
|
+
```
|
|
403
|
+
|
|
365
404
|
---
|
|
366
405
|
|
|
367
406
|
## Observability
|
|
@@ -459,16 +498,16 @@ end
|
|
|
459
498
|
|
|
460
499
|
Artefactos de detalle (modelo `dev-*`, RFC-001). El README indexa; no duplica.
|
|
461
500
|
|
|
462
|
-
Anclado a `v5.
|
|
501
|
+
Anclado a `v5.3.0`.
|
|
463
502
|
|
|
464
503
|
| Capa | Artefacto | Estado |
|
|
465
504
|
|---|---|---|
|
|
466
505
|
| Glosario | [docs/glossary/glossary.md](docs/glossary/glossary.md) | parcial, acreta por PR |
|
|
467
|
-
| Comportamiento | [docs/behavior/behavior.md](docs/behavior/behavior.md) | completa —
|
|
506
|
+
| Comportamiento | [docs/behavior/behavior.md](docs/behavior/behavior.md) | completa — 8 flujos (backfill on-demand) |
|
|
468
507
|
| Configuración | [docs/config/configuracion.md](docs/config/configuracion.md) | §a-§e/§i estructura + §f/§g/§h enrich — completa |
|
|
469
508
|
| Dependencias consumidas | [docs/consumed/rabbitmq.md](docs/consumed/rabbitmq.md) | §a/§b/§d estructura + §c/§e enrich |
|
|
470
509
|
| Errores | [docs/errors/errors.md](docs/errors/errors.md) | §a/§b/§d completas; §c política inferida (verificación humana pendiente) |
|
|
471
|
-
| Test | [docs/test/testing.md](docs/test/testing.md) | §a-§h —
|
|
510
|
+
| Test | [docs/test/testing.md](docs/test/testing.md) | §a-§h — 18 unit / 8 integration / 1 rails (`spec/rails/`, en su propio proceso) |
|
|
472
511
|
| Release | [docs/release/release.md](docs/release/release.md) | completa (régimen gema: build→publish; §e/f/g n/a) |
|
|
473
512
|
| Datos | — | n/a — gema sin DB (sin schema/models) |
|
|
474
513
|
| Eventos | — | n/a — la gema **es** el transporte; no declara catálogo de eventos de dominio propio |
|
data/Rakefile
CHANGED
|
@@ -4,6 +4,7 @@ require 'rspec/core/rake_task'
|
|
|
4
4
|
|
|
5
5
|
RSpec::Core::RakeTask.new(:spec) do |t|
|
|
6
6
|
t.rspec_opts = '--require spec_helper'
|
|
7
|
+
t.exclude_pattern = 'spec/rails/**/*_spec.rb'
|
|
7
8
|
end
|
|
8
9
|
|
|
9
10
|
RSpec::Core::RakeTask.new('spec:unit') do |t|
|
|
@@ -16,6 +17,12 @@ RSpec::Core::RakeTask.new('spec:integration') do |t|
|
|
|
16
17
|
t.rspec_opts = '--require spec_helper'
|
|
17
18
|
end
|
|
18
19
|
|
|
20
|
+
# Los specs que necesitan Rails (el Railtie) bootean una app propia, y Rails se inicializa una vez por
|
|
21
|
+
# proceso: corren aparte, para que el resto de la suite siga probando la gema fuera de Rails.
|
|
22
|
+
RSpec::Core::RakeTask.new('spec:rails') do |t|
|
|
23
|
+
t.pattern = 'spec/rails/**/*_spec.rb'
|
|
24
|
+
end
|
|
25
|
+
|
|
19
26
|
# Mantiene la tarea :test apuntando a los tests de integración legacy de Minitest
|
|
20
27
|
Rake::TestTask.new(:test) do |t|
|
|
21
28
|
t.libs << 'test'
|
|
@@ -25,4 +32,5 @@ Rake::TestTask.new(:test) do |t|
|
|
|
25
32
|
t.warning = false
|
|
26
33
|
end
|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
# Las dos suites, cada una en su proceso: es lo que corren `quality-code` y `gem-release`.
|
|
36
|
+
task default: [:spec, 'spec:rails']
|
data/docs/behavior/behavior.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Comportamiento — bug_bunny
|
|
2
2
|
|
|
3
|
-
> meta: artefacto `comportamiento` · RFC-007 (cadencia incremental default / completo on-demand) · generado dev-enrich 1.3.0 (backfill on-demand) · anclado a `
|
|
3
|
+
> meta: artefacto `comportamiento` · RFC-007 (cadencia incremental default / completo on-demand) · generado dev-enrich 1.3.0 (backfill on-demand) · anclado a `67cca83` (squash de #71 en `main`; antes `35b075a`) · cobertura: completa (8 flujos) · verificado por humano 2026-05-18 (base) · 2026-05-26 (refresco scoped: contrato de error wrapping post-#49) — incremento 2026-09-28 (#64, flujo `drain` + re-mapeo de `file:line`): escrito por el agente, **sin verificación humana todavía** — incremento 2026-10-16 (#70, flujo «Executor de Rails alrededor de cada mensaje»; re-anclado a `67cca83` en el release 5.3.0): escrito por el agente, **sin verificación humana todavía**
|
|
4
4
|
|
|
5
5
|
## 1. Resumen
|
|
6
6
|
|
|
@@ -17,8 +17,9 @@ Flujos de ejecución de la gema. Generado en **modo completo on-demand** (RFC-00
|
|
|
17
17
|
| Consumer drain (drenar y salir) | **documentado** | `consumer.rb:158-176,271-290`, `drain_tracker.rb:28-47` |
|
|
18
18
|
| Error handling / RemoteError | **documentado** | `consumer.rb:473-482`, `remote_error.rb`, `raise_error.rb:32-65` |
|
|
19
19
|
| Client middleware stack (onion) | **documentado** | `middleware/stack.rb:43-47`, `base.rb:35-43` |
|
|
20
|
+
| Executor de Rails alrededor de cada mensaje | **documentado** | `railtie.rb:44-51`, `consumer_middleware/rails_executor.rb:28-30`, `consumer.rb:227-238` |
|
|
20
21
|
|
|
21
|
-
Cobertura completa a `
|
|
22
|
+
Cobertura completa a `67cca83`. Acreta incremental en cada PR que toque un flujo (default RFC-007). Ausencia futura ≠ inexistencia.
|
|
22
23
|
|
|
23
24
|
## 2. Cuerpo
|
|
24
25
|
|
|
@@ -110,7 +111,7 @@ sequenceDiagram
|
|
|
110
111
|
C->>H: start_health_check (touch file)
|
|
111
112
|
C->>BR: q.subscribe(manual_ack, block:true) — LOOP
|
|
112
113
|
loop por mensaje
|
|
113
|
-
BR->>C: deliver → consumer_middlewares → process_message
|
|
114
|
+
BR->>C: deliver → consumer_middlewares (en Rails: RailsExecutor primero) → process_message
|
|
114
115
|
end
|
|
115
116
|
H-->>S: cada interval: queue_declare(passive:true)
|
|
116
117
|
H-->>S: falla → cierra session (fuerza reconnect)
|
|
@@ -146,6 +147,27 @@ sequenceDiagram
|
|
|
146
147
|
```
|
|
147
148
|
Contexto: `consumer.rb:158-176` (`drain`), `consumer.rb:271-290` (`consume_until_idle`), `drain_tracker.rb:28-47`. **Mensajes que llegan mientras drena:** entran en esta vuelta si llegan antes de que venza la ventana; los posteriores quedan para la próxima corrida. Una entrega ya recibida al cancelar se procesa antes de volver (Bunny drena su work pool al cancelar — leído de la fuente de Bunny 2.24.0, no medido); si igual no se ack-eara, **vuelve a la cola** (at-least-once). El conteo devuelto incluye los rechazados. **Una entrega que falla fuera del rescue de `process_message`** (un middleware, `handle_fatal_error`) se rechaza sin requeue en `handle_delivery` y se loguea `consumer.delivery_failed`; antes quedaba sin ack, trababa el prefetch y `drain` volvía "con éxito" con la cola llena (medido en el review de #65). **Con un flujo sostenido no retorna:** si los mensajes llegan más seguido que `drain_idle_timeout` la ventana no vence — la duración la decide el productor; se acota desde afuera (timeout del job). **La conexión es del llamador:** `drain` cierra el canal, no la conexión. **Distinto de `subscribe(block: false)`:** ese modo retorna al instante y el `ensure shutdown` cierra el canal, así que no consume nada (medido en #64).
|
|
148
149
|
|
|
150
|
+
### Flujo: Executor de Rails alrededor de cada mensaje
|
|
151
|
+
Dentro de Rails, cada entrega corre dentro de `Rails.application.executor.wrap`, como un job de Sidekiq. El Consumer procesa en el hilo de trabajo de Bunny, que Rails no conoce: sin esto, el estado por hilo pasa de un mensaje al siguiente (#70).
|
|
152
|
+
|
|
153
|
+
```mermaid
|
|
154
|
+
sequenceDiagram
|
|
155
|
+
participant BR as RabbitMQ
|
|
156
|
+
participant C as Consumer#handle_delivery
|
|
157
|
+
participant X as RailsExecutor
|
|
158
|
+
participant M as otros middlewares (p. ej. tracing de ExisRay)
|
|
159
|
+
participant P as process_message
|
|
160
|
+
BR->>C: deliver (hilo de Bunny)
|
|
161
|
+
C->>X: consumer_middlewares.call
|
|
162
|
+
X->>X: executor.wrap — al entrar: reset de CurrentAttributes · prende la caché de queries de AR
|
|
163
|
+
X->>M: @app.call
|
|
164
|
+
M->>P: core: rutea, llama al controller, ack/reject, reply RPC
|
|
165
|
+
P-->>X: vuelve
|
|
166
|
+
X->>X: al salir: reset de CurrentAttributes · la conexión de AR vuelve al pool · se apaga y limpia la caché de queries
|
|
167
|
+
Note over C: un error que escapa: wrap lo relanza (en Rails ≥ 7.0, antes lo reporta a Rails.error) → rescue de handle_delivery → reject sin requeue
|
|
168
|
+
```
|
|
169
|
+
Contexto: `railtie.rb:44-51` (registro en `after_initialize`, sólo si `config.consumer_rails_executor`), `consumer_middleware.rb:72-75` (`Stack#prepend`), `consumer_middleware/rails_executor.rb:28-30` (`wrap`), `consumer.rb:227-238` (`handle_delivery`: la cadena y su `rescue`). **Va primero** (`prepend`) porque el reset al entrar borraría el estado que un middleware anterior ya cargó: el de tracing de ExisRay carga `ExisRay::Tracer` (un `CurrentAttributes`) y se registra en su propio `after_initialize`, en un orden que no controlamos. El ack/reject queda adentro del wrap (vive en `process_message`); el `rescue` de `handle_delivery`, afuera. Si el hilo ya está dentro del executor, `wrap` no abre otro. **Fuera de Rails** el Railtie no se carga y la cadena no cambia. Probado con Rails de verdad en `spec/rails/consumer_rails_executor_spec.rb` (Rails 8.1.4, la versión que corre la suite). **Cambio de conducta:** la caché de queries, que en el hilo de Bunny estaba apagada, queda prendida durante cada mensaje (`ActiveRecord::QueryCache::ExecutorHooks`).
|
|
170
|
+
|
|
149
171
|
### Flujo: Error handling / RemoteError
|
|
150
172
|
Excepción no manejada en controller → serializada (clase/mensaje/backtrace[0..25]) → reply 500 → reconstruida client-side por `Middleware::RaiseError`.
|
|
151
173
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Configuración — bug_bunny
|
|
2
2
|
|
|
3
3
|
> meta: artefacto configuración · RFC-012 · generado `arch-structure` (inventario
|
|
4
|
-
> §a-§e/§i) + `arch-enrich` (§f/§g/§h/§j) · anclado a `
|
|
4
|
+
> §a-§e/§i) + `arch-enrich` (§f/§g/§h/§j) · anclado a `67cca83` (squash de #71 en `main`),
|
|
5
5
|
> `lib/bug_bunny/configuration.rb`, `lib/bug_bunny.rb`, `lib/bug_bunny/railtie.rb`,
|
|
6
6
|
> `lib/bug_bunny/consumer.rb`,
|
|
7
7
|
> `lib/generators/bug_bunny/install/templates/initializer.rb` · fecha 2026-06-30
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
> §j n/a. Incremento 2026-09-28 (#64): `drain_idle_timeout`/`drain_poll_interval`,
|
|
10
10
|
> anclados a `configuration.rb:37-38,99-105,273-275` y `consumer.rb:271-290`; todas las
|
|
11
11
|
> citas `file:line` re-mapeadas al árbol de `35b075a` (el refactor de #64 las corrió).
|
|
12
|
+
> Incremento 2026-10-16 (#70): `consumer_rails_executor` (`configuration.rb:196,277`,
|
|
13
|
+
> `railtie.rb:44-51`); las citas de `configuration.rb` re-mapeadas por contenido (el atributo
|
|
14
|
+
> nuevo corrió +10/+11 todo lo que viene después) y las de `railtie.rb` (§i). Re-anclado a
|
|
15
|
+
> `67cca83` en el release 5.3.0: entre `35b075a` y `67cca83` sólo cambiaron los archivos de #70
|
|
16
|
+
> (re-mapeados) y `version.rb` (no citado). Escrito por el agente, sin verificación humana todavía.
|
|
12
17
|
|
|
13
18
|
## 1. Resumen
|
|
14
19
|
|
|
@@ -22,12 +27,12 @@ en `lib/`**; el `ENV.fetch` vive en el **template** que sugiere al consumidor
|
|
|
22
27
|
|
|
23
28
|
### a. Hecho verificable
|
|
24
29
|
|
|
25
|
-
- **Total opciones (`Configuration`):**
|
|
26
|
-
2026-
|
|
30
|
+
- **Total opciones (`Configuration`):** 33 (32 `attr_accessor` + 1 `attr_reader`; medido
|
|
31
|
+
2026-10-16, con `consumer_rails_executor` de #70 — el `29` anterior ya estaba desfasado del código antes de #64).
|
|
27
32
|
- **Validadas requeridas (`VALIDATIONS`, `configuration.rb:25-39`):** 5 (`host`,
|
|
28
33
|
`port`, `username`, `password`, `vhost` — `required: true`; igual tienen
|
|
29
34
|
default, `validate!` exige no-vacío).
|
|
30
|
-
- **Con default:**
|
|
35
|
+
- **Con default:** 33 (todas; defaults en `initialize`/`init_callback_defaults`/`init_drain_defaults`).
|
|
31
36
|
- **Con rango validado:** 9 (`port`, `heartbeat`, `connection_timeout`,
|
|
32
37
|
`read_timeout`, `write_timeout`, `rpc_timeout`, `channel_prefetch`,
|
|
33
38
|
`drain_idle_timeout`, `drain_poll_interval`).
|
|
@@ -41,38 +46,39 @@ Origen `code-default` salvo nota. Consumidor = `configuration.rb` (default en
|
|
|
41
46
|
|
|
42
47
|
| nombre | tipo | requerida | default | origen | consumidor | secret? |
|
|
43
48
|
|---|---|---|---|---|---|---|
|
|
44
|
-
| `host` | String | sí (validate!) | `'127.0.0.1'` | code-default | `configuration.rb:
|
|
45
|
-
| `port` | Integer | sí (validate!, 1..65535) | `5672` | code-default | `:
|
|
46
|
-
| `username` | String | sí (validate!) | `'guest'` | code-default | `:
|
|
47
|
-
| `password` | String | sí (validate!) | `'guest'` | code-default | `:
|
|
48
|
-
| `vhost` | String | sí (validate!) | `'/'` | code-default | `:
|
|
49
|
-
| `logger` | Logger | no | `Logger.new($stdout)` INFO | code-default | `:
|
|
50
|
-
| `bunny_logger` | Logger | no | `Logger.new($stdout)` WARN | code-default | `:
|
|
51
|
-
| `automatically_recover` | Boolean | no | `true` | code-default | `:
|
|
52
|
-
| `network_recovery_interval` | Integer | no | `5` | code-default | `:
|
|
53
|
-
| `max_reconnect_attempts` | Integer/nil | no | `nil` (reintenta ∞) | code-default | `:
|
|
54
|
-
| `max_reconnect_interval` | Integer | no | `60` | code-default | `:
|
|
55
|
-
| `connection_timeout` | Integer | no (1..300) | `10` | code-default | `:
|
|
56
|
-
| `read_timeout` | Integer | no (1..300) | `30` | code-default | `:
|
|
57
|
-
| `write_timeout` | Integer | no (1..300) | `30` | code-default | `:
|
|
58
|
-
| `heartbeat` | Integer | no (0..3600) | `15` | code-default | `:
|
|
59
|
-
| `continuation_timeout` | Integer (ms) | no | `15000` | code-default | `:
|
|
60
|
-
| `channel_prefetch` | Integer | no (1..10000) | `1` | code-default | `:
|
|
61
|
-
| `rpc_timeout` | Integer | no (1..3600) | `10` | code-default | `:
|
|
62
|
-
| `health_check_interval` | Integer | no | `60` | code-default | `:
|
|
63
|
-
| `health_check_file` | String/nil | no | `nil` (desactivado) | code-default | `:
|
|
64
|
-
| `drain_idle_timeout` | Integer (s) | no (1..3600) | `5` | code-default | `:
|
|
65
|
-
| `drain_poll_interval` | Numeric (s) | no (0.01..10) | `0.1` | code-default | `:
|
|
66
|
-
| `controller_namespace` | String | no | `'BugBunny::Controllers'` | code-default | `:
|
|
67
|
-
| `log_tags` | Array | no | `[:uuid]` | code-default | `:
|
|
68
|
-
| `exchange_options` | Hash | no | `{}` | code-default | `:
|
|
69
|
-
| `queue_options` | Hash | no | `{}` | code-default | `:
|
|
70
|
-
| `consumer_middlewares` | Stack (attr_reader) | no | `Stack.new` | code-default | `:
|
|
71
|
-
| `rpc_reply_headers` | Proc/nil | no | `nil` | code-default | `:
|
|
72
|
-
| `on_rpc_reply` | Proc/nil | no | `nil` | code-default | `:
|
|
73
|
-
| `on_return` | Proc/nil | no | `nil` | code-default | `:
|
|
74
|
-
| `nack_raise` | Boolean | no | `true` | code-default | `:
|
|
75
|
-
| `return_raise` | Boolean | no | `true` | code-default | `:
|
|
49
|
+
| `host` | String | sí (validate!) | `'127.0.0.1'` | code-default | `configuration.rb:202` | no |
|
|
50
|
+
| `port` | Integer | sí (validate!, 1..65535) | `5672` | code-default | `:203` | no |
|
|
51
|
+
| `username` | String | sí (validate!) | `'guest'` | code-default | `:204` | no |
|
|
52
|
+
| `password` | String | sí (validate!) | `'guest'` | code-default | `:205` | **sí** |
|
|
53
|
+
| `vhost` | String | sí (validate!) | `'/'` | code-default | `:206` | no |
|
|
54
|
+
| `logger` | Logger | no | `Logger.new($stdout)` INFO | code-default | `:208` | no |
|
|
55
|
+
| `bunny_logger` | Logger | no | `Logger.new($stdout)` WARN | code-default | `:211` | no |
|
|
56
|
+
| `automatically_recover` | Boolean | no | `true` | code-default | `:213` | no |
|
|
57
|
+
| `network_recovery_interval` | Integer | no | `5` | code-default | `:214` | no |
|
|
58
|
+
| `max_reconnect_attempts` | Integer/nil | no | `nil` (reintenta ∞) | code-default | `:215` | no |
|
|
59
|
+
| `max_reconnect_interval` | Integer | no | `60` | code-default | `:216` | no |
|
|
60
|
+
| `connection_timeout` | Integer | no (1..300) | `10` | code-default | `:217` | no |
|
|
61
|
+
| `read_timeout` | Integer | no (1..300) | `30` | code-default | `:218` | no |
|
|
62
|
+
| `write_timeout` | Integer | no (1..300) | `30` | code-default | `:219` | no |
|
|
63
|
+
| `heartbeat` | Integer | no (0..3600) | `15` | code-default | `:220` | no |
|
|
64
|
+
| `continuation_timeout` | Integer (ms) | no | `15000` | code-default | `:221` | no |
|
|
65
|
+
| `channel_prefetch` | Integer | no (1..10000) | `1` | code-default | `:222` | no |
|
|
66
|
+
| `rpc_timeout` | Integer | no (1..3600) | `10` | code-default | `:223` | no |
|
|
67
|
+
| `health_check_interval` | Integer | no | `60` | code-default | `:224` | no |
|
|
68
|
+
| `health_check_file` | String/nil | no | `nil` (desactivado) | code-default | `:227` | no |
|
|
69
|
+
| `drain_idle_timeout` | Integer (s) | no (1..3600) | `5` | code-default | `:285` | no |
|
|
70
|
+
| `drain_poll_interval` | Numeric (s) | no (0.01..10) | `0.1` | code-default | `:286` | no |
|
|
71
|
+
| `controller_namespace` | String | no | `'BugBunny::Controllers'` | code-default | `:230` | no |
|
|
72
|
+
| `log_tags` | Array | no | `[:uuid]` | code-default | `:232` | no |
|
|
73
|
+
| `exchange_options` | Hash | no | `{}` | code-default | `:235` | no |
|
|
74
|
+
| `queue_options` | Hash | no | `{}` | code-default | `:236` | no |
|
|
75
|
+
| `consumer_middlewares` | Stack (attr_reader) | no | `Stack.new` | code-default | `:238` | no |
|
|
76
|
+
| `rpc_reply_headers` | Proc/nil | no | `nil` | code-default | `:272` | no |
|
|
77
|
+
| `on_rpc_reply` | Proc/nil | no | `nil` | code-default | `:273` | no |
|
|
78
|
+
| `on_return` | Proc/nil | no | `nil` | code-default | `:274` | no |
|
|
79
|
+
| `nack_raise` | Boolean | no | `true` | code-default | `:275` | no |
|
|
80
|
+
| `return_raise` | Boolean | no | `true` | code-default | `:276` | no |
|
|
81
|
+
| `consumer_rails_executor` | Boolean | no | `true` | code-default | `:277` | no |
|
|
76
82
|
|
|
77
83
|
> **Override por request:** `nack_raise` y `return_raise` se sobreescriben por
|
|
78
84
|
> llamada con `nack_raise:` / `return_raise:` en `Client#publish` (scope-override
|
|
@@ -93,7 +99,7 @@ El install template (`initializer.rb`) **sugiere al consumidor** wirear 4 ENV
|
|
|
93
99
|
### d. Derivaciones simples
|
|
94
100
|
|
|
95
101
|
- `url` ← `"amqp://#{username}:#{password}@#{host}:#{port}/#{vhost}"`
|
|
96
|
-
(`configuration.rb:
|
|
102
|
+
(`configuration.rb:246`).
|
|
97
103
|
- `create_connection(**options)` ← `merge_connection_options(options)` sobre la
|
|
98
104
|
config global; las options explícitas pisan los defaults (`bug_bunny.rb:88`).
|
|
99
105
|
|
|
@@ -106,11 +112,12 @@ El install template (`initializer.rb`) **sugiere al consumidor** wirear 4 ENV
|
|
|
106
112
|
|
|
107
113
|
| inyección | qué hace | ancla |
|
|
108
114
|
|---|---|---|
|
|
109
|
-
| `initializer 'bug_bunny.add_autoload_paths'` | registra `app/rabbit` en el autoloader (Zeitwerk, `eager_load: true`) si existe | `:
|
|
110
|
-
| `config.after_initialize` → `ForkTracker.after_fork` | cierra la conexión heredada en cada fork (Rails 7.1+) | `:
|
|
111
|
-
| `config.after_initialize` → `Puma.events.on_worker_boot` | mismo cierre, hook legacy Puma <5 | `:
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
115
|
+
| `initializer 'bug_bunny.add_autoload_paths'` | registra `app/rabbit` en el autoloader (Zeitwerk, `eager_load: true`) si existe | `:16-19` |
|
|
116
|
+
| `config.after_initialize` → `ForkTracker.after_fork` | cierra la conexión heredada en cada fork (Rails 7.1+) | `:25-27` |
|
|
117
|
+
| `config.after_initialize` → `Puma.events.on_worker_boot` | mismo cierre, hook legacy Puma <5 | `:31-35` |
|
|
118
|
+
| `config.after_initialize` → `consumer_middlewares.prepend(RailsExecutor)` | si `consumer_rails_executor` (default `true`), pone `ConsumerMiddleware::RailsExecutor` **primero** en la cadena del Consumer: cada mensaje corre dentro de `Rails.application.executor.wrap` (#70) | `:44-51` |
|
|
119
|
+
| `rake_tasks` | carga `tasks/bug_bunny.rake` (`bug_bunny:sync`) | `:54-56` |
|
|
120
|
+
| `Spring.after_fork` | cierra conexión en preloader Spring | `:59-63` |
|
|
114
121
|
|
|
115
122
|
### f. Enriquecimiento semántico
|
|
116
123
|
|
|
@@ -128,6 +135,7 @@ del código.
|
|
|
128
135
|
| Drain (`drain_idle_timeout`/`drain_poll_interval`) | latencia | `drain_idle_timeout` muy bajo → un productor lento deja mensajes para la próxima corrida (no se pierden); muy alto → el job tarda más en terminar tras vaciar la cola | ninguno: sólo acota cuánto espera `Consumer#drain` (`consumer.rb:285`) | correr un consumidor como job que termina (#64) |
|
|
129
136
|
| Callbacks (`on_return`/`on_rpc_reply`/`rpc_reply_headers`) | extensibilidad | una excepción en `on_return` se captura pero **degrada visibilidad** (YARD `configuration.rb:157`) | corren en hilos sensibles (ver §h) | propagar trace-context / alertar unroutable |
|
|
130
137
|
| Confirms (`nack_raise`/`return_raise`) | integridad de entrega | `false` → NACK/return solo se logea, la llamada retorna `202` (modo legacy, posible pérdida silenciosa) | habilitan el raise de `PublishNacked`/`PublishUnroutable` | elegir entre fail-fast vs best-effort en publish confirmado |
|
|
138
|
+
| Executor de Rails (`consumer_rails_executor`) | aislamiento entre mensajes | `false` dentro de Rails → el estado por hilo (`CurrentAttributes`, conexión de ActiveRecord) pasa de un mensaje al siguiente, salvo que el consumidor envuelva cada mensaje por su cuenta; fuera de Rails no tiene efecto | `true` → el Railtie registra `ConsumerMiddleware::RailsExecutor` **primero** en la cadena (`railtie.rb:44-51`); en Rails ≥ 7.0, un error que escapa del mensaje además se reporta a `Rails.error` (`handled: false`), y la caché de queries de AR queda prendida durante cada mensaje | que cada mensaje empiece limpio, como un job de Sidekiq (#70) |
|
|
131
139
|
| Routing (`controller_namespace`) | seguridad | clase resuelta no subclase de `BugBunny::Controller` → el worker responde **403** + reject (guard anti-RCE, `consumer.rb:375-381`) | acota qué clases son enrutables | superficie de control de RCE |
|
|
132
140
|
| Logging (`logger`/`bunny_logger`/`log_tags`) | observabilidad | — | salida a `$stdout` por default | trazabilidad estructurada |
|
|
133
141
|
| Infra (`exchange_options`/`queue_options`) | infraestructura | options incompatibles con el broker → `PreconditionFailed` (vía `CommunicationError`) | defaults globales mergeados por recurso | declaración AMQP por default |
|
|
@@ -135,7 +143,7 @@ del código.
|
|
|
135
143
|
### g. Ramificadores intra-config
|
|
136
144
|
|
|
137
145
|
- `health_check_file = nil` (default) **desactiva** el touchfile aunque
|
|
138
|
-
`health_check_interval` siga corriendo (`configuration.rb:109-111,
|
|
146
|
+
`health_check_interval` siga corriendo (`configuration.rb:109-111,227`).
|
|
139
147
|
- `return_raise` es **inerte cuando `mandatory: false`** — sin `mandatory` el
|
|
140
148
|
broker nunca retorna, así que el flag no tiene efecto (`configuration.rb:181`).
|
|
141
149
|
- `nack_raise`/`return_raise` se sobreescriben **por request** (`Client#publish`),
|
data/docs/errors/errors.md
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
> `remote_error.rb`, `middleware/raise_error.rb`, `controller.rb`, `consumer.rb`,
|
|
6
6
|
> `observability.rb` · fecha 2026-07-31 · cobertura: §a/§b/§d completas
|
|
7
7
|
> (estructura); §c política completa (enrich, **inferida** de HTTP/AMQP —
|
|
8
|
-
> verificación humana pendiente).
|
|
8
|
+
> verificación humana pendiente). Incremento 2026-10-16 (#70): las citas de `configuration.rb`
|
|
9
|
+
> (`:293,300,308`, los `raise` de `ConfigurationError`) son del árbol de `67cca83`; el resto sigue
|
|
10
|
+
> anclado a `5697541`.
|
|
9
11
|
|
|
10
12
|
## 1. Resumen
|
|
11
13
|
|
|
@@ -46,7 +48,7 @@ Jerarquía (todas bajo `BugBunny::Error < ::StandardError`):
|
|
|
46
48
|
|---|---|---|
|
|
47
49
|
| `Error` | `::StandardError` | base — no se levanta directa salvo `resource.rb:122` (pool ausente) |
|
|
48
50
|
| `CommunicationError` | `Error` | `bug_bunny.rb:96`, `session.rb:177,285`, `producer.rb:90`, `client.rb:168` — envuelve cualquier `Bunny::Exception` en la frontera del gem; original en `.cause` |
|
|
49
|
-
| `ConfigurationError` | `Error` | `configuration.rb:
|
|
51
|
+
| `ConfigurationError` | `Error` | `configuration.rb:293,300,308` — validación al final de `BugBunny.configure` |
|
|
50
52
|
| `PublishNacked` | `Error` | `producer.rb:235` — NACK del broker en modo `:confirmed`. Attrs: `path`, `nacked_count`. Opt-out `nack_raise: false` |
|
|
51
53
|
| `PublishUnroutable` | `Error` | `producer.rb:325` — `basic.return` con `mandatory: true`. Attrs: `path`, `exchange`, `routing_key`, `reply_code`, `reply_text`, `correlation_id`. Opt-out `return_raise: false` |
|
|
52
54
|
| `ClientError` | `Error` | `raise_error.rb:170` — 4xx no mapeado explícito |
|
data/docs/release/release.md
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
> a la RFC vigente) · anclado a `5697541`
|
|
6
6
|
> (`.github/workflows/release.yml`, `*.gemspec`, `lib/bug_bunny/version.rb`,
|
|
7
7
|
> `CHANGELOG.md`) · fecha 2026-07-31 · cobertura:
|
|
8
|
-
> completa (régimen gema: build→publish, §e/f/g n/a).
|
|
8
|
+
> completa (régimen gema: build→publish, §e/f/g n/a). Incremento 2026-10-16 (release 5.3.0):
|
|
9
|
+
> versión actual y los pines de los consumidores (§h), re-medidos; escrito por el agente, sin
|
|
10
|
+
> verificación humana todavía.
|
|
9
11
|
|
|
10
12
|
## 1. Resumen
|
|
11
13
|
|
|
@@ -21,9 +23,9 @@ out-of-repo).
|
|
|
21
23
|
|
|
22
24
|
### a. Hecho verificable
|
|
23
25
|
|
|
24
|
-
- **Convención de versión:** SemVer `vX.X.X`. Actual: **5.
|
|
25
|
-
- **Source of truth:** tag remoto (`v5.
|
|
26
|
-
`lib/bug_bunny/version.rb` (`VERSION = '5.
|
|
26
|
+
- **Convención de versión:** SemVer `vX.X.X`. Actual: **5.3.0**.
|
|
27
|
+
- **Source of truth:** tag remoto (`v5.3.0`) + **triple mirror**
|
|
28
|
+
`lib/bug_bunny/version.rb` (`VERSION = '5.3.0'`) ← `bug_bunny.gemspec:7`
|
|
27
29
|
(`spec.version = BugBunny::VERSION`).
|
|
28
30
|
- **Changelog canónico:** `CHANGELOG.md` único.
|
|
29
31
|
- **Patrón de trigger:** `gema-tag` (patrón 1).
|
|
@@ -34,7 +36,7 @@ out-of-repo).
|
|
|
34
36
|
|
|
35
37
|
- **Convención:** SemVer `vX.X.X` (**con `v`** — distinto al servicio).
|
|
36
38
|
- **Source of truth:** tag remoto canónico (`git tag --sort=-v:refname` →
|
|
37
|
-
`v5.
|
|
39
|
+
`v5.3.0`).
|
|
38
40
|
- **Mirror:** `lib/bug_bunny/version.rb` (`VERSION`), leído por
|
|
39
41
|
`bug_bunny.gemspec:7` (`spec.version = BugBunny::VERSION`).
|
|
40
42
|
`required_ruby_version >= 2.6.0` (`bug_bunny.gemspec:17`).
|
|
@@ -85,11 +87,12 @@ procedimiento per-repo porque no vive acá. Una versión yankeada se anotaría e
|
|
|
85
87
|
`>= 5.1.0, < 5.2.0`, así que un minor **no entra** sin editar el `Gemfile`.
|
|
86
88
|
Consecuencia operativa: un fix publicado como **patch** lo toman con
|
|
87
89
|
`bundle update bug_bunny`; uno publicado como **minor** requiere tocar el pin
|
|
88
|
-
en cada consumidor. Estado medido el 2026-
|
|
90
|
+
en cada consumidor. Estado medido el 2026-10-16 (Gemfile y Gemfile.lock del default de cada clon del
|
|
89
91
|
workspace): `box_manager_service` en `~> 5.1.1`; `box_acs_manager`,
|
|
90
|
-
`box_cluster_manager` y `box_radius_manager` en `~> 5.1.0
|
|
91
|
-
**5.2.0
|
|
92
|
-
|
|
92
|
+
`box_cluster_manager` y `box_radius_manager` en `~> 5.1.0`, los cuatro con 5.1.1 en el lock — o sea que
|
|
93
|
+
**ni 5.2.0 ni 5.3.0 le entran a ninguno** sin tocar el pin. `agent` sigue en `~> 4.11.1`, y el resto de
|
|
94
|
+
los consumidores (`olt_*`, `*_invoice_service`, `notification_sms_service`, `wispro_box_server`,
|
|
95
|
+
`wispro_lan`) en la línea 0.2.x. *(El 2026-09-29 la medición daba lo mismo para los cuatro de 5.x.)* Un cambio de contrato del gem (ej. el
|
|
93
96
|
behavior-change de `4.18.0` — `Bunny::Exception` → `CommunicationError`, o la
|
|
94
97
|
eliminación de `BugBunny::SecurityError` en `5.0.0`) obliga a los consumidores
|
|
95
98
|
a migrar; el `CHANGELOG.md` lo documenta como breaking note. **Orden de
|
data/docs/test/testing.md
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
# Test — bug_bunny
|
|
2
2
|
|
|
3
3
|
> meta: artefacto test · RFC-013 · generado `arch-structure` (§a-§d) +
|
|
4
|
-
> `arch-enrich` (§e-§h) · anclado a `
|
|
4
|
+
> `arch-enrich` (§e-§h) · anclado a `67cca83` (squash de #71 en `main`; antes `35b075a`, y antes `7bf1da7`, que no era ancestro de `main`), `Rakefile`, `bug_bunny.gemspec`,
|
|
5
5
|
> `spec/spec_helper.rb`, `spec/support/integration_helper.rb`,
|
|
6
6
|
> `.github/workflows/main.yml`, `CHANGELOG.md` · fecha 2026-07-22 · cobertura:
|
|
7
7
|
> §a-§d (estructura) + §e-§h (enrich, anclado a specs/CHANGELOG) completas.
|
|
8
8
|
> Incremento 2026-09-28 (#64): conteos, `drain_*_spec` y el gap de aislamiento.
|
|
9
|
+
> Incremento 2026-10-16 (#70): `spec/rails/` (Railtie con Rails de verdad), tarea
|
|
10
|
+
> `spec:rails` y conteos; re-anclado a `67cca83` en el release 5.3.0. Escrito por el agente, sin
|
|
11
|
+
> verificación humana todavía.
|
|
9
12
|
|
|
10
13
|
## 1. Resumen
|
|
11
14
|
|
|
12
|
-
Suite principal **RSpec** (`spec/`,
|
|
15
|
+
Suite principal **RSpec** (`spec/`, 27 specs: 18 unit + 8 integration + 1 rails). Tarea
|
|
13
16
|
`:test` legacy de **Minitest** (`test/`, 2 archivos) fuera del default y del CI.
|
|
14
|
-
CI corre `bundle exec rake`
|
|
17
|
+
CI corre `bundle exec rake` en Ruby 3.4.4: el default corre `:spec` y `spec:rails`, cada una en
|
|
18
|
+
su proceso. `bundle exec rspec` pelado corre `spec/` sin Rails: `.rspec` excluye `spec/rails/` salvo que se lo pida
|
|
19
|
+
explícitamente (`bundle exec rspec spec/rails` la corre, en su propio proceso). Sin coverage tool
|
|
15
20
|
configurado.
|
|
16
21
|
|
|
17
22
|
## 2. Cuerpo
|
|
@@ -20,7 +25,8 @@ configurado.
|
|
|
20
25
|
|
|
21
26
|
| framework | dir | nivel | nº | propósito |
|
|
22
27
|
|---|---|---|---|---|
|
|
23
|
-
| **RSpec** `~> 3.0` | `spec/unit/` | unit |
|
|
28
|
+
| **RSpec** `~> 3.0` | `spec/unit/` | unit | 18 | client/session pool, configuration, consumer, `drain_tracker`, producer, controller, raise_error, remote_error, request, route, observability, otel, resource, middleware, `extra_top_level_params` (hook de params hermanos en `Resource#save`), `consumer_rails_executor_outside_rails` (fuera de Rails no se carga el Railtie ni el middleware del executor) |
|
|
29
|
+
| **RSpec** + `railties`/`activerecord`/`sqlite3` | `spec/rails/` | integration con Rails (sin broker) | 1 | `consumer_rails_executor`: una app Rails mínima (`spec/rails/app.rb`, ActiveRecord sobre SQLite en memoria) prueba que cada mensaje corre dentro del executor — el `CurrentAttributes` no pasa al siguiente, la conexión vuelve al pool, el orden frente a un middleware de tracing, el error que escapa (reject + `Rails.error`) y `consumer_rails_executor = false` (en subproceso) |
|
|
24
30
|
| **RSpec** | `spec/integration/` | integration | 8 | client, consumer_middleware, controller, drain, error_handling, infrastructure, publisher_confirms, resource — **requieren RabbitMQ real** (usan `BugBunny.create_connection` + pool) |
|
|
25
31
|
| **Minitest** `~> 5.0` (+ `mocha`, `minitest-reporters`) | `test/integration/` | integration (legacy) | 2 | `manual_client_test.rb`, `infrastructure_test.rb` — tarea `:test`, **no** en default ni CI |
|
|
26
32
|
|
|
@@ -30,13 +36,16 @@ Sin tags declarados (`:slow`/`:js`) en la config de RSpec.
|
|
|
30
36
|
|
|
31
37
|
| objetivo | comando | qué corre |
|
|
32
38
|
|---|---|---|
|
|
33
|
-
| default / CI | `bundle exec rake` |
|
|
39
|
+
| default / CI | `bundle exec rake` | `:spec` (la suite RSpec menos `spec/rails/`) y después `spec:rails`, cada una en su proceso — `Rakefile:` `task default: [:spec, 'spec:rails']` |
|
|
40
|
+
| `spec/` sin Rails | `bundle exec rspec` | todo `spec/` menos `spec/rails/` (lo excluye `.rspec` cuando no se pide `spec/rails` explícitamente) |
|
|
34
41
|
| solo unit | `bundle exec rake spec:unit` | `spec/unit/**/*_spec.rb` |
|
|
35
42
|
| solo integration | `bundle exec rake spec:integration` | `spec/integration/**/*_spec.rb` (requiere broker) |
|
|
43
|
+
| Railtie (Rails) | `bundle exec rake spec:rails` | `spec/rails/**/*_spec.rb`, en su propio proceso (Rails se inicializa una vez por proceso); equivale a `bundle exec rspec spec/rails` |
|
|
36
44
|
| Minitest legacy | `bundle exec rake test` | `test/**/*_test.rb` |
|
|
37
45
|
| CI | `.github/workflows/main.yml` → `bundle exec rake` | matrix Ruby `3.4.4`, `ruby/setup-ruby@v1` + `bundler-cache`, on push `main` + PR |
|
|
38
46
|
|
|
39
|
-
>
|
|
47
|
+
> Las tareas RSpec corren con `--require spec_helper` (`Rakefile`), salvo `spec:rails`, que
|
|
48
|
+
> carga su propio `spec/rails/rails_helper.rb`.
|
|
40
49
|
|
|
41
50
|
### c. Fixtures / Factories
|
|
42
51
|
|
|
@@ -185,6 +185,16 @@ module BugBunny
|
|
|
185
185
|
# `Client#publish`.
|
|
186
186
|
attr_accessor :return_raise
|
|
187
187
|
|
|
188
|
+
# @return [Boolean] Si `true` (default) y la gema corre dentro de Rails, el Railtie envuelve
|
|
189
|
+
# cada mensaje del Consumer con `Rails.application.executor.wrap`
|
|
190
|
+
# ({BugBunny::ConsumerMiddleware::RailsExecutor}): el estado por hilo (`CurrentAttributes`,
|
|
191
|
+
# la conexión de ActiveRecord) no pasa de un mensaje al siguiente.
|
|
192
|
+
# Fuera de Rails no tiene efecto.
|
|
193
|
+
#
|
|
194
|
+
# Se lee al terminar de inicializar la app, así que se apaga desde `config/initializers`.
|
|
195
|
+
# Apagalo si el consumidor ya envuelve cada mensaje por su cuenta.
|
|
196
|
+
attr_accessor :consumer_rails_executor
|
|
197
|
+
|
|
188
198
|
# @!endgroup
|
|
189
199
|
|
|
190
200
|
# Inicializa la configuración con valores por defecto seguros.
|
|
@@ -264,6 +274,7 @@ module BugBunny
|
|
|
264
274
|
@on_return = nil
|
|
265
275
|
@nack_raise = true
|
|
266
276
|
@return_raise = true
|
|
277
|
+
@consumer_rails_executor = true
|
|
267
278
|
end
|
|
268
279
|
|
|
269
280
|
# Defaults de {BugBunny::Consumer#drain}.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BugBunny
|
|
4
|
+
module ConsumerMiddleware
|
|
5
|
+
# Envuelve cada mensaje con el executor de Rails, como hace Sidekiq con cada job.
|
|
6
|
+
#
|
|
7
|
+
# El Consumer procesa los mensajes en el hilo de trabajo de Bunny, que Rails no conoce. Sin
|
|
8
|
+
# el executor, lo que Rails guarda por hilo pasa de un mensaje al siguiente:
|
|
9
|
+
# - los `ActiveSupport::CurrentAttributes` (y lo que vive en ellos: tags, trazas, métricas);
|
|
10
|
+
# - la conexión de ActiveRecord, que el hilo retiene hasta morir en vez de devolverla al pool.
|
|
11
|
+
#
|
|
12
|
+
# Además, el executor prende la caché de queries de ActiveRecord durante el mensaje y la limpia al
|
|
13
|
+
# terminarlo (en el hilo de Bunny estaba apagada).
|
|
14
|
+
#
|
|
15
|
+
# Lo registra el Railtie al principio de la cadena (ver {Stack#prepend}) salvo que
|
|
16
|
+
# `config.consumer_rails_executor` sea `false`. Fuera de Rails no se carga.
|
|
17
|
+
#
|
|
18
|
+
# **Errores:** `executor.wrap` relanza lo que escapa del mensaje, así que sigue el camino de
|
|
19
|
+
# siempre (el Consumer lo rechaza sin requeue). En Rails >= 7.0, antes de relanzar, además lo
|
|
20
|
+
# reporta a `Rails.error` con `handled: false`.
|
|
21
|
+
#
|
|
22
|
+
# Si el hilo ya está dentro del executor, `wrap` no abre otro: sólo ejecuta el bloque.
|
|
23
|
+
class RailsExecutor < Base
|
|
24
|
+
# @param delivery_info [Bunny::DeliveryInfo]
|
|
25
|
+
# @param properties [Bunny::MessageProperties]
|
|
26
|
+
# @param body [String]
|
|
27
|
+
# @return [void]
|
|
28
|
+
def call(delivery_info, properties, body)
|
|
29
|
+
::Rails.application.executor.wrap { @app.call(delivery_info, properties, body) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -61,6 +61,19 @@ module BugBunny
|
|
|
61
61
|
self
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
# Registra un middleware al principio de la cadena, para que envuelva a todos los demás. Thread-safe.
|
|
65
|
+
#
|
|
66
|
+
# Es para lo que tiene que quedar afuera de todo sin depender del orden en que se registran los
|
|
67
|
+
# demás: {RailsExecutor} va primero porque limpia el estado por hilo al arrancar, y un middleware
|
|
68
|
+
# que carga ese estado antes que él (el de tracing de ExisRay) lo perdería.
|
|
69
|
+
#
|
|
70
|
+
# @param middleware_class [Class] Clase que hereda de {Base}.
|
|
71
|
+
# @return [self]
|
|
72
|
+
def prepend(middleware_class)
|
|
73
|
+
@mutex.synchronize { @middlewares.unshift(middleware_class) }
|
|
74
|
+
self
|
|
75
|
+
end
|
|
76
|
+
|
|
64
77
|
# @return [Boolean] `true` si no hay middlewares registrados. Thread-safe.
|
|
65
78
|
def empty?
|
|
66
79
|
@mutex.synchronize { @middlewares.empty? }
|
data/lib/bug_bunny/railtie.rb
CHANGED
|
@@ -8,6 +8,7 @@ module BugBunny
|
|
|
8
8
|
# Esta clase es responsable de:
|
|
9
9
|
# 1. Registrar la carpeta `app/rabbit` en el autoloader de Rails (Zeitwerk).
|
|
10
10
|
# 2. Gestionar la seguridad de las conexiones durante el "forking" de procesos (Puma/Spring).
|
|
11
|
+
# 3. Envolver cada mensaje del Consumer con el executor de Rails (ver {ConsumerMiddleware::RailsExecutor}).
|
|
11
12
|
#
|
|
12
13
|
# @see https://guides.rubyonrails.org/engines.html#railtie
|
|
13
14
|
class Railtie < ::Rails::Railtie
|
|
@@ -34,12 +35,27 @@ module BugBunny
|
|
|
34
35
|
end
|
|
35
36
|
end
|
|
36
37
|
|
|
37
|
-
# 3.
|
|
38
|
+
# 3. Executor de Rails alrededor de cada mensaje del Consumer
|
|
39
|
+
#
|
|
40
|
+
# El Consumer procesa en el hilo de Bunny, que Rails no conoce: sin esto, el estado por hilo
|
|
41
|
+
# pasa de un mensaje al siguiente. Va al principio de la cadena (`prepend`, no `use`) para que
|
|
42
|
+
# envuelva también a los middlewares que cargan ese estado, como el de tracing de ExisRay, que
|
|
43
|
+
# se registra en su propio `after_initialize` y en un orden que no controlamos.
|
|
44
|
+
config.after_initialize do
|
|
45
|
+
# Sin un `BugBunny.configure` previo, vale la configuración por default; un `configure` posterior la reusa.
|
|
46
|
+
BugBunny.configuration ||= BugBunny::Configuration.new
|
|
47
|
+
if BugBunny.configuration.consumer_rails_executor
|
|
48
|
+
require_relative 'consumer_middleware/rails_executor'
|
|
49
|
+
BugBunny.consumer_middlewares.prepend(BugBunny::ConsumerMiddleware::RailsExecutor)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# 4. Rake tasks (bug_bunny:sync)
|
|
38
54
|
rake_tasks do
|
|
39
55
|
load File.expand_path('../tasks/bug_bunny.rake', __dir__)
|
|
40
56
|
end
|
|
41
57
|
|
|
42
|
-
#
|
|
58
|
+
# 5. Hook de Spring (Preloader)
|
|
43
59
|
if defined?(Spring)
|
|
44
60
|
Spring.after_fork do
|
|
45
61
|
BugBunny.disconnect
|
data/lib/bug_bunny/version.rb
CHANGED
data/skill/SKILL.md
CHANGED
|
@@ -19,10 +19,12 @@ Gema Ruby: capa de routing RESTful sobre AMQP/RabbitMQ. Microservicios se comuni
|
|
|
19
19
|
|
|
20
20
|
## Contrato resumido (piso mínimo)
|
|
21
21
|
|
|
22
|
-
> Resume el contrato de **`bug_bunny` 5.
|
|
22
|
+
> Resume el contrato de **`bug_bunny` 5.3.0** (anclado a `v5.3.0`). Suficiente para el uso típico sin abrir el detalle; el detalle version-locked está en el **Índice de artefactos** de abajo. Antipatrones/API completa: más abajo (embebido interim, ver Cobertura y fronteras).
|
|
23
23
|
>
|
|
24
24
|
> **Si venís de 5.1.x, un cambio de conducta (5.2.0):** una entrega cuyo middleware —o `handle_fatal_error`— levanta **antes del ack** ahora se **rechaza sin requeue** (evento `consumer.delivery_failed`), en `subscribe` y en `drain`. Antes quedaba sin ack ni reject y trababa el consumidor. Si un middleware levantaba a propósito para que el mensaje se reintentara, ahora el mensaje se pierde: el reintento es tuyo. Detalle en [`docs/behavior/behavior.md`](../docs/behavior/behavior.md).
|
|
25
25
|
>
|
|
26
|
+
> **Si venís de 5.2.x, dos cambios de conducta dentro de Rails (5.3.0):** cada mensaje del Consumer corre dentro de `Rails.application.executor.wrap`. En Rails ≥ 7.0, un error que escapa del mensaje además se reporta a `Rails.error` (el mensaje se sigue rechazando sin requeue), y la caché de queries de ActiveRecord queda prendida durante cada mensaje. Se apagan con `config.consumer_rails_executor = false`. Detalle en [`docs/behavior/behavior.md`](../docs/behavior/behavior.md) y [`docs/config/configuracion.md`](../docs/config/configuracion.md).
|
|
27
|
+
>
|
|
26
28
|
> **Si venís de 4.x, dos breaking a mirar antes de subir:** `5.0.0` eliminó la constante pública `BugBunny::SecurityError` (un `rescue BugBunny::SecurityError` que sobreviva revienta con `NameError` y **enmascara la excepción original**) y `4.18.0` cambió el wrapping `Bunny::Exception` → `CommunicationError`. Detalle en `CHANGELOG.md`.
|
|
27
29
|
|
|
28
30
|
**Símbolos públicos clave**
|
|
@@ -63,16 +65,16 @@ client.publish('events', body: { type: 'x' }) # => { 'status' => 202 }
|
|
|
63
65
|
|
|
64
66
|
## Índice de artefactos (fuente de verdad)
|
|
65
67
|
|
|
66
|
-
El detalle vive en `docs/<capa>/` (modelo `dev-*`); esta skill **indexa y resume**, no duplica. Links relativos = version-locked (mismo tag del release, `v5.
|
|
68
|
+
El detalle vive en `docs/<capa>/` (modelo `dev-*`); esta skill **indexa y resume**, no duplica. Links relativos = version-locked (mismo tag del release, `v5.3.0`; `gemspec.files` incluye `docs/**`, así que estos archivos viajan dentro del `.gem` que ya tenés instalado).
|
|
67
69
|
|
|
68
70
|
| Capa | Artefacto | Estado |
|
|
69
71
|
|---|---|---|
|
|
70
72
|
| Glosario de dominio | [docs/glossary/glossary.md](../docs/glossary/glossary.md) | parcial, acreta por PR |
|
|
71
|
-
| Comportamiento (flujos) | [docs/behavior/behavior.md](../docs/behavior/behavior.md) | completa —
|
|
73
|
+
| Comportamiento (flujos) | [docs/behavior/behavior.md](../docs/behavior/behavior.md) | completa — 8 flujos |
|
|
72
74
|
| Configuración | [docs/config/configuracion.md](../docs/config/configuracion.md) | §a-§e/§i estructura + §f/§g/§h enrich — completa |
|
|
73
75
|
| Dependencias consumidas | [docs/consumed/rabbitmq.md](../docs/consumed/rabbitmq.md) | §a/§b/§d estructura + §c/§e enrich |
|
|
74
76
|
| Errores | [docs/errors/errors.md](../docs/errors/errors.md) | §a/§b/§d completas; §c política inferida (verificación humana pendiente) |
|
|
75
|
-
| Test | [docs/test/testing.md](../docs/test/testing.md) | §a-§h —
|
|
77
|
+
| Test | [docs/test/testing.md](../docs/test/testing.md) | §a-§h — 18 unit / 8 integration / 1 rails (`spec/rails/`, en su propio proceso) |
|
|
76
78
|
| Release | [docs/release/release.md](../docs/release/release.md) | completa (régimen gema: build→publish; §e/f/g n/a) |
|
|
77
79
|
| Datos | — | n/a — gema sin DB |
|
|
78
80
|
| Eventos | — | n/a — la gema **es** el transporte; no declara catálogo de eventos de dominio propio |
|
|
@@ -139,7 +141,8 @@ Por RFC-008 §2: no se fabrica la capa, no se borra contrato sin destino, no se
|
|
|
139
141
|
| `BugBunny::Producer` | Publica mensajes. Implementa tres modos: `#fire` (async), `#confirmed` (sync con `wait_for_confirms`) y `#rpc` (direct reply-to + `Concurrent::IVar`). |
|
|
140
142
|
| `BugBunny::Client` | API de alto nivel. Pool de conexiones y middleware stack (onion architecture). |
|
|
141
143
|
| `BugBunny::Consumer` | Subscribe loop con health check. Rutea mensajes via `BugBunny.routes`. |
|
|
142
|
-
| `BugBunny::ConsumerMiddleware::Stack` | Pipeline de middlewares antes de `process_message`. Thread-safe. |
|
|
144
|
+
| `BugBunny::ConsumerMiddleware::Stack` | Pipeline de middlewares antes de `process_message`. Thread-safe. `use` agrega al final; `prepend` pone al principio (envuelve a todos). |
|
|
145
|
+
| `BugBunny::ConsumerMiddleware::RailsExecutor` | Sólo en Rails: envuelve cada mensaje con `Rails.application.executor.wrap`, para que el estado por hilo (`CurrentAttributes`, conexión de AR) no pase al siguiente. Lo registra el Railtie primero; se apaga con `config.consumer_rails_executor = false`. |
|
|
143
146
|
| `BugBunny::Controller` | Base class tipo Rails. `before_action`, `around_action`, `after_action`, `rescue_from`, `render`. |
|
|
144
147
|
| `BugBunny::Resource` | ORM sobre AMQP. `find`, `where`, `create`, `save`, `destroy`. ActiveModel validations y callbacks. |
|
|
145
148
|
| `BugBunny::Routing::RouteSet` | DSL de rutas: `resources`, `namespace`, `member`, `collection`. |
|
|
@@ -147,7 +150,7 @@ Por RFC-008 §2: no se fabrica la capa, no se borra contrato sin destino, no se
|
|
|
147
150
|
| `BugBunny::Middleware::Stack` | Builder de middlewares client-side (onion architecture tipo Faraday). |
|
|
148
151
|
| BugBunny::Request | Value object del mensaje saliente con metadata AMQP completa. |
|
|
149
152
|
| BugBunny::OTel | Helpers para emitir campos siguiendo las OTel semantic conventions for messaging. |
|
|
150
|
-
| BugBunny::Railtie | Integración Rails: autoload de `app/rabbit`, fork safety (Puma, Spring). |
|
|
153
|
+
| BugBunny::Railtie | Integración Rails: autoload de `app/rabbit`, fork safety (Puma, Spring), executor de Rails alrededor de cada mensaje del Consumer. |
|
|
151
154
|
|
|
152
155
|
---
|
|
153
156
|
|
|
@@ -386,6 +389,9 @@ Cada slot del pool cachea su `Session` y `Producer` durante su vida útil. Esto
|
|
|
386
389
|
### ¿Cómo funciona fork safety?
|
|
387
390
|
`BugBunny::Railtie` registra hooks en `ActiveSupport::ForkTracker` (Rails 7.1+), `Puma.events.on_worker_boot` y `Spring.after_fork` para llamar `BugBunny.disconnect` y evitar sockets TCP heredados.
|
|
388
391
|
|
|
392
|
+
### ¿Por qué el estado de un mensaje no pasa al siguiente?
|
|
393
|
+
El Consumer procesa en el hilo de trabajo de Bunny, que Rails no conoce. En Rails, el Railtie registra `ConsumerMiddleware::RailsExecutor` al principio de la cadena (`prepend`), que envuelve cada mensaje con `Rails.application.executor.wrap`: al entrar y al salir se resetean los `CurrentAttributes`, y al salir la conexión de ActiveRecord vuelve al pool. Va primero para envolver también a los middlewares que cargan estado por hilo (el de tracing de ExisRay). **Cambios de conducta:** en Rails ≥ 7.0, un error que escapa del mensaje además se reporta a `Rails.error` con `handled: false` (el mensaje se sigue rechazando sin requeue); y la caché de queries de AR queda prendida durante cada mensaje (en el hilo de Bunny estaba apagada). Apagado: `config.consumer_rails_executor = false`.
|
|
394
|
+
|
|
389
395
|
---
|
|
390
396
|
|
|
391
397
|
## Antipatrones
|
|
@@ -87,6 +87,15 @@ BugBunny.configuration.consumer_middlewares.use MyTracing::Middleware
|
|
|
87
87
|
BugBunny.configuration.consumer_middlewares.use MyAuth::Middleware
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
`use` agrega al final de la cadena; `prepend` pone al principio, envolviendo a todos.
|
|
91
|
+
|
|
92
|
+
### Executor de Rails (automático en Rails)
|
|
93
|
+
|
|
94
|
+
En Rails, el Railtie registra `BugBunny::ConsumerMiddleware::RailsExecutor` **primero**: cada mensaje corre dentro
|
|
95
|
+
de `Rails.application.executor.wrap`, así que los `CurrentAttributes` no pasan de un mensaje al siguiente y la
|
|
96
|
+
conexión de ActiveRecord vuelve al pool. Cambios de conducta: en Rails ≥ 7.0, un error que escapa del mensaje además
|
|
97
|
+
se reporta a `Rails.error`, y la caché de queries de AR queda prendida durante cada mensaje. Se apaga con `config.consumer_rails_executor = false` (desde `config/initializers`).
|
|
98
|
+
|
|
90
99
|
### Crear Middleware
|
|
91
100
|
|
|
92
101
|
```ruby
|
data/skill/references/testing.md
CHANGED
|
@@ -131,7 +131,10 @@ TEST_POOL = ConnectionPool.new(size: 5) { BugBunny.create_connection }
|
|
|
131
131
|
## Ejecutar Tests
|
|
132
132
|
|
|
133
133
|
```bash
|
|
134
|
-
bundle exec
|
|
134
|
+
bundle exec rake # Todos: spec/ y spec/rails/ (cada una en su proceso)
|
|
135
|
+
bundle exec rspec # spec/, sin Rails (.rspec excluye spec/rails/ si no se la pide)
|
|
136
|
+
bundle exec rspec spec/rails # Solo el Railtie (igual que rake spec:rails)
|
|
137
|
+
bundle exec rake spec:rails # Solo el Railtie, con una app Rails mínima
|
|
135
138
|
bundle exec rspec spec/unit/ # Solo unit
|
|
136
139
|
bundle exec rspec spec/integration/ # Solo integration (requiere RabbitMQ)
|
|
137
140
|
bundle exec rspec spec/unit/session_spec.rb # Archivo específico
|
data/spec/rails/app.rb
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# App Rails mínima para probar el Railtie de BugBunny con Rails de verdad (ActiveRecord sobre SQLite
|
|
4
|
+
# en memoria).
|
|
5
|
+
#
|
|
6
|
+
# Rails se inicializa una vez por proceso, así que estos specs corren en su propio proceso
|
|
7
|
+
# (`rake spec:rails`): la suite de `spec/` sigue probando la gema fuera de Rails. Para bootear con
|
|
8
|
+
# otra configuración, el spec corre este archivo solo, en un subproceso (`ruby spec/rails/app.rb`),
|
|
9
|
+
# y lee la cadena de middlewares que imprime.
|
|
10
|
+
#
|
|
11
|
+
# `BUG_BUNNY_SPEC_EXECUTOR=off` bootea con `config.consumer_rails_executor = false`.
|
|
12
|
+
# `BUG_BUNNY_SPEC_CONFIGURE=none` bootea sin llamar a `BugBunny.configure` ni registrar otros middlewares:
|
|
13
|
+
# la configuración queda la de default.
|
|
14
|
+
|
|
15
|
+
ENV['RAILS_ENV'] = 'test'
|
|
16
|
+
ENV['DATABASE_URL'] = 'sqlite3::memory:'
|
|
17
|
+
|
|
18
|
+
require 'bundler/setup'
|
|
19
|
+
require 'logger'
|
|
20
|
+
require 'rails'
|
|
21
|
+
require 'active_record/railtie'
|
|
22
|
+
require 'bug_bunny'
|
|
23
|
+
|
|
24
|
+
module BugBunnyRailsSpec
|
|
25
|
+
# La app: sin archivos de configuración propios, todo acá.
|
|
26
|
+
class Application < Rails::Application
|
|
27
|
+
config.root = __dir__
|
|
28
|
+
config.eager_load = false
|
|
29
|
+
config.logger = Logger.new(nil)
|
|
30
|
+
config.secret_key_base = 'bug_bunny-rails-spec'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Lo que un mensaje deja por hilo: lo que los specs miran pasar (o no) al siguiente.
|
|
34
|
+
class LeftoverState < ActiveSupport::CurrentAttributes
|
|
35
|
+
attribute :value
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Contexto de traza, como el `ExisRay::Tracer` de ExisRay: un `CurrentAttributes`.
|
|
39
|
+
class TraceContext < ActiveSupport::CurrentAttributes
|
|
40
|
+
attribute :trace_id
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Un middleware que carga estado por hilo antes de procesar el mensaje y lo limpia al salir, como
|
|
44
|
+
# el de tracing de ExisRay. Se registra ANTES de que el Railtie de BugBunny registre el suyo: es el
|
|
45
|
+
# orden que hace falta para ver si el executor lo borra.
|
|
46
|
+
class TracingMiddleware < BugBunny::ConsumerMiddleware::Base
|
|
47
|
+
def call(delivery_info, properties, body)
|
|
48
|
+
TraceContext.trace_id = 'from-tracing'
|
|
49
|
+
@app.call(delivery_info, properties, body)
|
|
50
|
+
ensure
|
|
51
|
+
TraceContext.reset
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
unless ENV['BUG_BUNNY_SPEC_CONFIGURE'] == 'none'
|
|
57
|
+
BugBunny.configure do |config|
|
|
58
|
+
config.logger = Logger.new(nil)
|
|
59
|
+
# El `respond_to?` deja correr estos specs contra una versión de la gema sin la opción, para ver
|
|
60
|
+
# que fallan sin el cambio.
|
|
61
|
+
if config.respond_to?(:consumer_rails_executor=)
|
|
62
|
+
config.consumer_rails_executor = ENV['BUG_BUNNY_SPEC_EXECUTOR'] != 'off'
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
BugBunny.consumer_middlewares.use(BugBunnyRailsSpec::TracingMiddleware)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
BugBunnyRailsSpec::Application.initialize!
|
|
69
|
+
|
|
70
|
+
puts BugBunny.consumer_middlewares.instance_variable_get(:@middlewares).map(&:name) if $PROGRAM_NAME == __FILE__
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'rails_helper'
|
|
4
|
+
require 'open3'
|
|
5
|
+
|
|
6
|
+
# Cada mensaje del Consumer corre dentro del executor de Rails (sequre/bug_bunny#70).
|
|
7
|
+
#
|
|
8
|
+
# Las entregas se procesan en un hilo propio, como el hilo de trabajo de Bunny: Rails no lo conoce.
|
|
9
|
+
RSpec.describe 'BugBunny::ConsumerMiddleware::RailsExecutor' do
|
|
10
|
+
let(:channel) do
|
|
11
|
+
double('channel').tap do |ch|
|
|
12
|
+
allow(ch).to receive(:ack)
|
|
13
|
+
allow(ch).to receive(:reject)
|
|
14
|
+
allow(ch).to receive(:open?).and_return(true)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
let(:consumer) do
|
|
19
|
+
session = instance_double(BugBunny::Session, channel: channel, close: nil)
|
|
20
|
+
described_consumer = BugBunny::Consumer.new(double('connection'))
|
|
21
|
+
described_consumer.instance_variable_set(:@session, session)
|
|
22
|
+
described_consumer
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
let(:properties) do
|
|
26
|
+
double('properties', type: 'probe/show', headers: { 'x-http-method' => 'GET' },
|
|
27
|
+
correlation_id: 'corr-1', reply_to: nil, content_type: 'application/json')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Lo que hace la acción del controller en cada entrega.
|
|
31
|
+
let(:on_message) { -> {} }
|
|
32
|
+
|
|
33
|
+
before do
|
|
34
|
+
allow(BugBunny.routes).to receive(:recognize)
|
|
35
|
+
.and_return({ controller: 'probe', action: 'show', params: {}, namespace: nil })
|
|
36
|
+
|
|
37
|
+
handler = on_message
|
|
38
|
+
stub_const('BugBunny::Controllers::ProbeController', Class.new(BugBunny::Controller) do
|
|
39
|
+
define_method(:show) do
|
|
40
|
+
handler.call
|
|
41
|
+
render status: 200, json: {}
|
|
42
|
+
end
|
|
43
|
+
end)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Procesa las entregas en orden, en un hilo propio (como el de Bunny).
|
|
47
|
+
def deliver_on_a_bunny_like_thread(*tags)
|
|
48
|
+
Thread.new do
|
|
49
|
+
tags.each do |tag|
|
|
50
|
+
delivery_info = double('delivery_info', exchange: 'x', routing_key: 'probe', delivery_tag: tag,
|
|
51
|
+
redelivered?: false)
|
|
52
|
+
yield(:before, tag) if block_given?
|
|
53
|
+
consumer.send(:handle_delivery, delivery_info, properties, '{}')
|
|
54
|
+
yield(:after, tag) if block_given?
|
|
55
|
+
end
|
|
56
|
+
end.join
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'queda primero en la cadena, antes del middleware de tracing registrado antes que él' do
|
|
60
|
+
middlewares = BugBunny.consumer_middlewares.instance_variable_get(:@middlewares).map(&:name)
|
|
61
|
+
|
|
62
|
+
expect(middlewares).to eq(%w[BugBunny::ConsumerMiddleware::RailsExecutor BugBunnyRailsSpec::TracingMiddleware])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe 'el estado por hilo no pasa de un mensaje al siguiente' do
|
|
66
|
+
let(:seen) { [] }
|
|
67
|
+
let(:on_message) do
|
|
68
|
+
lambda do
|
|
69
|
+
seen << BugBunnyRailsSpec::LeftoverState.value
|
|
70
|
+
BugBunnyRailsSpec::LeftoverState.value = 'left by the previous message'
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'el segundo mensaje arranca sin lo que el primero dejó en un CurrentAttributes' do
|
|
75
|
+
deliver_on_a_bunny_like_thread(1, 2)
|
|
76
|
+
|
|
77
|
+
expect(seen).to eq([nil, nil])
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe 'la conexión de ActiveRecord' do
|
|
82
|
+
# Si el hilo tiene una conexión tomada del pool (en Rails 8.1, `active_connection?` devuelve la
|
|
83
|
+
# conexión, o nil).
|
|
84
|
+
def connection_leased?
|
|
85
|
+
ActiveRecord::Base.connection_pool.active_connection?.present?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
let(:inside) { [] }
|
|
89
|
+
let(:on_message) do
|
|
90
|
+
lambda do
|
|
91
|
+
ActiveRecord::Base.connection.execute('SELECT 1')
|
|
92
|
+
inside << connection_leased?
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'vuelve al pool al terminar el mensaje que la usó' do
|
|
97
|
+
after = []
|
|
98
|
+
deliver_on_a_bunny_like_thread(1) do |moment, _tag|
|
|
99
|
+
after << connection_leased? if moment == :after
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
expect(inside).to eq([true]) # el mensaje la usó
|
|
103
|
+
expect(after).to eq([false]) # y al terminar ya no la tiene el hilo
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe 'un middleware que carga estado antes que el executor' do
|
|
108
|
+
let(:seen) { [] }
|
|
109
|
+
let(:on_message) { -> { seen << BugBunnyRailsSpec::TraceContext.trace_id } }
|
|
110
|
+
|
|
111
|
+
it 'conserva ese estado dentro del mensaje: el executor lo envuelve y no lo borra' do
|
|
112
|
+
deliver_on_a_bunny_like_thread(1)
|
|
113
|
+
|
|
114
|
+
expect(seen).to eq(['from-tracing'])
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe 'un error que escapa del procesamiento del mensaje' do
|
|
119
|
+
let(:reports) { [] }
|
|
120
|
+
let(:subscriber) do
|
|
121
|
+
collected = reports
|
|
122
|
+
Class.new do
|
|
123
|
+
define_method(:report) { |error, handled:, **| collected << [error.message, handled] }
|
|
124
|
+
end.new
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
before { Rails.error.subscribe(subscriber) }
|
|
128
|
+
after { Rails.error.unsubscribe(subscriber) if Rails.error.respond_to?(:unsubscribe) } # no existe en Rails 7.0
|
|
129
|
+
|
|
130
|
+
it 'termina en un reject sin requeue, como hoy, y además se reporta a Rails.error' do
|
|
131
|
+
allow(consumer).to receive(:process_message).and_raise(RuntimeError, 'boom')
|
|
132
|
+
|
|
133
|
+
deliver_on_a_bunny_like_thread(7)
|
|
134
|
+
|
|
135
|
+
expect(channel).to have_received(:reject).with(7, false)
|
|
136
|
+
expect(reports).to eq([['boom', false]])
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
describe 'config.consumer_rails_executor' do
|
|
141
|
+
# Rails se inicializa una vez por proceso: la app se bootea de nuevo en un subproceso.
|
|
142
|
+
# Las líneas de stdout que nombran un middleware: un warning del entorno no las rompe.
|
|
143
|
+
def middlewares_after_boot(env)
|
|
144
|
+
app = File.expand_path('app.rb', __dir__)
|
|
145
|
+
stdout, stderr, status = Open3.capture3(env, RbConfig.ruby, app)
|
|
146
|
+
raise "la app no booteó:\n#{stderr}" unless status.success?
|
|
147
|
+
|
|
148
|
+
stdout.lines.map(&:strip).grep(/\ABugBunny/)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it 'con false, no registra el middleware' do
|
|
152
|
+
expect(middlewares_after_boot('BUG_BUNNY_SPEC_EXECUTOR' => 'off')).to eq(['BugBunnyRailsSpec::TracingMiddleware'])
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it 'con true, lo registra primero' do
|
|
156
|
+
expect(middlewares_after_boot('BUG_BUNNY_SPEC_EXECUTOR' => 'on'))
|
|
157
|
+
.to eq(%w[BugBunny::ConsumerMiddleware::RailsExecutor BugBunnyRailsSpec::TracingMiddleware])
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it 'sin BugBunny.configure previo, bootea y lo registra: la opción ausente vale su default' do
|
|
161
|
+
expect(middlewares_after_boot('BUG_BUNNY_SPEC_CONFIGURE' => 'none'))
|
|
162
|
+
.to eq(['BugBunny::ConsumerMiddleware::RailsExecutor'])
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -188,6 +188,12 @@ RSpec.describe BugBunny::Configuration do
|
|
|
188
188
|
end
|
|
189
189
|
end
|
|
190
190
|
|
|
191
|
+
describe 'consumer_rails_executor flag' do
|
|
192
|
+
it 'tiene default true (el Railtie envuelve cada mensaje con el executor de Rails)' do
|
|
193
|
+
expect(BugBunny::Configuration.new.consumer_rails_executor).to be(true)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
191
197
|
describe 'nack_raise flag' do
|
|
192
198
|
it 'tiene default true (raise PublishNacked en NACK)' do
|
|
193
199
|
expect(BugBunny::Configuration.new.nack_raise).to be(true)
|
|
@@ -47,6 +47,19 @@ RSpec.describe BugBunny::ConsumerMiddleware::Stack do
|
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
describe '#prepend' do
|
|
51
|
+
it 'registra el middleware al principio: envuelve a los ya registrados' do
|
|
52
|
+
log = []
|
|
53
|
+
stack.use(tracking_middleware(log, :a))
|
|
54
|
+
result = stack.prepend(tracking_middleware(log, :first))
|
|
55
|
+
|
|
56
|
+
stack.call(delivery_info, properties, body) { log << :core }
|
|
57
|
+
|
|
58
|
+
expect(result).to be(stack)
|
|
59
|
+
expect(log).to eq(%i[first_before a_before core a_after first_after])
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
50
63
|
describe '#empty?' do
|
|
51
64
|
it 'retorna true cuando no hay middlewares' do
|
|
52
65
|
expect(stack.empty?).to be(true)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
# Fuera de Rails la gema se comporta como antes de sequre/bug_bunny#70: el middleware del executor
|
|
6
|
+
# lo registra el Railtie, y el Railtie no se carga.
|
|
7
|
+
RSpec.describe 'BugBunny fuera de Rails' do
|
|
8
|
+
it 'no carga el Railtie' do
|
|
9
|
+
expect(defined?(Rails)).to be_nil
|
|
10
|
+
expect(defined?(BugBunny::Railtie)).to be_nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'no carga el middleware del executor ni lo pone en la cadena del Consumer' do
|
|
14
|
+
middlewares = BugBunny.consumer_middlewares.instance_variable_get(:@middlewares).map(&:name)
|
|
15
|
+
|
|
16
|
+
expect(defined?(BugBunny::ConsumerMiddleware::RailsExecutor)).to be_nil
|
|
17
|
+
expect(middlewares).not_to include('BugBunny::ConsumerMiddleware::RailsExecutor')
|
|
18
|
+
end
|
|
19
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bug_bunny
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gabix
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|
|
@@ -220,6 +220,48 @@ dependencies:
|
|
|
220
220
|
- - "~>"
|
|
221
221
|
- !ruby/object:Gem::Version
|
|
222
222
|
version: '1.6'
|
|
223
|
+
- !ruby/object:Gem::Dependency
|
|
224
|
+
name: activerecord
|
|
225
|
+
requirement: !ruby/object:Gem::Requirement
|
|
226
|
+
requirements:
|
|
227
|
+
- - ">="
|
|
228
|
+
- !ruby/object:Gem::Version
|
|
229
|
+
version: '6.1'
|
|
230
|
+
type: :development
|
|
231
|
+
prerelease: false
|
|
232
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
233
|
+
requirements:
|
|
234
|
+
- - ">="
|
|
235
|
+
- !ruby/object:Gem::Version
|
|
236
|
+
version: '6.1'
|
|
237
|
+
- !ruby/object:Gem::Dependency
|
|
238
|
+
name: railties
|
|
239
|
+
requirement: !ruby/object:Gem::Requirement
|
|
240
|
+
requirements:
|
|
241
|
+
- - ">="
|
|
242
|
+
- !ruby/object:Gem::Version
|
|
243
|
+
version: '6.1'
|
|
244
|
+
type: :development
|
|
245
|
+
prerelease: false
|
|
246
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
247
|
+
requirements:
|
|
248
|
+
- - ">="
|
|
249
|
+
- !ruby/object:Gem::Version
|
|
250
|
+
version: '6.1'
|
|
251
|
+
- !ruby/object:Gem::Dependency
|
|
252
|
+
name: sqlite3
|
|
253
|
+
requirement: !ruby/object:Gem::Requirement
|
|
254
|
+
requirements:
|
|
255
|
+
- - ">="
|
|
256
|
+
- !ruby/object:Gem::Version
|
|
257
|
+
version: '1.4'
|
|
258
|
+
type: :development
|
|
259
|
+
prerelease: false
|
|
260
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
261
|
+
requirements:
|
|
262
|
+
- - ">="
|
|
263
|
+
- !ruby/object:Gem::Version
|
|
264
|
+
version: '1.4'
|
|
223
265
|
description: BugBunny is a lightweight RPC framework for Ruby on Rails over RabbitMQ.
|
|
224
266
|
It simulates a RESTful architecture with an intelligent router, Active Record-like
|
|
225
267
|
resources, and middleware support.
|
|
@@ -229,6 +271,7 @@ executables: []
|
|
|
229
271
|
extensions: []
|
|
230
272
|
extra_rdoc_files: []
|
|
231
273
|
files:
|
|
274
|
+
- ".rspec"
|
|
232
275
|
- AGENTS.md
|
|
233
276
|
- CHANGELOG.md
|
|
234
277
|
- CLAUDE.md
|
|
@@ -247,6 +290,7 @@ files:
|
|
|
247
290
|
- lib/bug_bunny/configuration.rb
|
|
248
291
|
- lib/bug_bunny/consumer.rb
|
|
249
292
|
- lib/bug_bunny/consumer_middleware.rb
|
|
293
|
+
- lib/bug_bunny/consumer_middleware/rails_executor.rb
|
|
250
294
|
- lib/bug_bunny/controller.rb
|
|
251
295
|
- lib/bug_bunny/drain_tracker.rb
|
|
252
296
|
- lib/bug_bunny/exception.rb
|
|
@@ -287,6 +331,9 @@ files:
|
|
|
287
331
|
- spec/integration/infrastructure_spec.rb
|
|
288
332
|
- spec/integration/publisher_confirms_spec.rb
|
|
289
333
|
- spec/integration/resource_spec.rb
|
|
334
|
+
- spec/rails/app.rb
|
|
335
|
+
- spec/rails/consumer_rails_executor_spec.rb
|
|
336
|
+
- spec/rails/rails_helper.rb
|
|
290
337
|
- spec/spec_helper.rb
|
|
291
338
|
- spec/support/bunny_mocks.rb
|
|
292
339
|
- spec/support/integration_helper.rb
|
|
@@ -294,6 +341,7 @@ files:
|
|
|
294
341
|
- spec/unit/communication_error_wrapping_spec.rb
|
|
295
342
|
- spec/unit/configuration_spec.rb
|
|
296
343
|
- spec/unit/consumer_middleware_spec.rb
|
|
344
|
+
- spec/unit/consumer_rails_executor_outside_rails_spec.rb
|
|
297
345
|
- spec/unit/consumer_spec.rb
|
|
298
346
|
- spec/unit/controller_after_action_spec.rb
|
|
299
347
|
- spec/unit/drain_tracker_spec.rb
|
|
@@ -317,7 +365,7 @@ metadata:
|
|
|
317
365
|
homepage_uri: https://github.com/gedera/bug_bunny
|
|
318
366
|
source_code_uri: https://github.com/gedera/bug_bunny
|
|
319
367
|
changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
|
|
320
|
-
documentation_uri: https://github.com/gedera/bug_bunny/blob/v5.
|
|
368
|
+
documentation_uri: https://github.com/gedera/bug_bunny/blob/v5.3.0/skill
|
|
321
369
|
post_install_message:
|
|
322
370
|
rdoc_options: []
|
|
323
371
|
require_paths:
|