bug_bunny 3.1.3 → 3.1.5

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: 4aab7949b09bfbf861d4a9ff736bc3bef832fdb3c8a302924544783bdbc9e1bf
4
- data.tar.gz: 12b73641a812ec4ce72d2a5ad8992a1f2b91c4b0b11ce181aac02fe3a4912751
3
+ metadata.gz: a96d4e81fd57e9a1a524aa479bbb85d6fe3a99bd654da95014f633d2fb0cb2d2
4
+ data.tar.gz: cb49f5e1789c1737572fb98b2d21428cf02c129c33e3049de1a0de4f6270a9d3
5
5
  SHA512:
6
- metadata.gz: 2f4475f1754c1de91be6576d4fabb23d025b1f3735d4d76d3123415e71b907d2bfc307dd8e8f4a921ad5a11b2524cd85a1ee17ea3fb1d42f6e15c62ddef01425
7
- data.tar.gz: c3973288d2d394121491ffb3c7fa809b8841c9fdc3fc87486fb807b62b6f948e48298fd9f4b696132c9b86d448dad82bd4cfa909c39384efc2ab1b7729c15f31
6
+ metadata.gz: ea0ae037590a852734607ad7e46fabff4cfab87aef068a724da42d506fc79ccda376d0db85f0d4ebd1f516ac2aef963fcdd18604a6d808efb3e4e2de0ca32391
7
+ data.tar.gz: 6930cc1f181bc7d6968cb4fe3ed82f25cb59650522f45c5ad5e6f9328cc33125bc41d197de35f05f16e1126014ea7ada86cb2d6a348e7c4e0b6a4c8e56a61a7a
data/CHANGELOG.md CHANGED
@@ -1,4 +1,21 @@
1
1
  # Changelog
2
+ ## [3.1.5] - 2026-02-25
3
+
4
+ ### ✨ New Features & Improvements
5
+ * **Smart Heuristic Router (Namespace Support):** El enrutador interno del consumidor (`Consumer#router_dispatch`) fue reescrito para soportar namespaces profundos y rutas anidadas sin necesidad de configuración manual. Utiliza una heurística basada en Regex para detectar dinámicamente identificadores (Enteros, UUIDs o hashes alfanuméricos largos) dentro de la URL.
6
+ * Esto permite que rutas complejas como `GET api/v1/ecommerce/orders/a1b2c3d4/cancel` resuelvan automáticamente al controlador `Api::V1::Ecommerce::OrdersController`, asignando `id: a1b2c3d4` y `action: cancel`.
7
+
8
+ ## [3.1.4] - 2026-02-21
9
+
10
+ ### 🚀 Cloud Native & Infrastructure Features
11
+ * **Docker Swarm / Kubernetes Health Checks:** Introduced native support for external orchestrator health checks using the **Touchfile** pattern.
12
+ * Added `config.health_check_file` to the global configuration.
13
+ * The `Consumer`'s internal heartbeat now automatically updates the modification time (`touch`) of the specified file upon successful validation of the RabbitMQ connection and queue existence.
14
+ * Fails gracefully without interrupting the consumer if file system permissions are restricted.
15
+
16
+ ### 📖 Documentation
17
+ * **Production Guide Expansion:** Added a comprehensive "Health Checks en Docker Swarm / Kubernetes" section to the README. Includes detailed `docker-compose.yml` examples demonstrating best practices for integrating the touchfile pattern, specifically highlighting the critical use of `start_period` to accommodate Rails boot times.
18
+
2
19
  ## [3.1.3] - 2026-02-19
3
20
 
4
21
  ### 🏗️ Architectural Refactoring (Middleware Standardization)
data/README.md CHANGED
@@ -76,11 +76,14 @@ BugBunny.configure do |config|
76
76
  config.rpc_timeout = 10 # Segundos máx para esperar respuesta (Síncrono)
77
77
  config.network_recovery_interval = 5 # Reintento de conexión
78
78
 
79
- # 3. Logging
79
+ # 3. Health Checks (Opcional, para Docker Swarm / K8s)
80
+ config.health_check_file = '/tmp/bug_bunny_health'
81
+
82
+ # 4. Logging
80
83
  config.logger = Rails.logger
