bug_bunny 4.1.1 → 4.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 599004cdcb498ca746612f60202301ce9783a67ac37d14ecd4c29f51ccaf48bb
4
- data.tar.gz: f748ba9a55e91f96a5571f671afa524f7e330cceede0f12c5c7cef6d25d2d2ff
3
+ metadata.gz: 26437c12bdcd679124739cffc4d1c9769587bc6ffd241d6a03a75880c3bd910d
4
+ data.tar.gz: cc185721833cbf505d1fe44ac1dac7ac496fda1677b54898ce950a1d1badff68
5
5
  SHA512:
6
- metadata.gz: 7df1503a55cb62d9f87168a84b8e92fe773b4c9525b143d6f848227e66fe63e5ab75f6191a408b00bb9c8840b44c0f8fdbc36cd0a8d2eb56e02018d09654d269
7
- data.tar.gz: d5433d839c45ad95a2d0399535ff629112458e90d0e4e1ec89f97afa450b139bcb7855c64c27042c22bce0bde87b2bfa7bd6f8bae5b879dd7eb0c71ff1d20d3c
6
+ metadata.gz: 71da0a97f33b38263957a878350e8a2daa935a0f211e0fa59af589b507a7d11635e84ac6170ee2a4fafb16b0c4f8ddeec61b388f38d7f9856d171e3059b004f3
7
+ data.tar.gz: 962de8a788d263c04cf0d9fa5da2e6c3a5ec3a2d6156b80ff542b8d2fd333c4d9d0915f94775f8fb9c8101d507691cf757701904fe1d0477d5dabfcd006efa7e
data/CHANGELOG.md CHANGED
@@ -1,15 +1,28 @@
1
1
  # Changelog
2
2
 
3
- ## [4.1.1] - 2026-03-22
3
+ ## [4.2.0] - 2026-03-22
4
4
 
