bug_bunny 4.12.0 → 4.14.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 +22 -0
- data/README.md +19 -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 +88 -18
- data/lib/bug_bunny/request.rb +4 -1
- data/lib/bug_bunny/version.rb +1 -1
- data/skill/SKILL.md +34 -2
- data/skill/references/client-middleware.md +11 -3
- data/skill/references/errores.md +7 -0
- data/spec/unit/configuration_spec.rb +12 -0
- data/spec/unit/producer_spec.rb +105 -8
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6081a471e20278bc190a7d7517b69cf6fe6d852293bca353974c5ba1cbe4e746
|
|
4
|
+
data.tar.gz: 2e1b952c9627b7f3f1f9ab1e2af25af49ca0422ff59f062ac7499c14265d6a57
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6cf7de01b98c925c8f02de1e09aafc0113f24f93145498ace1616445deecf6319bde3464d9afcd89b5b461904fbe878d4bfb24ea3a2a310149a32f6f2b06f4d0
|
|
7
|
+
data.tar.gz: 2560004c6036b238456fe9c9af9bf478dde67b1ff07240fdac942ecab303467d1db04f3029c2579463e5017b7c9f25ad2e5f1c5ef2c999f86bcde0bedb42eb67
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.14.0] - 2026-05-12
|
|
4
|
+
|
|
5
|
+
### Nuevas funcionalidades
|
|
6
|
+
- **Duraciones medidas internamente en el Producer:** BugBunny ahora emite `duration_s` automáticamente en los eventos del publisher siguiendo las [OpenTelemetry metric semantic conventions](https://opentelemetry.io/docs/specs/semconv/general/metrics/) (`Float` en segundos). El código de aplicación ya no necesita envolver `client.publish` con `Process.clock_gettime`. — @Gabriel
|
|
7
|
+
- `producer.published` (INFO): `duration_s` del `basic_publish` (TCP enqueue al broker, sin esperar ACK).
|
|
8
|
+
- `producer.confirmed` (INFO): tres duraciones desglosadas — `publish_duration_s`, `confirm_duration_s` (espera de `wait_for_confirms`) y `duration_s` total. Útil para distinguir latencia de red vs latencia del confirm policy del broker.
|
|
9
|
+
- `producer.rpc_response_received`: ahora incluye `duration_s` con el round-trip RPC completo (publish + procesamiento remoto + reply).
|
|
10
|
+
|
|
11
|
+
### Cambios de comportamiento
|
|
12
|
+
- **`producer.rpc_response_received` promovido de DEBUG a INFO.** No es breaking de API pero aumenta el volumen de logs en clientes RPC. Si el cambio impacta tu pipeline de observabilidad, filtralo por nivel.
|
|
13
|
+
|
|
14
|
+
### Documentación
|
|
15
|
+
- README + `skill/SKILL.md` + `skill/references/client-middleware.md` actualizados con el catálogo completo de eventos de log emitidos por la gema y la tabla de qué mide cada `duration_s`. Mensaje explícito en ambas audiencias (humana + agente) advirtiendo no duplicar la medición en código de aplicación.
|
|
16
|
+
|
|
17
|
+
## [4.13.0] - 2026-05-11
|
|
18
|
+
|
|
19
|
+
### Nuevas funcionalidades
|
|
20
|
+
- **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
|
|
21
|
+
|
|
22
|
+
### Cambios de comportamiento (semi-breaking)
|
|
23
|
+
- **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.
|
|
24
|
+
|
|
3
25
|
## [4.12.0] - 2026-05-11
|
|
4
26
|
|
|
5
27
|
### 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
|
|
|
@@ -249,11 +252,27 @@ BugBunny implementa de forma nativa las [OpenTelemetry semantic conventions for
|
|
|
249
252
|
Todos los eventos internos se emiten como logs `key=value` compatibles con Datadog, CloudWatch, ELK y ExisRay.
|
|
250
253
|
|
|
251
254
|
```
|
|
255
|
+
component=bug_bunny event=producer.publish method=POST path=acct/publish messaging_destination_name=acct_x messaging_routing_key=acct.start.42
|
|
256
|
+
component=bug_bunny event=producer.published method=POST path=acct/publish routing_key=acct.start.42 messaging_message_id=corr-1 duration_s=0.000812
|
|
257
|
+
component=bug_bunny event=producer.confirmed method=POST path=acct/publish routing_key=acct.start.42 publish_duration_s=0.000812 confirm_duration_s=0.012 duration_s=0.013
|
|
258
|
+
component=bug_bunny event=producer.rpc_response_received method=GET path=users/42 duration_s=0.034 messaging_operation=receive
|
|
252
259
|
component=bug_bunny event=consumer.message_processed status=200 duration_s=0.012 messaging_operation=process controller=NodesController action=show
|
|
253
260
|
component=bug_bunny event=consumer.execution_error error_class=RuntimeError error_message="..." duration_s=0.003
|
|
254
261
|
component=bug_bunny event=consumer.connection_error attempt_count=2 retry_in_s=10 error_message="..."
|
|
255
262
|
```
|
|
256
263
|
|
|
264
|
+
### Duraciones medidas internamente
|
|
265
|
+
|
|
266
|
+
BugBunny mide y emite duraciones automáticamente — **no es necesario envolver llamadas a `client.publish` con `Process.clock_gettime` en el código de aplicación**. Las unidades siguen las [OpenTelemetry metric semantic conventions](https://opentelemetry.io/docs/specs/semconv/general/metrics/) (`s`, segundos como `Float`).
|
|
267
|
+
|
|
268
|
+
| Evento | Duración | Mide |
|
|
269
|
+
|---|---|---|
|
|
270
|
+
| `producer.published` | `duration_s` | Solo el `basic_publish` (TCP enqueue al broker). |
|
|
271
|
+
| `producer.confirmed` | `publish_duration_s` + `confirm_duration_s` + `duration_s` (total) | Publish + espera de ACK del broker. |
|
|
272
|
+
| `producer.rpc_response_received` | `duration_s` | Round-trip RPC completo (publish + procesamiento remoto + reply). |
|
|
273
|
+
| `consumer.message_processed` | `duration_s` | Procesamiento del mensaje (router + controller + reply). |
|
|
274
|
+
| `consumer.execution_error` | `duration_s` | Tiempo transcurrido hasta el error. |
|
|
275
|
+
|
|
257
276
|
Las claves sensibles (`password`, `token`, `secret`, `api_key`, `authorization`, etc.) se filtran automáticamente a `[FILTERED]` en toda la salida de logs.
|
|
258
277
|
|
|
259
278
|
---
|
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,24 @@ 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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
started_at = monotonic_now
|
|
64
|
+
publish_duration = publish_message(request)
|
|
65
|
+
|
|
66
|
+
confirm_started_at = monotonic_now
|
|
67
|
+
acked = wait_for_confirms!(request)
|
|
68
|
+
confirm_duration = duration_s(confirm_started_at)
|
|
69
|
+
|
|
70
|
+
handle_confirm_result(request, acked)
|
|
71
|
+
log_confirmed(request, publish_duration, confirm_duration, started_at)
|
|
72
|
+
|
|
63
73
|
{ 'status' => 202, 'body' => nil }
|
|
64
74
|
rescue BugBunny::Error
|
|
65
75
|
raise
|
|
@@ -87,6 +97,7 @@ module BugBunny
|
|
|
87
97
|
future = Concurrent::IVar.new
|
|
88
98
|
@pending_requests[cid] = future
|
|
89
99
|
|
|
100
|
+
started_at = monotonic_now
|
|
90
101
|
begin
|
|
91
102
|
fire(request)
|
|
92
103
|
|
|
@@ -98,11 +109,7 @@ module BugBunny
|
|
|
98
109
|
raise BugBunny::RequestTimeout, "Timeout waiting for RPC: #{request.path} [#{request.method}]" if result.nil?
|
|
99
110
|
|
|
100
111
|
BugBunny.configuration.on_rpc_reply&.call(result[:headers])
|
|
101
|
-
|
|
102
|
-
safe_log(:debug, 'producer.rpc_response_received',
|
|
103
|
-
messaging_system: 'rabbitmq', messaging_operation: 'receive', messaging_message_id: cid,
|
|
104
|
-
response_body: result[:body]&.truncate(500),
|
|
105
|
-
response_headers: result[:headers]&.to_json&.truncate(300))
|
|
112
|
+
log_rpc_response_received(request, cid, result, started_at)
|
|
106
113
|
|
|
107
114
|
parse_response(result[:body])
|
|
108
115
|
ensure
|
|
@@ -116,8 +123,11 @@ module BugBunny
|
|
|
116
123
|
# Resuelve exchange, serializa payload, logea y publica el mensaje.
|
|
117
124
|
# Compartido por {#fire} y {#confirmed}.
|
|
118
125
|
#
|
|
126
|
+
# Emite `producer.publish` antes y `producer.published` después con `duration_s`
|
|
127
|
+
# midiendo solo el `basic_publish` (TCP enqueue al broker, sin esperar ACK).
|
|
128
|
+
#
|
|
119
129
|
# @param request [BugBunny::Request]
|
|
120
|
-
# @return [
|
|
130
|
+
# @return [Float] duración del publish en segundos.
|
|
121
131
|
def publish_message(request)
|
|
122
132
|
x = @session.exchange(
|
|
123
133
|
name: request.exchange,
|
|
@@ -126,7 +136,43 @@ module BugBunny
|
|
|
126
136
|
)
|
|
127
137
|
payload = serialize_message(request.body)
|
|
128
138
|
log_request(request, payload)
|
|
139
|
+
|
|
140
|
+
started_at = monotonic_now
|
|
129
141
|
x.publish(payload, request.amqp_options.merge(routing_key: request.final_routing_key))
|
|
142
|
+
publish_duration = duration_s(started_at)
|
|
143
|
+
|
|
144
|
+
log_published(request, publish_duration)
|
|
145
|
+
publish_duration
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @api private
|
|
149
|
+
def log_published(request, publish_duration_s)
|
|
150
|
+
safe_log(:info, 'producer.published',
|
|
151
|
+
method: request.method.to_s.upcase, path: request.path,
|
|
152
|
+
routing_key: request.final_routing_key,
|
|
153
|
+
messaging_message_id: request.correlation_id,
|
|
154
|
+
duration_s: publish_duration_s)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# @api private
|
|
158
|
+
def log_rpc_response_received(request, cid, result, started_at)
|
|
159
|
+
safe_log(:info, 'producer.rpc_response_received',
|
|
160
|
+
method: request.method.to_s.upcase, path: request.path,
|
|
161
|
+
messaging_system: 'rabbitmq', messaging_operation: 'receive', messaging_message_id: cid,
|
|
162
|
+
duration_s: duration_s(started_at),
|
|
163
|
+
response_body: result[:body]&.truncate(500),
|
|
164
|
+
response_headers: result[:headers]&.to_json&.truncate(300))
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# @api private
|
|
168
|
+
def log_confirmed(request, publish_duration_s, confirm_duration_s, started_at)
|
|
169
|
+
safe_log(:info, 'producer.confirmed',
|
|
170
|
+
method: request.method.to_s.upcase, path: request.path,
|
|
171
|
+
routing_key: request.final_routing_key,
|
|
172
|
+
messaging_message_id: request.correlation_id,
|
|
173
|
+
publish_duration_s: publish_duration_s,
|
|
174
|
+
confirm_duration_s: confirm_duration_s,
|
|
175
|
+
duration_s: duration_s(started_at))
|
|
130
176
|
end
|
|
131
177
|
|
|
132
178
|
# Espera la confirmación del broker con timeout opcional.
|
|
@@ -154,20 +200,44 @@ module BugBunny
|
|
|
154
200
|
"Timeout (#{timeout}s) waiting for publisher confirms: #{request.path}"
|
|
155
201
|
end
|
|
156
202
|
|
|
157
|
-
#
|
|
158
|
-
#
|
|
159
|
-
#
|
|
203
|
+
# Procesa el resultado de `wait_for_confirms`.
|
|
204
|
+
#
|
|
205
|
+
# Si el broker NACKeó (`acked == false`), logea el evento `producer.confirms_nacked`
|
|
206
|
+
# y opcionalmente levanta {BugBunny::PublishNacked} según el flag `nack_raise`
|
|
207
|
+
# resuelto desde el request o la configuración global.
|
|
160
208
|
#
|
|
161
209
|
# @param request [BugBunny::Request]
|
|
210
|
+
# @param acked [Boolean] Resultado de `wait_for_confirms` (true = todos los confirms positivos).
|
|
211
|
+
# @raise [BugBunny::PublishNacked] Si hay NACK y `nack_raise?` es true.
|
|
162
212
|
# @return [void]
|
|
163
|
-
def
|
|
213
|
+
def handle_confirm_result(request, acked)
|
|
214
|
+
return if acked
|
|
215
|
+
|
|
216
|
+
count = nacked_count
|
|
217
|
+
safe_log(:warn, 'producer.confirms_nacked', count: count, path: request.path)
|
|
218
|
+
return unless nack_raise?(request)
|
|
219
|
+
|
|
220
|
+
raise BugBunny::PublishNacked.new(path: request.path, nacked_count: count)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Cuenta los delivery tags NACKeados reportados por el canal.
|
|
224
|
+
#
|
|
225
|
+
# @return [Integer]
|
|
226
|
+
def nacked_count
|
|
164
227
|
ch = @session.channel
|
|
165
|
-
return unless ch.respond_to?(:nacked_set)
|
|
228
|
+
return 0 unless ch.respond_to?(:nacked_set)
|
|
229
|
+
|
|
230
|
+
ch.nacked_set&.size || 0
|
|
231
|
+
end
|
|
166
232
|
|
|
167
|
-
|
|
168
|
-
|
|
233
|
+
# Resuelve el flag `nack_raise` con prioridad request > configuración global.
|
|
234
|
+
#
|
|
235
|
+
# @param request [BugBunny::Request]
|
|
236
|
+
# @return [Boolean]
|
|
237
|
+
def nack_raise?(request)
|
|
238
|
+
return request.nack_raise unless request.nack_raise.nil?
|
|
169
239
|
|
|
170
|
-
|
|
240
|
+
BugBunny.configuration.nack_raise
|
|
171
241
|
end
|
|
172
242
|
|
|
173
243
|
# 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.
|
|
@@ -97,6 +97,37 @@ BugBunny implementa las [OpenTelemetry semantic conventions for messaging](https
|
|
|
97
97
|
- **Consumer:** Extrae los campos de los logs estructurados sin mutar los headers originales. Los eventos `consumer.message_received` y `consumer.message_processed` incluyen estos campos automáticamente.
|
|
98
98
|
- **RPC Reply:** El consumer inyecta los mismos campos en el reply para cerrar el ciclo de traza del lado del cliente.
|
|
99
99
|
|
|
100
|
+
### Eventos de log y duraciones internas
|
|
101
|
+
|
|
102
|
+
BugBunny mide y emite duraciones automáticamente. **No envolver llamadas a `client.publish` con `Process.clock_gettime` en código de aplicación** — duplica el trabajo. Las duraciones siguen las [OpenTelemetry metric semantic conventions](https://opentelemetry.io/docs/specs/semconv/general/metrics/) (`duration_s` como `Float` en segundos).
|
|
103
|
+
|
|
104
|
+
| Evento | Nivel | Emitido por | Campos clave |
|
|
105
|
+
|---|---|---|---|
|
|
106
|
+
| `producer.publish` | INFO | `Producer#publish_message` (pre) | `method`, `path`, `messaging_*` |
|
|
107
|
+
| `producer.publish_payload` | INFO | `Producer#publish_message` | `payload` (truncado), `body_size` |
|
|
108
|
+
| `producer.publish_detail` | DEBUG | `Producer#publish_message` | `exchange_opts` final |
|
|
109
|
+
| `producer.published` | INFO | `Producer#publish_message` (post) | `method`, `path`, `routing_key`, `messaging_message_id`, **`duration_s`** (publish solo) |
|
|
110
|
+
| `producer.confirmed` | INFO | `Producer#confirmed` (post-ACK) | `method`, `path`, `routing_key`, **`publish_duration_s`**, **`confirm_duration_s`**, **`duration_s`** (total) |
|
|
111
|
+
| `producer.confirms_nacked` | WARN | `Producer#confirmed` (NACK) | `count`, `path` |
|
|
112
|
+
| `producer.rpc_waiting` | DEBUG | `Producer#rpc` | `messaging_message_id`, `timeout_s` |
|
|
113
|
+
| `producer.rpc_response_received` | INFO | `Producer#rpc` (reply recibido) | `method`, `path`, **`duration_s`** (round-trip total), `response_body` |
|
|
114
|
+
| `producer.rpc_response_orphaned` | WARN | reply listener | `correlation_id` |
|
|
115
|
+
| `consumer.message_received` | INFO | `Consumer#process_message` | `method`, `path`, `messaging_*` |
|
|
116
|
+
| `consumer.message_processed` | INFO | `Consumer#process_message` (post) | `response_status`, **`duration_s`**, `controller`, `action`, `messaging_*` |
|
|
117
|
+
| `consumer.execution_error` | ERROR | `Consumer#process_message` (rescue) | **`duration_s`**, `error_class`, `error_message` |
|
|
118
|
+
| `consumer.route_not_found` | WARN | `Consumer#process_message` | `method`, `path` |
|
|
119
|
+
| `consumer.connection_error` | ERROR | `Consumer#subscribe` (retry loop) | `attempt_count`, `retry_in_s`, `error_*` |
|
|
120
|
+
| `session.broker_return` | WARN | `Session` (mandatory unrouted) | `reply_code`, `reply_text`, `exchange`, `routing_key` |
|
|
121
|
+
|
|
122
|
+
**Resumen de qué mide cada `duration_s`:**
|
|
123
|
+
|
|
124
|
+
- `producer.published.duration_s` — solo el `basic_publish` (TCP enqueue al broker).
|
|
125
|
+
- `producer.confirmed.publish_duration_s` — el publish.
|
|
126
|
+
- `producer.confirmed.confirm_duration_s` — la espera del ACK del broker (`wait_for_confirms`).
|
|
127
|
+
- `producer.confirmed.duration_s` — total (publish + ACK).
|
|
128
|
+
- `producer.rpc_response_received.duration_s` — round-trip RPC completo (publish + procesamiento remoto + reply).
|
|
129
|
+
- `consumer.message_processed.duration_s` — procesamiento server-side (router + controller + reply).
|
|
130
|
+
|
|
100
131
|
---
|
|
101
132
|
|
|
102
133
|
## API: Configuración Global
|
|
@@ -202,7 +233,8 @@ client.publish('events', method: :post, body: { type: 'order.placed' })
|
|
|
202
233
|
client.publish('acct.start', exchange: 'acct_x', body: payload,
|
|
203
234
|
confirmed: true, mandatory: true, confirm_timeout: 0.5)
|
|
204
235
|
# → { 'status' => 202, 'body' => nil } # broker ACK confirmado
|
|
205
|
-
# Si broker NACK: logea `producer.confirms_nacked` y
|
|
236
|
+
# Si broker NACK: logea `producer.confirms_nacked` y raise BugBunny::PublishNacked
|
|
237
|
+
# (default — opt-out con config.nack_raise = false o nack_raise: false per request).
|
|
206
238
|
# Si timeout: raise BugBunny::RequestTimeout.
|
|
207
239
|
# Si mandatory + no ruteable: dispara `Configuration#on_return` (default: logea `session.broker_return`).
|
|
208
240
|
```
|
|
@@ -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
|
|
|
@@ -58,19 +59,22 @@ El `Producer` es usado internamente por el `Client`. Implementa tres patrones de
|
|
|
58
59
|
- Reply listener (`basic_consume`) auto-iniciado en el primer RPC.
|
|
59
60
|
- Double-checked locking mutex para seguridad del listener.
|
|
60
61
|
- Timeout lanza `BugBunny::RequestTimeout`.
|
|
62
|
+
- **Emite `producer.rpc_response_received` (INFO) con `duration_s` = round-trip total** (publish + procesamiento remoto + reply). No medir en código de aplicación.
|
|
61
63
|
|
|
62
64
|
### Fire-and-Forget (`Producer#fire`)
|
|
63
65
|
|
|
64
66
|
- Publica en el exchange y retorna `{ 'status' => 202 }` inmediatamente.
|
|
65
67
|
- Sin confirmación de procesamiento.
|
|
68
|
+
- **Emite `producer.published` (INFO) con `duration_s`** = solo el `basic_publish` (TCP enqueue al broker).
|
|
66
69
|
|
|
67
70
|
### Confirmed (`Producer#confirmed`)
|
|
68
71
|
|
|
69
72
|
- Publica y bloquea hasta `channel.wait_for_confirms` del broker.
|
|
70
73
|
- 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
|
-
-
|
|
74
|
+
- 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
75
|
- 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
76
|
- Errores del canal se envuelven en `BugBunny::CommunicationError`; errores `BugBunny::Error` pre-existentes se propagan sin envolver.
|
|
77
|
+
- **Emite `producer.confirmed` (INFO) con tres duraciones desglosadas**: `publish_duration_s` (TCP enqueue), `confirm_duration_s` (`wait_for_confirms`), `duration_s` (total). Útil para distinguir latencia de red vs latencia de confirm policy del broker.
|
|
74
78
|
|
|
75
79
|
## Middleware Stack (Client-side, Onion Architecture)
|
|
76
80
|
|
|
@@ -164,6 +168,7 @@ req.timestamp # Time.now.to_i
|
|
|
164
168
|
req.content_type # 'application/json'
|
|
165
169
|
req.mandatory # Boolean — solo modo :confirmed
|
|
166
170
|
req.confirm_timeout # Float|nil — solo modo :confirmed
|
|
171
|
+
req.nack_raise # Boolean|nil — override per-request de config.nack_raise (solo modo :confirmed)
|
|
167
172
|
```
|
|
168
173
|
|
|
169
174
|
Cuando `mandatory == true`, `Request#amqp_options` inyecta `mandatory: true` en el hash que va a `basic_publish`.
|
|
@@ -182,7 +187,10 @@ Producer#confirmed
|
|
|
182
187
|
│
|
|
183
188
|
├──> wait_for_confirms! (espera ACK del broker, con timeout opcional)
|
|
184
189
|
│
|
|
185
|
-
└──>
|
|
190
|
+
└──> handle_confirm_result
|
|
191
|
+
├─ acked == true → return { status: 202 }
|
|
192
|
+
└─ acked == false → log WARN producer.confirms_nacked
|
|
193
|
+
└─ raise BugBunny::PublishNacked (si config.nack_raise || req.nack_raise)
|
|
186
194
|
|
|
187
195
|
Asíncronamente, si el broker no pudo rutear:
|
|
188
196
|
broker ──basic.return──> Exchange#on_return ──> Session handler ──> Configuration#on_return
|
|
@@ -221,7 +229,7 @@ Excepciones del callback se capturan y se logean como `session.on_return_failed`
|
|
|
221
229
|
| Auditoría, billing, eventos críticos | `:confirmed` (con `mandatory: true` si es ruteable) |
|
|
222
230
|
| Request-response síncrono | `:rpc` |
|
|
223
231
|
|
|
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)
|
|
232
|
+
`: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
233
|
|
|
226
234
|
## Cascada de Configuración (3 niveles)
|
|
227
235
|
|
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
|
@@ -95,6 +95,31 @@ RSpec.describe BugBunny::Producer do
|
|
|
95
95
|
messaging_destination_name: 'events_x'
|
|
96
96
|
)
|
|
97
97
|
end
|
|
98
|
+
|
|
99
|
+
it 'producer.published incluye duration_s y campos de routing' do
|
|
100
|
+
request = BugBunny::Request.new('users')
|
|
101
|
+
request.exchange = 'users_x'
|
|
102
|
+
request.method = :post
|
|
103
|
+
request.correlation_id = 'corr-1'
|
|
104
|
+
|
|
105
|
+
fake_exchange = double('exchange')
|
|
106
|
+
allow(session).to receive(:exchange).and_return(fake_exchange)
|
|
107
|
+
allow(fake_exchange).to receive(:publish)
|
|
108
|
+
|
|
109
|
+
producer.fire(request)
|
|
110
|
+
|
|
111
|
+
published_event = logged_events.find { |e| e[:event] == 'producer.published' }
|
|
112
|
+
expect(published_event).not_to be_nil
|
|
113
|
+
expect(published_event[:level]).to eq(:info)
|
|
114
|
+
expect(published_event[:kwargs]).to include(
|
|
115
|
+
method: 'POST',
|
|
116
|
+
path: 'users',
|
|
117
|
+
routing_key: 'users',
|
|
118
|
+
messaging_message_id: 'corr-1'
|
|
119
|
+
)
|
|
120
|
+
expect(published_event[:kwargs][:duration_s]).to be_a(Numeric)
|
|
121
|
+
expect(published_event[:kwargs][:duration_s]).to be >= 0
|
|
122
|
+
end
|
|
98
123
|
end
|
|
99
124
|
|
|
100
125
|
describe '#rpc — log events incluyen campos OTel' do
|
|
@@ -182,6 +207,28 @@ RSpec.describe BugBunny::Producer do
|
|
|
182
207
|
messaging_message_id: 'corr-reply-test'
|
|
183
208
|
)
|
|
184
209
|
end
|
|
210
|
+
|
|
211
|
+
it 'producer.rpc_response_received incluye duration_s total' do
|
|
212
|
+
request = BugBunny::Request.new('users')
|
|
213
|
+
request.exchange = 'users_x'
|
|
214
|
+
request.method = :get
|
|
215
|
+
|
|
216
|
+
allow(rpc_producer).to receive(:ensure_reply_listener!)
|
|
217
|
+
|
|
218
|
+
ivar = Concurrent::IVar.new
|
|
219
|
+
allow(Concurrent::IVar).to receive(:new).and_return(ivar)
|
|
220
|
+
|
|
221
|
+
request.correlation_id = 'corr-dur'
|
|
222
|
+
rpc_producer.instance_variable_get(:@pending_requests)['corr-dur'] = ivar
|
|
223
|
+
|
|
224
|
+
Thread.new { ivar.set({ body: '{"ok":true}', headers: {} }) }.join(0.1)
|
|
225
|
+
|
|
226
|
+
rpc_producer.rpc(request)
|
|
227
|
+
|
|
228
|
+
response_event = logged_events.find { |e| e[:event] == 'producer.rpc_response_received' }
|
|
229
|
+
expect(response_event[:kwargs][:duration_s]).to be_a(Numeric)
|
|
230
|
+
expect(response_event[:kwargs][:duration_s]).to be >= 0
|
|
231
|
+
end
|
|
185
232
|
end
|
|
186
233
|
end
|
|
187
234
|
|
|
@@ -245,21 +292,71 @@ RSpec.describe BugBunny::Producer do
|
|
|
245
292
|
)
|
|
246
293
|
end
|
|
247
294
|
|
|
248
|
-
it 'logea producer.confirms_nacked cuando
|
|
249
|
-
allow(mock_channel).to receive(:nacked_set).and_return(Set.new([1, 2]))
|
|
250
|
-
|
|
295
|
+
it 'NO logea producer.confirms_nacked cuando wait_for_confirms devuelve true' do
|
|
251
296
|
confirmed_producer.confirmed(build_request)
|
|
252
297
|
|
|
253
298
|
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')
|
|
299
|
+
expect(nack_event).to be_nil
|
|
256
300
|
end
|
|
257
301
|
|
|
258
|
-
it '
|
|
302
|
+
it 'logea producer.confirmed con publish_duration_s, confirm_duration_s y duration_s total' do
|
|
259
303
|
confirmed_producer.confirmed(build_request)
|
|
260
304
|
|
|
261
|
-
|
|
262
|
-
expect(
|
|
305
|
+
ev = logged_events.find { |e| e[:event] == 'producer.confirmed' }
|
|
306
|
+
expect(ev).not_to be_nil
|
|
307
|
+
expect(ev[:level]).to eq(:info)
|
|
308
|
+
expect(ev[:kwargs]).to include(method: 'POST', path: 'acct.start', routing_key: 'acct.start')
|
|
309
|
+
expect(ev[:kwargs][:publish_duration_s]).to be_a(Numeric)
|
|
310
|
+
expect(ev[:kwargs][:confirm_duration_s]).to be_a(Numeric)
|
|
311
|
+
expect(ev[:kwargs][:duration_s]).to be_a(Numeric)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
context 'cuando el broker NACKea (wait_for_confirms devuelve false)' do
|
|
315
|
+
before do
|
|
316
|
+
allow(mock_channel).to receive(:wait_for_confirms).and_return(false)
|
|
317
|
+
allow(mock_channel).to receive(:nacked_set).and_return(Set.new([1, 2]))
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
it 'levanta BugBunny::PublishNacked por default (config.nack_raise = true)' do
|
|
321
|
+
expect { confirmed_producer.confirmed(build_request) }.to raise_error(BugBunny::PublishNacked) do |err|
|
|
322
|
+
expect(err.path).to eq('acct.start')
|
|
323
|
+
expect(err.nacked_count).to eq(2)
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
it 'logea producer.confirms_nacked antes de levantar' do
|
|
328
|
+
expect { confirmed_producer.confirmed(build_request) }.to raise_error(BugBunny::PublishNacked)
|
|
329
|
+
|
|
330
|
+
nack_event = logged_events.find { |e| e[:event] == 'producer.confirms_nacked' }
|
|
331
|
+
expect(nack_event).not_to be_nil
|
|
332
|
+
expect(nack_event[:kwargs]).to include(count: 2, path: 'acct.start')
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
it 'no levanta si el request override `nack_raise = false`' do
|
|
336
|
+
req = build_request
|
|
337
|
+
req.nack_raise = false
|
|
338
|
+
|
|
339
|
+
result = confirmed_producer.confirmed(req)
|
|
340
|
+
|
|
341
|
+
expect(result).to eq('status' => 202, 'body' => nil)
|
|
342
|
+
expect(logged_events.find { |e| e[:event] == 'producer.confirms_nacked' }).not_to be_nil
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
it 'no levanta si la configuración global tiene `nack_raise = false`' do
|
|
346
|
+
allow(BugBunny.configuration).to receive(:nack_raise).and_return(false)
|
|
347
|
+
|
|
348
|
+
result = confirmed_producer.confirmed(build_request)
|
|
349
|
+
|
|
350
|
+
expect(result).to eq('status' => 202, 'body' => nil)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
it 'el override per-request gana sobre la configuración global' do
|
|
354
|
+
allow(BugBunny.configuration).to receive(:nack_raise).and_return(false)
|
|
355
|
+
req = build_request
|
|
356
|
+
req.nack_raise = true
|
|
357
|
+
|
|
358
|
+
expect { confirmed_producer.confirmed(req) }.to raise_error(BugBunny::PublishNacked)
|
|
359
|
+
end
|
|
263
360
|
end
|
|
264
361
|
|
|
265
362
|
it 'levanta BugBunny::RequestTimeout si wait_for_confirms excede confirm_timeout' do
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bug_bunny
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gabix
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|
|
@@ -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.14.0/skill
|
|
307
307
|
post_install_message:
|
|
308
308
|
rdoc_options: []
|
|
309
309
|
require_paths:
|