81
84
  end
82
85
 
83
- # 4. Connection Pool (CRÍTICO para concurrencia)
86
+ # 5. Connection Pool (CRÍTICO para concurrencia)
84
87
  # Define un pool global para compartir conexiones entre hilos
85
88
  BUG_BUNNY_POOL = ConnectionPool.new(size: ENV.fetch('RAILS_MAX_THREADS', 5).to_i, timeout: 5) do
86
89
  BugBunny.create_connection
@@ -377,6 +380,35 @@ Para máxima velocidad, BugBunny usa `amq.rabbitmq.reply-to`.
377
380
  ### Seguridad
378
381
  El Router incluye protecciones contra **Remote Code Execution (RCE)**. Verifica estrictamente que la clase instanciada herede de `BugBunny::Controller` antes de ejecutarla, impidiendo la inyección de clases arbitrarias de Ruby vía el header `type`.
379
382
 
383
+ ### Health Checks en Docker Swarm / Kubernetes
384
+ 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.
385
+
386
+ BugBunny implementa el patrón **Touchfile**. Puedes configurar la gema para que actualice la fecha de modificación de un archivo temporal en cada latido exitoso (heartbeat) hacia RabbitMQ.
387
+
388
+ **1. Configurar la gema:**
389
+ ```ruby
390
+ # config/initializers/bug_bunny.rb
391
+ BugBunny.configure do |config|
392
+ # Actualizará la fecha de este archivo si la conexión a la cola está sana
393
+ config.health_check_file = '/tmp/bug_bunny_health'
394
+ end
395
+ ```
396
+
397
+ **2. Configurar el Orquestador (Ejemplo docker-compose.yml):**
398
+ Con esta configuración, Docker Swarm verificará que el archivo haya sido modificado (tocado) en los últimos 15 segundos. Si el worker se bloquea o pierde la conexión de manera irrecuperable, Docker reiniciará el contenedor automáticamente.
399
+
400
+ ```yaml
401
+ services:
402
+ worker:
403
+ image: my_rails_app
404
+ command: bundle exec rake bug_bunny:work
405
+ healthcheck:
406
+ test: ["CMD-SHELL", "test $$(expr $$(date +%s) - $$(stat -c %Y /tmp/bug_bunny_health)) -lt 15 || exit 1"]
407
+ interval: 10s
408
+ timeout: 5s
409
+ retries: 3
410
+ ```
411
+
380
412
  ---
381
413
 
382
414
  ## 📄 Licencia
@@ -11,6 +11,7 @@ module BugBunny
11
11
  # BugBunny.configure do |config|
12
12
  # config.host = '127.0.0.1'
13
13
  # config.exchange_options = { durable: true, auto_delete: false }
14
+ # config.health_check_file = '/tmp/bug_bunny_health'
14
15
  # end
15
16
  class Configuration
16
17
  # @return [String] Host o IP del servidor RabbitMQ (ej: 'localhost').
@@ -64,6 +65,12 @@ module BugBunny
64
65
  # @return [Integer] Intervalo en segundos para verificar la salud de la cola.
65
66
  attr_accessor :health_check_interval
66
67
 
68
+ # @return [String, nil] Ruta del archivo que se actualizará (touch) en cada health check exitoso.
69
+ # Ideal para sondas (probes) de orquestadores como Docker Swarm o Kubernetes.
70
+ # Si es `nil`, la funcionalidad de touchfile se desactiva.
71
+ # @example '/tmp/bug_bunny_health'
72
+ attr_accessor :health_check_file
73
+
67
74
  # @return [String] Namespace base donde se buscarán los controladores (default: 'Rabbit::Controllers').
68
75
  attr_accessor :controller_namespace
69
76
 
@@ -108,6 +115,9 @@ module BugBunny
108
115
  @rpc_timeout = 10
109
116
  @health_check_interval = 60
110
117
 
118
+ # Desactivado por defecto. El usuario debe especificar una ruta explícita para habilitarlo.
119
+ @health_check_file = nil
120
+
111
121
  # Configuración por defecto para mantener compatibilidad
112
122
  @controller_namespace = 'Rabbit::Controllers'
113
123
 