5
- ### 🐛 Bug Fixes
6
- * **Consumer:** Previene memory leak al detener el `TimerTask` de health check previo antes de realizar una reconexión.
7
- * **Controller:** Corrige la mutación accidental de \`log_tags\` globales al usar una lógica de herencia no destructiva en \`compute_tags\`.
5
+ ### 🔠Observability & Structured Logging
6
+ * **Structured Logs (Key-Value):** Se migraron todos los logs del framework a un formato \`key=value\` estructurado, ideal para herramientas de monitoreo como Datadog o CloudWatch. Se eliminaron emojis y texto libre para mejorar el parseo automático.
7
+ * **Lazy Evaluation (Debug Blocks):** Las llamadas a \`logger.debug\` ahora utilizan bloques para evitar la interpolación de strings innecesaria en producción, optimizando el uso de CPU y memoria.
8
+
9
+ ### ðŸ›¡ï¸ Resilience & Connectivity
10
+ * **Exponential Backoff:** El \`Consumer\` ahora implementa un algoritmo de reintento exponencial para reconectarse a RabbitMQ, evitando picos de carga durante caídas del broker.
11
+ * **Max Reconnect Attempts:** Nueva configuración \`max_reconnect_attempts\` que permite que el worker falle definitivamente tras N intentos, facilitando el reinicio del Pod por parte de orquestadores como Kubernetes.
12
+ * **Performance Tuning:** Se desactivaron los \`publisher_confirms\` en el canal del \`Consumer\` al responder RPCs para reducir la latencia de respuesta (round-trips innecesarios).
13
+
14
+ ## [4.1.2] - 2026-03-22
8
15
 
9
16
  ### ✨ Improvements
10
17
  * **Controller:** Ahora lanza una excepción \`BugBunny::BadRequest\` (400) si el cuerpo de la petición contiene un JSON inválido, mejorando la depuración en el cliente.
11
18
  * **Resource:** Se añadió una protección a \`.with\` (\`ScopeProxy\`) para asegurar que el contexto sea de un solo uso, evitando efectos secundarios en llamadas encadenadas.
12
19
 
20
+ ## [4.1.1] - 2026-03-22
21
+
22
+ ### 🐛 Bug Fixes
23
+ * **Consumer:** Previene memory leak al detener el `TimerTask` de health check previo antes de realizar una reconexión.
24
+ * **Controller:** Corrige la mutación accidental de \`log_tags\` globales al usar una lógica de herencia no destructiva en \`compute_tags\`.
25
+
13
26
  ## [4.1.0] - 2026-03-22
14
27
 
15
28
  ### 🚀 New Features & Improvements
data/README.md CHANGED
@@ -74,7 +74,9 @@ BugBunny.configure do |config|
74
74
 
75
75
  # 2. Timeouts y Recuperación
76
76
  config.rpc_timeout = 10 # Segundos máx para esperar respuesta (Síncrono)
77
- config.network_recovery_interval = 5 # Reintento de conexión
77
+ config.network_recovery_interval = 5 # Base del backoff de reconexión (segundos)
78
+ config.max_reconnect_interval = 60 # Techo del backoff exponencial (segundos)
79
+ config.max_reconnect_attempts = nil # nil = reintenta infinitamente; Integer = falla hard
78
80
 
79
81
  # 3. Health Checks (Opcional, para Docker Swarm / K8s)
80
82
  config.health_check_file = '/tmp/bug_bunny_health'
@@ -420,6 +422,30 @@ Para máxima velocidad, BugBunny usa `amq.rabbitmq.reply-to`.
420
422
  ### Seguridad
421
423
  El Router incluye protecciones contra **Remote Code Execution (RCE)**. El Consumer verifica estrictamente que el Controlador resuelto a través del archivo de rutas herede de `BugBunny::Controller` antes de ejecutarla, impidiendo la inyección de clases arbitrarias. Además, las llamadas a rutas no registradas fallan rápido con un `404 Not Found`.
422
424
 
425
+ ### Reconexión con Backoff Exponencial
426
+
427
+ Cuando el Consumer pierde la conexión a RabbitMQ, reintenta automáticamente usando un backoff exponencial basado en `network_recovery_interval`:
428
+
429
+ | Intento | Espera (base = 5s, techo = 60s) |
430
+ |---------|----------------------------------|
431
+ | 1 | 5s |
432
+ | 2 | 10s |
433
+ | 3 | 20s |
434
+ | 4 | 40s |
435
+ | 5+ | 60s (cap) |
436
+
437
+ Por defecto reintenta indefinidamente (`max_reconnect_attempts: nil`). En entornos orquestados (Kubernetes, Docker Swarm), es preferible dejar que el orquestador reinicie el contenedor cuando la infraestructura no está disponible:
438
+
439
+ ```ruby
440
+ BugBunny.configure do |config|
441
+ config.network_recovery_interval = 5 # Base del backoff
442
+ config.max_reconnect_interval = 60 # Techo máximo de espera
443
+ config.max_reconnect_attempts = 10 # Falla hard después de 10 intentos
444
+ end
445
+ ```
446
+
447
+ Con esta configuración, si RabbitMQ no vuelve en ~10 reintentos el proceso levanta la excepción y el orquestador lo reinicia con su propia política de restart.
448
+
423
449
  ### Health Checks en Docker Swarm / Kubernetes
424
450
  Dado que un Worker se ejecuta en segundo plano sin exponer un servidor web tradicional, orquestadores como Docker Swarm o Kubernetes no pueden usar un endpoint HTTP para verificar si el proceso está saludable.
425
451
 
@@ -38,9 +38,16 @@ module BugBunny
38
38
  # @return [Boolean] Si `true`, Bunny intentará reconectar automáticamente.
39
39
  attr_accessor :automatically_recover
40
40
 
41
- # @return [Integer] Tiempo en segundos a esperar antes de intentar reconectar.
41
+ # @return [Integer] Tiempo en segundos a esperar antes de intentar reconectar (base del backoff).
42
42
  attr_accessor :network_recovery_interval
43
43
 
44
+ # @return [Integer, nil] Número máximo de intentos de reconexión del Consumer antes de rendirse.
45
+ # Si es `nil` (default), reintenta indefinidamente.
46
+ attr_accessor :max_reconnect_attempts
47
+
48
+ # @return [Integer] Techo en segundos para el backoff exponencial de reconexión (default: 60).
49
+ attr_accessor :max_reconnect_interval
50
+
44
51
  # @return [Integer] Timeout en segundos para establecer la conexión TCP inicial.
45
52
  attr_accessor :connection_timeout
46
53
 
@@ -106,6 +113,8 @@ module BugBunny
106
113
  @bunny_logger.level = Logger::WARN
107
114
  @automatically_recover = true
108
115
  @network_recovery_interval = 5
116
+ @max_reconnect_attempts = nil
117
+ @max_reconnect_interval = 60
109
118
  @connection_timeout = 10
110
119
  @read_timeout = 30
111
120
  @write_timeout = 30
@@ -43,7 +43,7 @@ module BugBunny
43
43
  #
44
44
  # @param connection [Bunny::Session] Conexión nativa de Bunny.
45
45
  def initialize(connection)
46
- @session = BugBunny::Session.new(connection)
46
+ @session = BugBunny::Session.new(connection, publisher_confirms: false)
47
47
  @health_timer = nil
48
48
  end
49
49
 
@@ -61,41 +61,58 @@ module BugBunny
61
61
  # @param block [Boolean] Si es `true`, bloquea el hilo actual (loop infinito).
62
62
  # @return [void]
63
63
  def subscribe(queue_name:, exchange_name:, routing_key:, exchange_type: 'direct', exchange_opts: {}, queue_opts: {}, block: true)
64
- # Declaración de Infraestructura
65
- x = session.exchange(name: exchange_name, type: exchange_type, opts: exchange_opts)
66
- q = session.queue(queue_name, queue_opts)
67
- q.bind(x, routing_key: routing_key)
68
-
69
- # 📊 LOGGING DE OBSERVABILIDAD: Calculamos las opciones finales para mostrarlas en consola
70
- final_x_opts = BugBunny::Session::DEFAULT_EXCHANGE_OPTIONS
71
- .merge(BugBunny.configuration.exchange_options || {})
72
- .merge(exchange_opts || {})
73
- final_q_opts = BugBunny::Session::DEFAULT_QUEUE_OPTIONS
74
- .merge(BugBunny.configuration.queue_options || {})
75
- .merge(queue_opts || {})
76
-
77
- BugBunny.configuration.logger.info("[BugBunny::Consumer] 🎧 Listening on '#{queue_name}' (Opts: #{final_q_opts})")
78
- BugBunny.configuration.logger.info("[BugBunny::Consumer] 🔀 Bounded to Exchange '#{exchange_name}' (#{exchange_type}) | Opts: #{final_x_opts} | RK: '#{routing_key}'")
79
-
80
- start_health_check(queue_name)
81
-
82
- q.subscribe(manual_ack: true, block: block) do |delivery_info, properties, body|
83
- trace_id = properties.correlation_id
84
-
85
- logger = BugBunny.configuration.logger
86
-
87
- if logger.respond_to?(:tagged)
88
- logger.tagged(trace_id) { process_message(delivery_info, properties, body) }
89
- elsif defined?(Rails) && Rails.logger.respond_to?(:tagged)
90
- Rails.logger.tagged(trace_id) { process_message(delivery_info, properties, body) }
91
- else
92
- process_message(delivery_info, properties, body)
64
+ attempt = 0
65
+
66
+ begin
67
+ # Declaración de Infraestructura
68
+ x = session.exchange(name: exchange_name, type: exchange_type, opts: exchange_opts)
69
+ q = session.queue(queue_name, queue_opts)
70
+ q.bind(x, routing_key: routing_key)
71
+
72
+ # 📊 LOGGING DE OBSERVABILIDAD: Calculamos las opciones finales para mostrarlas en consola
73
+ final_x_opts = BugBunny::Session::DEFAULT_EXCHANGE_OPTIONS
74
+ .merge(BugBunny.configuration.exchange_options || {})
75
+ .merge(exchange_opts || {})
76
+ final_q_opts = BugBunny::Session::DEFAULT_QUEUE_OPTIONS
77
+ .merge(BugBunny.configuration.queue_options || {})
78
+ .merge(queue_opts || {})
79
+
80
+ BugBunny.configuration.logger.info("component=bug_bunny event=consumer_start queue=#{queue_name} queue_opts=#{final_q_opts}")
81
+ BugBunny.configuration.logger.info("component=bug_bunny event=consumer_bound exchange=#{exchange_name} exchange_type=#{exchange_type} routing_key=#{routing_key} exchange_opts=#{final_x_opts}")
82
+
83
+ start_health_check(queue_name)
84
+
85
+ q.subscribe(manual_ack: true, block: block) do |delivery_info, properties, body|
86
+ trace_id = properties.correlation_id
87
+
88
+ logger = BugBunny.configuration.logger
89
+
90
+ if logger.respond_to?(:tagged)
91
+ logger.tagged(trace_id) { process_message(delivery_info, properties, body) }
92
+ elsif defined?(Rails) && Rails.logger.respond_to?(:tagged)
93
+ Rails.logger.tagged(trace_id) { process_message(delivery_info, properties, body) }
94
+ else
95
+ process_message(delivery_info, properties, body)
96
+ end
97
+ end
98
+ rescue StandardError => e
99
+ attempt += 1
100
+ max_attempts = BugBunny.configuration.max_reconnect_attempts
101
+
102
+ if max_attempts && attempt >= max_attempts
103
+ BugBunny.configuration.logger.error { "component=bug_bunny event=reconnect_exhausted attempts=#{max_attempts} error=#{e.message.inspect}" }
104
+ raise
93
105
  end
106
+
107
+ wait = [
108
+ BugBunny.configuration.network_recovery_interval * (2 ** (attempt - 1)),
109
+ BugBunny.configuration.max_reconnect_interval
110
+ ].min
111
+
112
+ BugBunny.configuration.logger.error { "component=bug_bunny event=connection_error error=#{e.message.inspect} attempt=#{attempt} max_attempts=#{max_attempts || 'infinity'} retry_in=#{wait}" }
113
+ sleep wait
114
+ retry
94
115
  end
95
- rescue StandardError => e
96
- BugBunny.configuration.logger.error("[BugBunny::Consumer] 💥 Connection Error: #{e.message}. Retrying in #{BugBunny.configuration.network_recovery_interval}s...")
97
- sleep BugBunny.configuration.network_recovery_interval
98
- retry
99
116
  end
100
117
 
101
118
  private
@@ -113,7 +130,7 @@ module BugBunny
113
130
  path = properties.type || (properties.headers && properties.headers['path'])
114
131
 
115
132
  if path.nil? || path.empty?
116
- BugBunny.configuration.logger.error("[BugBunny::Consumer] Rejected: Missing 'type' header.")
133
+ BugBunny.configuration.logger.error('component=bug_bunny event=message_rejected reason=missing_type_header')
117
134
  session.channel.reject(delivery_info.delivery_tag, false)
118
135
  return
119
136
  end
@@ -122,8 +139,8 @@ module BugBunny
122
139
  headers_hash = properties.headers || {}
123
140
  http_method = (headers_hash['x-http-method'] || headers_hash['method'] || 'GET').to_s.upcase
124
141
 
125
- BugBunny.configuration.logger.info("[BugBunny::Consumer] 📥 Received #{http_method} \"/#{path}\" | RK: '#{delivery_info.routing_key}'")
126
- BugBunny.configuration.logger.debug("[BugBunny::Consumer] 📦 Body: #{body.truncate(200)}")
142
+ BugBunny.configuration.logger.info("component=bug_bunny event=message_received method=#{http_method} path=#{path} routing_key=#{delivery_info.routing_key}")
143
+ BugBunny.configuration.logger.debug { "component=bug_bunny event=message_received_body body=#{body.truncate(200).inspect}" }
127
144
 
128
145
  # ===================================================================
129
146
  # 3. Ruteo Declarativo
@@ -140,7 +157,7 @@ module BugBunny
140
157
  route_info = BugBunny.routes.recognize(http_method, uri.path)
141
158
 
142
159
  if route_info.nil?
143
- BugBunny.configuration.logger.warn("[BugBunny::Consumer] ⚠️ No route matches [#{http_method}] \"/#{uri.path}\"")
160
+ BugBunny.configuration.logger.warn("component=bug_bunny event=route_not_found method=#{http_method} path=#{uri.path}")
144
161
  handle_fatal_error(properties, 404, "Not Found", "No route matches [#{http_method}] \"/#{uri.path}\"")
145
162
  session.channel.reject(delivery_info.delivery_tag, false)
146
163
  return
@@ -159,7 +176,7 @@ module BugBunny
159
176
  begin
160
177
  controller_class = controller_class_name.constantize
161
178
  rescue NameError
162
- BugBunny.configuration.logger.warn("[BugBunny::Consumer] ⚠️ Controller class not found: #{controller_class_name}")
179
+ BugBunny.configuration.logger.warn("component=bug_bunny event=controller_not_found controller=#{controller_class_name}")
163
180
  handle_fatal_error(properties, 404, "Not Found", "Controller #{controller_class_name} not found")
164
181
  session.channel.reject(delivery_info.delivery_tag, false)
165
182
  return
@@ -167,13 +184,13 @@ module BugBunny
167
184
 
168
185
  # Verificación estricta de Seguridad (RCE Prevention)
169
186
  unless controller_class < BugBunny::Controller
170
- BugBunny.configuration.logger.error("[BugBunny::Consumer] Security Alert: #{controller_class} is not a valid BugBunny Controller")
187
+ BugBunny.configuration.logger.error("component=bug_bunny event=security_violation reason=invalid_controller controller=#{controller_class}")
171
188
  handle_fatal_error(properties, 403, "Forbidden", "Invalid Controller Class")
172
189
  session.channel.reject(delivery_info.delivery_tag, false)
173
190
  return
174
191
  end
175
192
 
176
- BugBunny.configuration.logger.debug("[BugBunny::Consumer] 🎯 Routed to #{controller_class_name}##{route_info[:action]}")
193
+ BugBunny.configuration.logger.debug { "component=bug_bunny event=route_matched controller=#{controller_class_name} action=#{route_info[:action]}" }
177
194
 
178
195
  request_metadata = {
179
196
  type: path,
@@ -199,8 +216,8 @@ module BugBunny
199
216
  session.channel.ack(delivery_info.delivery_tag)
200
217
 
201
218
  rescue StandardError => e
202
- BugBunny.configuration.logger.error("[BugBunny::Consumer] 💥 Execution Error (#{e.class}): #{e.message}")
203
- BugBunny.configuration.logger.debug(e.backtrace.join("\n"))
219
+ BugBunny.configuration.logger.error { "component=bug_bunny event=execution_error error_class=#{e.class} error=#{e.message.inspect}" }
220
+ BugBunny.configuration.logger.debug { "component=bug_bunny event=execution_error backtrace=#{e.backtrace.first(5).join(' | ').inspect}" }
204
221
  handle_fatal_error(properties, 500, "Internal Server Error", e.message)
205
222
  session.channel.reject(delivery_info.delivery_tag, false)
206
223
  end
@@ -212,7 +229,7 @@ module BugBunny
212
229
  # @param correlation_id [String] ID para correlacionar la respuesta con la petición original.
213
230
  # @return [void]
214
231
  def reply(payload, reply_to, correlation_id)
215
- BugBunny.configuration.logger.debug("[BugBunny::Consumer] 📤 Sending RPC Reply to #{reply_to} | ID: #{correlation_id}")
232
+ BugBunny.configuration.logger.debug { "component=bug_bunny event=rpc_reply reply_to=#{reply_to} correlation_id=#{correlation_id}" }
216
233
  session.channel.default_exchange.publish(
217
234
  payload.to_json,
218
235
  routing_key: reply_to,
@@ -261,7 +278,7 @@ module BugBunny
261
278
  # 2. Si llegamos aquí, RabbitMQ y la cola están vivos. Avisamos al orquestador actualizando el archivo.
262
279
  touch_health_file(file_path) if file_path
263
280
  rescue StandardError => e
264
- BugBunny.configuration.logger.warn("[BugBunny::Consumer] ⚠️ Queue check failed: #{e.message}. Reconnecting session...")
281
+ BugBunny.configuration.logger.warn("component=bug_bunny event=health_check_failed queue=#{q_name} error=#{e.message.inspect}")
265
282
  session.close
266
283
  end
267
284
  @health_timer.execute
@@ -276,7 +293,7 @@ module BugBunny
276
293
  def touch_health_file(file_path)
277
294
  FileUtils.touch(file_path)
278
295
  rescue StandardError => e
279
- BugBunny.configuration.logger.error("[BugBunny::Consumer] ⚠️ Cannot touch health check file '#{file_path}': #{e.message}")
296
+ BugBunny.configuration.logger.error("component=bug_bunny event=health_check_file_error path=#{file_path} error=#{e.message.inspect}")
280
297
  end
281
298
  end
282
299
  end
@@ -204,8 +204,8 @@ module BugBunny
204
204
  end
205
205
 
206
206
  # Fallback genérico si la excepción no fue mapeada
207
- BugBunny.configuration.logger.error("[BugBunny::Controller] 💥 Unhandled Exception (#{exception.class}): #{exception.message}")
208
- BugBunny.configuration.logger.error(exception.backtrace.first(5).join("\n"))
207
+ BugBunny.configuration.logger.error { "component=bug_bunny event=unhandled_exception error_class=#{exception.class} error=#{exception.message.inspect}" }
208
+ BugBunny.configuration.logger.error { "component=bug_bunny event=unhandled_exception backtrace=#{exception.backtrace.first(5).join(' | ').inspect}" }
209
209
 
210
210
  {
211
211
  status: 500,
@@ -73,7 +73,7 @@ module BugBunny
73
73
  begin
74
74
  fire(request)
75
75
 
76
- BugBunny.configuration.logger.debug("[BugBunny::Producer] Waiting for RPC response | ID: #{cid} | Timeout: #{wait_timeout}s")
76
+ BugBunny.configuration.logger.debug { "component=bug_bunny event=rpc_waiting correlation_id=#{cid} timeout=#{wait_timeout}" }
77
77
 
78
78
  # Bloqueamos el hilo aquí hasta que llegue la respuesta o expire el timeout
79
79
  response_payload = future.value(wait_timeout)
@@ -106,12 +106,9 @@ module BugBunny
106
106
  .merge(BugBunny.configuration.exchange_options || {})
107
107
  .merge(request.exchange_options || {})
108
108
 
109
- # INFO: Resumen de una línea (Traffic)
110
- BugBunny.configuration.logger.info("[BugBunny::Producer] 📤 #{verb} /#{target} | RK: '#{rk}' | ID: #{id}")
111
-
112
- # DEBUG: Detalle completo de Infraestructura y Payload
113
- BugBunny.configuration.logger.debug("[BugBunny::Producer] ⚙️ Exchange #{request.exchange} | Opts: #{final_x_opts}")
114
- BugBunny.configuration.logger.debug("[BugBunny::Producer] 📦 Payload: #{payload.truncate(300)}") if payload.is_a?(String)
109
+ BugBunny.configuration.logger.info("component=bug_bunny event=publish method=#{verb} path=#{target} routing_key=#{rk} correlation_id=#{id}")
110
+ BugBunny.configuration.logger.debug { "component=bug_bunny event=publish_detail exchange=#{request.exchange} exchange_opts=#{final_x_opts}" }
111
+ BugBunny.configuration.logger.debug { "component=bug_bunny event=publish_payload payload=#{payload.truncate(300).inspect}" } if payload.is_a?(String)
115
112
  end
116
113
 
117
114
  # Serializa el mensaje para su transporte.
@@ -145,16 +142,16 @@ module BugBunny
145
142
  @reply_listener_mutex.synchronize do
146
143
  return if @reply_listener_started
147
144
 
148
- BugBunny.configuration.logger.debug("[BugBunny::Producer] 👂 Starting Reply Listener on 'amq.rabbitmq.reply-to'")
145
+ BugBunny.configuration.logger.debug { 'component=bug_bunny event=reply_listener_start queue=amq.rabbitmq.reply-to' }
149
146
 
150
147
  # Consumimos sin ack (auto-ack) porque reply-to no soporta acks manuales de forma estándar
151
148
  @session.channel.basic_consume('amq.rabbitmq.reply-to', '', true, false, nil) do |_, props, body|
152
149
  cid = props.correlation_id.to_s
153
- BugBunny.configuration.logger.debug("[BugBunny::Producer] 📥 RPC Response matched | ID: #{cid}")
150
+ BugBunny.configuration.logger.debug { "component=bug_bunny event=rpc_response_received correlation_id=#{cid}" }
154
151
  if (future = @pending_requests[cid])
155
152
  future.set(body)
156
153
  else
157
- BugBunny.configuration.logger.warn("[BugBunny::Producer] ⚠️ Orphaned RPC Response received | ID: #{cid}")
154
+ BugBunny.configuration.logger.warn("component=bug_bunny event=rpc_response_orphaned correlation_id=#{cid}")
158
155
  end
159
156
  end
160
157
  @reply_listener_started = true
@@ -24,8 +24,12 @@ module BugBunny
24
24
  # Inicializa una nueva sesión sin abrir canales todavía.
25
25
  #
26
26
  # @param connection [Bunny::Session] Una conexión (puede estar abierta o cerrada temporalmente).
27
- def initialize(connection)
27
+ # @param publisher_confirms [Boolean] Si es `true`, el canal se abre en modo Publisher Confirms.
28
+ # Activar solo en sesiones de Producer. En sesiones de Consumer genera overhead innecesario
29
+ # ya que los replies RPC son fire-and-forget desde la perspectiva del servidor.
30
+ def initialize(connection, publisher_confirms: true)
28
31
  @connection = connection
32
+ @publisher_confirms = publisher_confirms
29
33
  @channel = nil
30
34
  end
31
35
 
@@ -105,8 +109,7 @@ module BugBunny
105
109
  def create_channel!
106
110
  @channel = @connection.create_channel
107
111
 
108
- # Configuraciones globales de BugBunny
109
- @channel.confirm_select
112
+ @channel.confirm_select if @publisher_confirms
110
113
 
111
114
  if BugBunny.configuration.channel_prefetch
112
115
  @channel.prefetch(BugBunny.configuration.channel_prefetch)
@@ -122,10 +125,10 @@ module BugBunny
122
125
  def ensure_connection!
123
126
  return if @connection.open?
124
127
 
125
- BugBunny.configuration.logger.warn("[BugBunny::Session] ⚠️ Connection lost. Attempting to reconnect...")
128
+ BugBunny.configuration.logger.warn('component=bug_bunny event=reconnect_attempt')
126
129
  @connection.start
127
130
  rescue StandardError => e
128
- BugBunny.configuration.logger.error("[BugBunny::Session] Critical connection failure: #{e.message}")
131
+ BugBunny.configuration.logger.error { "component=bug_bunny event=reconnect_failed error=#{e.message.inspect}" }
129
132
  raise BugBunny::CommunicationError, "Could not reconnect to RabbitMQ: #{e.message}"
130
133
  end
131
134
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = "4.1.1"
4
+ VERSION = "4.2.0"
5
5
  end
data/lib/bug_bunny.rb CHANGED
@@ -83,7 +83,7 @@ module BugBunny
83
83
 
84
84
  @global_connection.close if @global_connection.open?
85
85
  @global_connection = nil
86
- configuration.logger.info('[BugBunny] 🔌 Global connection closed.')
86
+ configuration.logger.info('component=bug_bunny event=disconnect')
87
87
  end
88
88
 
89
89
  # @api private
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.1.1
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - gabix