bug_bunny 4.1.1 → 4.1.2

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: 979c38a4f25c9359afd10b0f3c728b794fdf4dee795adfaea7fd090e832134bc
4
+ data.tar.gz: 658a0088c4e8cedde115074b81a9247e6cf1c15d226e880c444bd94ed8168cc5
5
5
  SHA512:
6
- metadata.gz: 7df1503a55cb62d9f87168a84b8e92fe773b4c9525b143d6f848227e66fe63e5ab75f6191a408b00bb9c8840b44c0f8fdbc36cd0a8d2eb56e02018d09654d269
7
- data.tar.gz: d5433d839c45ad95a2d0399535ff629112458e90d0e4e1ec89f97afa450b139bcb7855c64c27042c22bce0bde87b2bfa7bd6f8bae5b879dd7eb0c71ff1d20d3c
6
+ metadata.gz: 0d80a2015e3f26626e6f0261597a261129a4c55f87ed5556f3bab7523327a747e1238241e5c49769f22dd83d231e151913a786b34119eabe43ec858f456c60d1
7
+ data.tar.gz: 94635e6463d95eac5ffd65e7fe5ff3eaf3c946825052477a31b9ddde0da763b87b5e2f27e877b8c72c5a3bcb13920e28269b12f5c123567bd75ff0ed00d049ae
data/CHANGELOG.md CHANGED
@@ -1,15 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.1.2] - 2026-03-22
4
+
5
+ ### ✨ Improvements
6
+ * **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.
7
+ * **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.
8
+
3
9
  ## [4.1.1] - 2026-03-22
4
10
 
5
11
  ### 🐛 Bug Fixes
6
12
  * **Consumer:** Previene memory leak al detener el `TimerTask` de health check previo antes de realizar una reconexión.
7
13
  * **Controller:** Corrige la mutación accidental de \`log_tags\` globales al usar una lógica de herencia no destructiva en \`compute_tags\`.
8
14
 
9
- ### ✨ Improvements
10
- * **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
- * **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
-
13
15
  ## [4.1.0] - 2026-03-22
14
16
 
15
17
  ### 🚀 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("[BugBunny::Consumer] 🎧 Listening on '#{queue_name}' (Opts: #{final_q_opts})")
81
+ BugBunny.configuration.logger.info("[BugBunny::Consumer] 🔀 Bounded to Exchange '#{exchange_name}' (#{exchange_type}) | Opts: #{final_x_opts} | RK: '#{routing_key}'")
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("[BugBunny::Consumer] 💥 Max reconnect attempts (#{max_attempts}) reached. Giving up: #{e.message}")
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("[BugBunny::Consumer] 💥 Connection Error: #{e.message}. Retrying in #{wait}s (attempt #{attempt}/#{max_attempts || '∞'})...")
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
@@ -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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = "4.1.1"
4
+ VERSION = "4.1.2"
5
5
  end
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.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - gabix