@@ -1,10 +1,12 @@
1
- # lib/bug_bunny/consumer.rb
1
+ # frozen_string_literal: true
2
+
2
3
  require 'active_support/core_ext/string/inflections'
3
4
  require 'concurrent'
4
5
  require 'json'
5
6
  require 'uri'
6
7
  require 'cgi'
7
8
  require 'rack/utils' # Necesario para parse_nested_query
9
+ require 'fileutils' # Necesario para el touchfile del health check
8
10
 
9
11
  module BugBunny
10
12
  # Consumidor de mensajes AMQP que actúa como un Router RESTful.
@@ -177,44 +179,58 @@ module BugBunny
177
179
 
178
180
  # Interpreta la URL y el verbo para decidir qué controlador ejecutar.
179
181
  #
180
- # Utiliza `Rack::Utils.parse_nested_query` para soportar parámetros anidados
181
- # como `q[service]=rabbit`.
182
+ # Implementa un Router Heurístico que soporta namespaces y acciones custom
183
+ # buscando dinámicamente el ID en la ruta.
182
184
  #
183
185
  # @param method [String] Verbo HTTP (GET, POST, etc).
184
- # @param path [String] URL virtual del recurso (ej: 'users/1?active=true').
186
+ # @param path [String] URL virtual del recurso (ej: 'foo/bar/algo/13/test').
185
187
  # @return [Hash] Estructura con keys {:controller, :action, :id, :params}.
186
188
  def router_dispatch(method, path)
187
- # Usamos URI para separar path de query string
188
189
  uri = URI.parse("http://dummy/#{path}")
189
190
  segments = uri.path.split('/').reject(&:empty?)
190
191
 
191
- # --- FIX: Uso de Rack para soportar params anidados ---
192
192
  query_params = uri.query ? Rack::Utils.parse_nested_query(uri.query) : {}
193
-
194
- # Si estamos en Rails, convertimos a HashWithIndifferentAccess para comodidad
195
193
  if defined?(ActiveSupport::HashWithIndifferentAccess)
196
194
  query_params = query_params.with_indifferent_access
197
195
  end
198
196
 
199
- # Lógica de Ruteo Convencional
200
- controller_name = segments[0]
201
- id = segments[1]
202
-
203
- action = case method.to_s.upcase
204
- when 'GET' then id ? 'show' : 'index'
205
- when 'POST' then 'create'
206
- when 'PUT', 'PATCH' then 'update'
207
- when 'DELETE' then 'destroy'
208
- else id || 'index'
209
- end
210
-
211
- # Soporte para rutas miembro custom (POST users/1/promote)
212
- if segments.size >= 3
213
- id = segments[1]
214
- action = segments[2]
197
+ # 1. Acción Built-in: Health Check Global (/up o /api/up)
198
+ if segments.last == 'up' && method.to_s.upcase == 'GET'
199
+ # Si la ruta es solo 'up', usamos un controlador genérico 'application'
200
+ ctrl = segments.size > 1 ? segments[0...-1].join('/') : 'application'
201
+ return { controller: ctrl, action: 'up', id: nil, params: query_params }
202
+ end
203
+
204
+ # 2. Búsqueda dinámica del ID (Heurística)
205
+ # Patrón: Números enteros, UUIDs, o hashes alfanuméricos largos (MongoDB/Snowflake)
206
+ id_pattern = /^(?:\d+|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[0-9a-fA-F]{24})$/
207
+ id_index = segments.find_index { |s| s.match?(id_pattern) }
208
+
209
+ if id_index
210
+ # ESCENARIO A: Ruta Miembro (ej. foo/bar/algo/13/test)
211
+ # Todo lo que está antes del ID es el namespace/controlador
212
+ controller_name = segments[0...id_index].join('/') # "foo/bar/algo"
213
+ id = segments[id_index] # "13"
214
+ action = segments[id_index + 1] # "test" (puede ser nil)
215
+ else
216
+ # ESCENARIO B: Ruta Colección (ej. foo/bar/algo o api/v1/users)
217
+ controller_name = segments.join('/')
218
+ id = nil
219
+ action = nil
215
220
  end
216
221
 
