bug_bunny 4.12.0 → 4.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +3 -0
- data/lib/bug_bunny/client.rb +4 -0
- data/lib/bug_bunny/configuration.rb +21 -3
- data/lib/bug_bunny/exception.rb +31 -0
- data/lib/bug_bunny/producer.rb +38 -11
- data/lib/bug_bunny/request.rb +4 -1
- data/lib/bug_bunny/version.rb +1 -1
- data/skill/SKILL.md +3 -2
- data/skill/references/client-middleware.md +8 -3
- data/skill/references/errores.md +7 -0
- data/spec/unit/configuration_spec.rb +12 -0
- data/spec/unit/producer_spec.rb +47 -9
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d1bcda002a00d8ebc93786a496f8649d43e82d58645aa2b77e3d9b72044e7e07
|
|
4
|
+
data.tar.gz: b49f4c724ae39592d6c1a1774873fc8b272cf853688d998cbfb2f22d721d3589
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b16b769d198b82f5455294bfca2c9c157f6dc7cb34a4cceedf9441d11677035a51c97b1d29e7cb59994d7d954072062919c24958ac340d9da018dded6c9e199
|
|
7
|
+
data.tar.gz: fdea4a6b6d49dd032787c520e5516045812d94afa5ba4dc8a6ff818e7e89bc6263f92877266e82a40a3789907a2aafd9727ecd2770949511b3e8584915cc87ab
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.13.0] - 2026-05-11
|
|
4
|
+
|
|
5
|
+
### Nuevas funcionalidades
|
|
6
|
+
- **NACK explícito como excepción en modo `:confirmed` (#37):** `Producer#confirmed` ahora levanta `BugBunny::PublishNacked` cuando el broker NACKea la publicación, en lugar de retornar 202 silenciosamente. La excepción expone `path` y `nacked_count` para que callers críticos (auditoría, billing, RADIUS accounting) puedan escalar a HTTP 5xx y permitir retries upstream. Configurable globalmente vía `BugBunny.configuration.nack_raise` (default `true`) y por request via `client.publish(..., nack_raise: false)`. El evento `producer.confirms_nacked` se sigue logueando para observabilidad. — @Gabriel
|
|
7
|
+
|
|
8
|
+
### Cambios de comportamiento (semi-breaking)
|
|
9
|
+
- **Default `nack_raise: true`:** Publicaciones con `confirmed: true` que reciben NACK del broker ahora levantan excepción por default. En 4.12.0, el NACK se logueaba pero retornaba 202 igualmente — comportamiento que ocultaba pérdida de mensajes desde la perspectiva del publisher. Para mantener el comportamiento previo: `BugBunny.configuration.nack_raise = false` o `nack_raise: false` per request.
|
|
10
|
+
|
|
3
11
|
## [4.12.0] - 2026-05-11
|
|
4
12
|
|
|
5
13
|
### Nuevas funcionalidades
|
data/README.md
CHANGED
|
@@ -220,6 +220,9 @@ client.publish('acct.start',
|
|
|
220
220
|
| `confirmed` | Boolean | `false` | Block until `wait_for_confirms` returns. |
|
|
221
221
|
| `mandatory` | Boolean | `false` | Broker returns the message if it cannot be routed to any queue. Requires `confirmed: true` to be useful. |
|
|
222
222
|
| `confirm_timeout` | Float | `nil` | Seconds to wait for the broker ACK. Raises `BugBunny::RequestTimeout` if exceeded. |
|
|
223
|
+
| `nack_raise` | Boolean | `nil` | Per-request override of `config.nack_raise`. When `nil`, falls back to the global flag. |
|
|
224
|
+
|
|
225
|
+
If the broker NACKs the publish (explicit rejection — disk full, internal confirm policy, etc.), the call raises `BugBunny::PublishNacked` by default. The exception exposes `path` and `nacked_count`. Critical publishers (audit, billing, RADIUS accounting) can let it bubble up to translate into HTTP 5xx so upstream systems retry. To restore the legacy "log-only" behaviour, set `BugBunny.configuration.nack_raise = false` globally or pass `nack_raise: false` per request.
|
|
223
226
|
|
|
224
227
|
Unroutable returned messages are handled by a single global callback (see `config.on_return` below). The default handler logs them as `session.broker_return` at `warn` level — nothing is dropped silently.
|
|
225
228
|
|
data/lib/bug_bunny/client.rb
CHANGED
|
@@ -93,9 +93,12 @@ module BugBunny
|
|
|
93
93
|
# @option args [Boolean] :mandatory Si `true`, el broker retorna el mensaje si no es ruteable.
|
|
94
94
|
# Para procesar retornos, configurar {BugBunny.configuration.on_return}.
|
|
95
95
|
# @option args [Float] :confirm_timeout Segundos a esperar el confirm. `nil` espera indefinidamente.
|
|
96
|
+
# @option args [Boolean] :nack_raise Override per-request del flag
|
|
97
|
+
# `BugBunny.configuration.nack_raise`. Si `nil` (default), se usa la configuración global.
|
|
96
98
|
# @yield [req] Bloque para configurar el objeto Request.
|
|
97
99
|
# @return [Hash] `{ 'status' => 202, 'body' => nil }`.
|
|
98
100
|
# @raise [BugBunny::RequestTimeout] Si `confirmed: true` y el broker no confirma a tiempo.
|
|
101
|
+
# @raise [BugBunny::PublishNacked] Si `confirmed: true`, el broker NACKea, y `nack_raise` resuelto es true.
|
|
99
102
|
def publish(url, **args)
|
|
100
103
|
send(url, **args) do |req|
|
|
101
104
|
req.delivery_mode = args[:confirmed] ? :confirmed : :publish
|
|
@@ -175,6 +178,7 @@ module BugBunny
|
|
|
175
178
|
def apply_publisher_confirms_args(req, args)
|
|
176
179
|
req.mandatory = args[:mandatory] if args.key?(:mandatory)
|
|
177
180
|
req.confirm_timeout = args[:confirm_timeout] if args.key?(:confirm_timeout)
|
|
181
|
+
req.nack_raise = args[:nack_raise] if args.key?(:nack_raise)
|
|
178
182
|
end
|
|
179
183
|
|
|
180
184
|
# Recupera o crea la Session asociada al slot de conexión dado.
|
|
@@ -153,6 +153,15 @@ module BugBunny
|
|
|
153
153
|
# }
|
|
154
154
|
attr_accessor :on_return
|
|
155
155
|
|
|
156
|
+
# @return [Boolean] Si `true` (default), {BugBunny::Producer#confirmed} levanta
|
|
157
|
+
# {BugBunny::PublishNacked} cuando el broker NACKea la publicación. Si `false`,
|
|
158
|
+
# el NACK solo se logea como `producer.confirms_nacked` y la llamada retorna
|
|
159
|
+
# `{ 'status' => 202 }` (modo legacy).
|
|
160
|
+
#
|
|
161
|
+
# El valor puede sobreescribirse por request pasando `nack_raise:` en
|
|
162
|
+
# `Client#publish`.
|
|
163
|
+
attr_accessor :nack_raise
|
|
164
|
+
|
|
156
165
|
# @!endgroup
|
|
157
166
|
|
|
158
167
|
# Inicializa la configuración con valores por defecto seguros.
|
|
@@ -194,9 +203,7 @@ module BugBunny
|
|
|
194
203
|
@queue_options = {}
|
|
195
204
|
|
|
196
205
|
@consumer_middlewares = ConsumerMiddleware::Stack.new
|
|
197
|
-
|
|
198
|
-
@on_rpc_reply = nil
|
|
199
|
-
@on_return = nil
|
|
206
|
+
init_callback_defaults
|
|
200
207
|
end
|
|
201
208
|
|
|
202
209
|
# Construye la URL de conexión AMQP basada en los atributos configurados.
|
|
@@ -223,6 +230,17 @@ module BugBunny
|
|
|
223
230
|
|
|
224
231
|
private
|
|
225
232
|
|
|
233
|
+
# Defaults para callbacks y flags relacionados con publish/RPC.
|
|
234
|
+
# Extraído de {#initialize} para mantener el ABC size dentro de los límites.
|
|
235
|
+
#
|
|
236
|
+
# @return [void]
|
|
237
|
+
def init_callback_defaults
|
|
238
|
+
@rpc_reply_headers = nil
|
|
239
|
+
@on_rpc_reply = nil
|
|
240
|
+
@on_return = nil
|
|
241
|
+
@nack_raise = true
|
|
242
|
+
end
|
|
243
|
+
|
|
226
244
|
def validate_required!(attr, value, rules)
|
|
227
245
|
return unless rules[:required]
|
|
228
246
|
return unless value.nil? || (value.is_a?(String) && value.empty?)
|
data/lib/bug_bunny/exception.rb
CHANGED
|
@@ -19,6 +19,37 @@ module BugBunny
|
|
|
19
19
|
# Protege contra vulnerabilidades de RCE validando la herencia de las clases enrutadas.
|
|
20
20
|
class SecurityError < Error; end
|
|
21
21
|
|
|
22
|
+
# Error lanzado cuando el broker responde NACK a una publicación en modo `:confirmed`.
|
|
23
|
+
#
|
|
24
|
+
# Un NACK significa que el broker rechazó explícitamente el mensaje (ej: política de
|
|
25
|
+
# confirms interna, disk full, replicación insuficiente). El mensaje no fue aceptado
|
|
26
|
+
# y se considera no entregado — equivalente a un fallo de transporte desde la
|
|
27
|
+
# perspectiva del publisher.
|
|
28
|
+
#
|
|
29
|
+
# Se levanta por default desde {BugBunny::Producer#confirmed}. Para opt-out,
|
|
30
|
+
# configurar `BugBunny.configuration.nack_raise = false` o pasar
|
|
31
|
+
# `nack_raise: false` por request.
|
|
32
|
+
#
|
|
33
|
+
# @example
|
|
34
|
+
# rescue BugBunny::PublishNacked => e
|
|
35
|
+
# e.path # => 'acct.start'
|
|
36
|
+
# e.nacked_count # => 1
|
|
37
|
+
class PublishNacked < Error
|
|
38
|
+
# @return [String] La ruta del request cuyo publish fue NACKeado.
|
|
39
|
+
attr_reader :path
|
|
40
|
+
|
|
41
|
+
# @return [Integer] Cantidad de mensajes NACKeados según `Bunny::Channel#nacked_set`.
|
|
42
|
+
attr_reader :nacked_count
|
|
43
|
+
|
|
44
|
+
# @param path [String] Ruta lógica del request (ej: 'acct.start').
|
|
45
|
+
# @param nacked_count [Integer] Cantidad de NACKs reportados por el broker.
|
|
46
|
+
def initialize(path:, nacked_count:)
|
|
47
|
+
@path = path
|
|
48
|
+
@nacked_count = nacked_count
|
|
49
|
+
super("broker NACK on path=#{path} (nacked=#{nacked_count})")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
22
53
|
# ==========================================
|
|
23
54
|
# Categoría: Errores del Cliente (4xx)
|
|
24
55
|
# ==========================================
|
data/lib/bug_bunny/producer.rb
CHANGED
|
@@ -52,14 +52,17 @@ module BugBunny
|
|
|
52
52
|
# A diferencia de {#rpc} (que espera la respuesta de un Consumer remoto), aquí solo se
|
|
53
53
|
# espera el ACK del propio broker — no hay round-trip al servicio destino.
|
|
54
54
|
#
|
|
55
|
-
# @param request [BugBunny::Request] Request con `mandatory`, `confirm_timeout
|
|
55
|
+
# @param request [BugBunny::Request] Request con `mandatory`, `confirm_timeout`, `nack_raise`
|
|
56
|
+
# y/o `on_return` opcionales.
|
|
56
57
|
# @return [Hash] `{ 'status' => 202, 'body' => nil }` si el broker confirmó la recepción.
|
|
57
58
|
# @raise [BugBunny::RequestTimeout] Si el broker no confirma dentro de `confirm_timeout` segundos.
|
|
59
|
+
# @raise [BugBunny::PublishNacked] Si el broker NACKea la publicación y `nack_raise` está activo
|
|
60
|
+
# (default `true` — ver {BugBunny::Configuration#nack_raise}).
|
|
58
61
|
# @raise [BugBunny::CommunicationError] Si el canal AMQP falla durante la publicación o confirm.
|
|
59
62
|
def confirmed(request)
|
|
60
63
|
publish_message(request)
|
|
61
|
-
wait_for_confirms!(request)
|
|
62
|
-
|
|
64
|
+
acked = wait_for_confirms!(request)
|
|
65
|
+
handle_confirm_result(request, acked)
|
|
63
66
|
{ 'status' => 202, 'body' => nil }
|
|
64
67
|
rescue BugBunny::Error
|
|
65
68
|
raise
|
|
@@ -154,20 +157,44 @@ module BugBunny
|
|
|
154
157
|
"Timeout (#{timeout}s) waiting for publisher confirms: #{request.path}"
|
|
155
158
|
end
|
|
156
159
|
|
|
157
|
-
#
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
+
# Procesa el resultado de `wait_for_confirms`.
|
|
161
|
+
#
|
|
162
|
+
# Si el broker NACKeó (`acked == false`), logea el evento `producer.confirms_nacked`
|
|
163
|
+
# y opcionalmente levanta {BugBunny::PublishNacked} según el flag `nack_raise`
|
|
164
|
+
# resuelto desde el request o la configuración global.
|
|
160
165
|
#
|
|
161
166
|
# @param request [BugBunny::Request]
|
|
167
|
+
# @param acked [Boolean] Resultado de `wait_for_confirms` (true = todos los confirms positivos).
|
|
168
|
+
# @raise [BugBunny::PublishNacked] Si hay NACK y `nack_raise?` es true.
|
|
162
169
|
# @return [void]
|
|
163
|
-
def
|
|
170
|
+
def handle_confirm_result(request, acked)
|
|
171
|
+
return if acked
|
|
172
|
+
|
|
173
|
+
count = nacked_count
|
|
174
|
+
safe_log(:warn, 'producer.confirms_nacked', count: count, path: request.path)
|
|
175
|
+
return unless nack_raise?(request)
|
|
176
|
+
|
|
177
|
+
raise BugBunny::PublishNacked.new(path: request.path, nacked_count: count)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Cuenta los delivery tags NACKeados reportados por el canal.
|
|
181
|
+
#
|
|
182
|
+
# @return [Integer]
|
|
183
|
+
def nacked_count
|
|
164
184
|
ch = @session.channel
|
|
165
|
-
return unless ch.respond_to?(:nacked_set)
|
|
185
|
+
return 0 unless ch.respond_to?(:nacked_set)
|
|
186
|
+
|
|
187
|
+
ch.nacked_set&.size || 0
|
|
188
|
+
end
|
|
166
189
|
|
|
167
|
-
|
|
168
|
-
|
|
190
|
+
# Resuelve el flag `nack_raise` con prioridad request > configuración global.
|
|
191
|
+
#
|
|
192
|
+
# @param request [BugBunny::Request]
|
|
193
|
+
# @return [Boolean]
|
|
194
|
+
def nack_raise?(request)
|
|
195
|
+
return request.nack_raise unless request.nack_raise.nil?
|
|
169
196
|
|
|
170
|
-
|
|
197
|
+
BugBunny.configuration.nack_raise
|
|
171
198
|
end
|
|
172
199
|
|
|
173
200
|
# Registra la petición en el log calculando las opciones de infraestructura.
|
data/lib/bug_bunny/request.rb
CHANGED
|
@@ -27,6 +27,8 @@ module BugBunny
|
|
|
27
27
|
# vía {BugBunny.configuration.on_return}.
|
|
28
28
|
# @attr confirm_timeout [Float, nil] Segundos máximos a esperar el `wait_for_confirms`.
|
|
29
29
|
# `nil` espera indefinidamente.
|
|
30
|
+
# @attr nack_raise [Boolean, nil] Override per-request del flag global
|
|
31
|
+
# `BugBunny.configuration.nack_raise`. `nil` (default) delega a la configuración global.
|
|
30
32
|
class Request
|
|
31
33
|
attr_accessor :body, :headers, :params, :path, :method, :exchange, :exchange_type, :routing_key, :timeout,
|
|
32
34
|
:delivery_mode, :queue_options
|
|
@@ -35,7 +37,7 @@ module BugBunny
|
|
|
35
37
|
attr_accessor :exchange_options
|
|
36
38
|
|
|
37
39
|
# Publisher Confirms (delivery_mode = :confirmed)
|
|
38
|
-
attr_accessor :mandatory, :confirm_timeout
|
|
40
|
+
attr_accessor :mandatory, :confirm_timeout, :nack_raise
|
|
39
41
|
|
|
40
42
|
# Metadatos AMQP Estándar
|
|
41
43
|
attr_accessor :app_id, :content_type, :content_encoding, :priority,
|
|
@@ -63,6 +65,7 @@ module BugBunny
|
|
|
63
65
|
# Defaults para Publisher Confirms (modo :confirmed)
|
|
64
66
|
@mandatory = false
|
|
65
67
|
@confirm_timeout = nil
|
|
68
|
+
@nack_raise = nil
|
|
66
69
|
end
|
|
67
70
|
|
|
68
71
|
# Combina el path con los params como query string.
|
data/lib/bug_bunny/version.rb
CHANGED
data/skill/SKILL.md
CHANGED
|
@@ -15,7 +15,7 @@ Skill de conocimiento completo sobre BugBunny. Consultame para cualquier pregunt
|
|
|
15
15
|
**Session** — `BugBunny::Session` envuelve canales de Bunny con thread-safety y double-checked locking.
|
|
16
16
|
**RPC** — Patrón síncrono que usa la pseudo-cola `amq.rabbitmq.reply-to` para respuestas sin crear queues temporales.
|
|
17
17
|
**Fire-and-Forget** — Patrón asíncrono donde el producer publica y continúa sin esperar respuesta. Retorna `{ 'status' => 202 }`.
|
|
18
|
-
**Publisher Confirms** — Extensión de RabbitMQ que confirma al publisher que el broker recibió el mensaje. BugBunny lo expone como `confirmed: true` en `Client#publish`: el publish bloquea hasta `wait_for_confirms`. NACK
|
|
18
|
+
**Publisher Confirms** — Extensión de RabbitMQ que confirma al publisher que el broker recibió el mensaje. BugBunny lo expone como `confirmed: true` en `Client#publish`: el publish bloquea hasta `wait_for_confirms`. NACK del broker (rechazo explícito) levanta `BugBunny::PublishNacked` por default; configurable via `config.nack_raise = false` o `nack_raise: false` per request para volver al modo log-only.
|
|
19
19
|
**Mandatory** — Flag de `basic.publish` que pide al broker retornar el mensaje al publisher si no es ruteable a ninguna cola. Se procesa via `basic.return` (no es respuesta del request).
|
|
20
20
|
**basic.return** — Evento asincrónico que Bunny dispatcha por exchange (`Bunny::Exchange#on_return`). BugBunny registra un handler único por nombre de exchange al resolverlo en `Session#exchange` y lo delega a `Configuration#on_return` o al default que logea.
|
|
21
21
|
**Resource** — ORM tipo ActiveRecord que mapea operaciones CRUD a llamadas AMQP.
|
|
@@ -202,7 +202,8 @@ client.publish('events', method: :post, body: { type: 'order.placed' })
|
|
|
202
202
|
client.publish('acct.start', exchange: 'acct_x', body: payload,
|
|
203
203
|
confirmed: true, mandatory: true, confirm_timeout: 0.5)
|
|
204
204
|
# → { 'status' => 202, 'body' => nil } # broker ACK confirmado
|
|
205
|
-
# Si broker NACK: logea `producer.confirms_nacked` y
|
|
205
|
+
# Si broker NACK: logea `producer.confirms_nacked` y raise BugBunny::PublishNacked
|
|
206
|
+
# (default — opt-out con config.nack_raise = false o nack_raise: false per request).
|
|
206
207
|
# Si timeout: raise BugBunny::RequestTimeout.
|
|
207
208
|
# Si mandatory + no ruteable: dispara `Configuration#on_return` (default: logea `session.broker_return`).
|
|
208
209
|
```
|
|
@@ -46,6 +46,7 @@ end
|
|
|
46
46
|
| `confirmed` | Boolean | `false` | En `Client#publish`, flipea `delivery_mode` a `:confirmed`. Bloquea hasta `wait_for_confirms`. |
|
|
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
|
+
| `nack_raise` | Boolean | `nil` | Override per-request de `config.nack_raise`. `nil` = usa flag global. |
|
|
49
50
|
|
|
50
51
|
## Producer (bajo nivel)
|
|
51
52
|
|
|
@@ -68,7 +69,7 @@ El `Producer` es usado internamente por el `Client`. Implementa tres patrones de
|
|
|
68
69
|
|
|
69
70
|
- Publica y bloquea hasta `channel.wait_for_confirms` del broker.
|
|
70
71
|
- Bunny 2.x **no soporta timeout** nativo en `wait_for_confirms` — BugBunny envuelve la llamada en un hilo auxiliar y usa `Concurrent::IVar#value(timeout)` como reloj. Si `confirm_timeout` expira → `BugBunny::RequestTimeout`.
|
|
71
|
-
-
|
|
72
|
+
- Si `wait_for_confirms` devuelve `false` (broker NACKea), se logea `producer.confirms_nacked` con `count` y `path`. Por default (`config.nack_raise = true`) levanta `BugBunny::PublishNacked` con `path` y `nacked_count`. Para opt-out: `config.nack_raise = false` o pasar `nack_raise: false` per request — en ese caso solo logea y retorna 202.
|
|
72
73
|
- Si `mandatory: true` y el mensaje no es ruteable, el broker dispara `basic.return`. El handler se atacha vía `Bunny::Exchange#on_return` en `Session#exchange` la primera vez que se resuelve cada exchange (cacheado por nombre, una sola vez por canal) y delega a `Configuration#on_return` o al logger por default.
|
|
73
74
|
- Errores del canal se envuelven en `BugBunny::CommunicationError`; errores `BugBunny::Error` pre-existentes se propagan sin envolver.
|
|
74
75
|
|
|
@@ -164,6 +165,7 @@ req.timestamp # Time.now.to_i
|
|
|
164
165
|
req.content_type # 'application/json'
|
|
165
166
|
req.mandatory # Boolean — solo modo :confirmed
|
|
166
167
|
req.confirm_timeout # Float|nil — solo modo :confirmed
|
|
168
|
+
req.nack_raise # Boolean|nil — override per-request de config.nack_raise (solo modo :confirmed)
|
|
167
169
|
```
|
|
168
170
|
|
|
169
171
|
Cuando `mandatory == true`, `Request#amqp_options` inyecta `mandatory: true` en el hash que va a `basic_publish`.
|
|
@@ -182,7 +184,10 @@ Producer#confirmed
|
|
|
182
184
|
│
|
|
183
185
|
├──> wait_for_confirms! (espera ACK del broker, con timeout opcional)
|
|
184
186
|
│
|
|
185
|
-
└──>
|
|
187
|
+
└──> handle_confirm_result
|
|
188
|
+
├─ acked == true → return { status: 202 }
|
|
189
|
+
└─ acked == false → log WARN producer.confirms_nacked
|
|
190
|
+
└─ raise BugBunny::PublishNacked (si config.nack_raise || req.nack_raise)
|
|
186
191
|
|
|
187
192
|
Asíncronamente, si el broker no pudo rutear:
|
|
188
193
|
broker ──basic.return──> Exchange#on_return ──> Session handler ──> Configuration#on_return
|
|
@@ -221,7 +226,7 @@ Excepciones del callback se capturan y se logean como `session.on_return_failed`
|
|
|
221
226
|
| Auditoría, billing, eventos críticos | `:confirmed` (con `mandatory: true` si es ruteable) |
|
|
222
227
|
| Request-response síncrono | `:rpc` |
|
|
223
228
|
|
|
224
|
-
`:confirmed` cuesta un round-trip al broker pero **no** al consumer remoto — más rápido que RPC, con garantía de entrega al broker. NACK del broker es raro (típicamente por confirm policies internas)
|
|
229
|
+
`:confirmed` cuesta un round-trip al broker pero **no** al consumer remoto — más rápido que RPC, con garantía de entrega al broker. NACK del broker es raro (típicamente por confirm policies internas, disk full, replicación insuficiente) pero **sí** implica que el mensaje no fue aceptado. Por default BugBunny levanta `BugBunny::PublishNacked` para que el caller pueda escalar (ej: convertir a HTTP 503 y dejar que el sistema upstream reintente). Opt-out con `config.nack_raise = false` o `nack_raise: false` per request.
|
|
225
230
|
|
|
226
231
|
## Cascada de Configuración (3 niveles)
|
|
227
232
|
|
data/skill/references/errores.md
CHANGED
|
@@ -8,6 +8,7 @@ StandardError
|
|
|
8
8
|
├── BugBunny::CommunicationError
|
|
9
9
|
├── BugBunny::ConfigurationError
|
|
10
10
|
├── BugBunny::SecurityError
|
|
11
|
+
├── BugBunny::PublishNacked
|
|
11
12
|
├── BugBunny::ClientError (4xx)
|
|
12
13
|
│ ├── BugBunny::BadRequest (400)
|
|
13
14
|
│ ├── BugBunny::NotFound (404)
|
|
@@ -37,6 +38,12 @@ StandardError
|
|
|
37
38
|
**Cuándo:** El consumer resuelve la clase pero falla la validación `is_a?(BugBunny::Controller)`.
|
|
38
39
|
**Resolución:** Verificar que el controlador herede de `BugBunny::Controller` y que `config.controller_namespace` sea correcto.
|
|
39
40
|
|
|
41
|
+
### BugBunny::PublishNacked
|
|
42
|
+
**Causa:** El broker NACKea explícitamente una publicación en modo `:confirmed` (`Client#publish(..., confirmed: true)`). Indica que el mensaje no fue aceptado — disk full, replicación insuficiente, confirm policy interna, etc.
|
|
43
|
+
**Cuándo:** `Producer#confirmed` detecta `wait_for_confirms == false`. Se levanta por default (`config.nack_raise = true`).
|
|
44
|
+
**Atributos:** `path` (ruta del request) y `nacked_count` (cantidad de delivery tags NACKeados).
|
|
45
|
+
**Resolución:** Para casos críticos (auditoría, billing, RADIUS accounting), dejar que la excepción bubble para que el caller upstream reintente (ej: HTTP 503 → retry). Para tolerar NACKs (eventos best-effort), `BugBunny.configuration.nack_raise = false` global o `nack_raise: false` per request — en ese caso solo se logea `producer.confirms_nacked`.
|
|
46
|
+
|
|
40
47
|
## Errores de Cliente (4xx)
|
|
41
48
|
|
|
42
49
|
### BugBunny::BadRequest (400)
|
|
@@ -162,6 +162,18 @@ RSpec.describe BugBunny::Configuration do
|
|
|
162
162
|
end
|
|
163
163
|
end
|
|
164
164
|
|
|
165
|
+
describe 'nack_raise flag' do
|
|
166
|
+
it 'tiene default true (raise PublishNacked en NACK)' do
|
|
167
|
+
expect(BugBunny::Configuration.new.nack_raise).to be(true)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it 'acepta false para opt-out (modo legacy)' do
|
|
171
|
+
configure_with(nack_raise: false)
|
|
172
|
+
|
|
173
|
+
expect(BugBunny.configuration.nack_raise).to be(false)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
165
177
|
describe '.validate! directamente' do
|
|
166
178
|
it 'es invocable directamente sobre la instancia' do
|
|
167
179
|
config = BugBunny::Configuration.new
|
data/spec/unit/producer_spec.rb
CHANGED
|
@@ -245,21 +245,59 @@ RSpec.describe BugBunny::Producer do
|
|
|
245
245
|
)
|
|
246
246
|
end
|
|
247
247
|
|
|
248
|
-
it 'logea producer.confirms_nacked cuando
|
|
249
|
-
allow(mock_channel).to receive(:nacked_set).and_return(Set.new([1, 2]))
|
|
250
|
-
|
|
248
|
+
it 'NO logea producer.confirms_nacked cuando wait_for_confirms devuelve true' do
|
|
251
249
|
confirmed_producer.confirmed(build_request)
|
|
252
250
|
|
|
253
251
|
nack_event = logged_events.find { |e| e[:event] == 'producer.confirms_nacked' }
|
|
254
|
-
expect(nack_event).
|
|
255
|
-
expect(nack_event[:kwargs]).to include(count: 2, path: 'acct.start')
|
|
252
|
+
expect(nack_event).to be_nil
|
|
256
253
|
end
|
|
257
254
|
|
|
258
|
-
|
|
259
|
-
|
|
255
|
+
context 'cuando el broker NACKea (wait_for_confirms devuelve false)' do
|
|
256
|
+
before do
|
|
257
|
+
allow(mock_channel).to receive(:wait_for_confirms).and_return(false)
|
|
258
|
+
allow(mock_channel).to receive(:nacked_set).and_return(Set.new([1, 2]))
|
|
259
|
+
end
|
|
260
260
|
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
it 'levanta BugBunny::PublishNacked por default (config.nack_raise = true)' do
|
|
262
|
+
expect { confirmed_producer.confirmed(build_request) }.to raise_error(BugBunny::PublishNacked) do |err|
|
|
263
|
+
expect(err.path).to eq('acct.start')
|
|
264
|
+
expect(err.nacked_count).to eq(2)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
it 'logea producer.confirms_nacked antes de levantar' do
|
|
269
|
+
expect { confirmed_producer.confirmed(build_request) }.to raise_error(BugBunny::PublishNacked)
|
|
270
|
+
|
|
271
|
+
nack_event = logged_events.find { |e| e[:event] == 'producer.confirms_nacked' }
|
|
272
|
+
expect(nack_event).not_to be_nil
|
|
273
|
+
expect(nack_event[:kwargs]).to include(count: 2, path: 'acct.start')
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
it 'no levanta si el request override `nack_raise = false`' do
|
|
277
|
+
req = build_request
|
|
278
|
+
req.nack_raise = false
|
|
279
|
+
|
|
280
|
+
result = confirmed_producer.confirmed(req)
|
|
281
|
+
|
|
282
|
+
expect(result).to eq('status' => 202, 'body' => nil)
|
|
283
|
+
expect(logged_events.find { |e| e[:event] == 'producer.confirms_nacked' }).not_to be_nil
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
it 'no levanta si la configuración global tiene `nack_raise = false`' do
|
|
287
|
+
allow(BugBunny.configuration).to receive(:nack_raise).and_return(false)
|
|
288
|
+
|
|
289
|
+
result = confirmed_producer.confirmed(build_request)
|
|
290
|
+
|
|
291
|
+
expect(result).to eq('status' => 202, 'body' => nil)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it 'el override per-request gana sobre la configuración global' do
|
|
295
|
+
allow(BugBunny.configuration).to receive(:nack_raise).and_return(false)
|
|
296
|
+
req = build_request
|
|
297
|
+
req.nack_raise = true
|
|
298
|
+
|
|
299
|
+
expect { confirmed_producer.confirmed(req) }.to raise_error(BugBunny::PublishNacked)
|
|
300
|
+
end
|
|
263
301
|
end
|
|
264
302
|
|
|
265
303
|
it 'levanta BugBunny::RequestTimeout si wait_for_confirms excede confirm_timeout' do
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bug_bunny
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gabix
|
|
@@ -303,7 +303,7 @@ metadata:
|
|
|
303
303
|
homepage_uri: https://github.com/gedera/bug_bunny
|
|
304
304
|
source_code_uri: https://github.com/gedera/bug_bunny
|
|
305
305
|
changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
|
|
306
|
-
documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.
|
|
306
|
+
documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.13.0/skill
|
|
307
307
|
post_install_message:
|
|
308
308
|
rdoc_options: []
|
|
309
309
|
require_paths:
|