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 +4 -4
- data/CHANGELOG.md +6 -4
- data/README.md +27 -1
- data/lib/bug_bunny/configuration.rb +10 -1
- data/lib/bug_bunny/consumer.rb +51 -34
- data/lib/bug_bunny/session.rb +6 -3
- data/lib/bug_bunny/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 979c38a4f25c9359afd10b0f3c728b794fdf4dee795adfaea7fd090e832134bc
|
|
4
|
+
data.tar.gz: 658a0088c4e8cedde115074b81a9247e6cf1c15d226e880c444bd94ed8168cc5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 #
|
|
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
|
data/lib/bug_bunny/consumer.rb
CHANGED
|
@@ -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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
logger
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
data/lib/bug_bunny/session.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|
data/lib/bug_bunny/version.rb
CHANGED