217
- # Inyectamos el ID en los params si existe en la ruta
222
+ # 3. Inferimos la acción si no hay una explícita en la ruta
223
+ unless action
224
+ action = case method.to_s.upcase
225
+ when 'GET' then id ? 'show' : 'index'
226
+ when 'POST' then 'create'
227
+ when 'PUT', 'PATCH' then 'update'
228
+ when 'DELETE' then 'destroy'
229
+ else id ? 'show' : 'index'
230
+ end
231
+ end
232
+
233
+ # 4. Inyectamos el ID en los parámetros para que el Controller lo tenga fácil
218
234
  query_params['id'] = id if id
219
235
 
220
236
  { controller: controller_name, action: action, id: id, params: query_params }
@@ -253,14 +269,40 @@ module BugBunny
253
269
  # Tarea de fondo (Heartbeat lógico) para verificar la salud del canal.
254
270
  # Si la cola desaparece o la conexión se cierra, fuerza una reconexión.
255
271
  #
272
+ # Adicionalmente, si `health_check_file` está configurado, actualiza la
273
+ # fecha de modificación (touch) de dicho archivo para notificar a orquestadores
274
+ # externos (como Docker Swarm o Kubernetes) que el proceso está saludable.
275
+ #
256
276
  # @param q_name [String] Nombre de la cola a monitorear.
277
+ # @return [void]
257
278
  def start_health_check(q_name)
279
+ file_path = BugBunny.configuration.health_check_file
280
+
281
+ # Toque inicial para indicar al orquestador que el worker arrancó correctamente
282
+ touch_health_file(file_path) if file_path
283
+
258
284
  Concurrent::TimerTask.new(execution_interval: BugBunny.configuration.health_check_interval) do
285
+ # 1. Verificamos la salud de RabbitMQ (si falla, levanta un error y corta la ejecución del bloque)
259
286
  session.channel.queue_declare(q_name, passive: true)
260
- rescue StandardError
261
- BugBunny.configuration.logger.warn("[BugBunny::Consumer] ⚠️ Queue check failed. Reconnecting session...")
287
+
288
+ # 2. Si llegamos aquí, RabbitMQ y la cola están vivos. Avisamos al orquestador actualizando el archivo.
289
+ touch_health_file(file_path) if file_path
290
+ rescue StandardError => e
291
+ BugBunny.configuration.logger.warn("[BugBunny::Consumer] ⚠️ Queue check failed: #{e.message}. Reconnecting session...")
262
292
  session.close
263
293
  end.execute
264
294
  end
295
+
296
+ # Actualiza la fecha de modificación del archivo de health check (touchfile).
297
+ # Se utiliza un `rescue` genérico para no interrumpir el flujo principal del worker
298
+ # si el contenedor de Docker tiene problemas de permisos sobre la carpeta temporal.
299
+ #
300
+ # @param file_path [String] Ruta absoluta del archivo a tocar.
301
+ # @return [void]
302
+ def touch_health_file(file_path)
303
+ FileUtils.touch(file_path)
304
+ rescue StandardError => e
305
+ BugBunny.configuration.logger.error("[BugBunny::Consumer] ⚠️ Cannot touch health check file '#{file_path}': #{e.message}")
306
+ end
265
307
  end
266
308
  end
@@ -110,7 +110,7 @@ module BugBunny
110
110
  BugBunny.configuration.logger.info("[BugBunny::Producer] 📤 #{verb} /#{target} | RK: '#{rk}' | ID: #{id}")
111
111
 
112
112
  # DEBUG: Detalle completo de Infraestructura y Payload
113
- BugBunny.configuration.logger.debug("[BugBunny::Producer] ⚙️ Exchange Opts: #{final_x_opts}")
113
+ BugBunny.configuration.logger.debug("[BugBunny::Producer] ⚙️ Exchange #{request.exchange} | Opts: #{final_x_opts}")
114
114
  BugBunny.configuration.logger.debug("[BugBunny::Producer] 📦 Payload: #{payload.truncate(300)}") if payload.is_a?(String)
115
115
  end
116
116
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = "3.1.3"
4
+ VERSION = "3.1.5"
5
5
  end
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: 3.1.3
4
+ version: 3.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - gabix
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-02-19 00:00:00.000000000 Z
11
+ date: 2026-02-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny