bug_bunny 4.0.1 → 4.1.1

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: 4b15b464fc158fbab2ed65325af31aedda4160fa435762a87bba018a827714ba
4
- data.tar.gz: dfeca1173ed11ec9b7f9615b37ff596156988700cd95e7b155335771095d17dd
3
+ metadata.gz: 599004cdcb498ca746612f60202301ce9783a67ac37d14ecd4c29f51ccaf48bb
4
+ data.tar.gz: f748ba9a55e91f96a5571f671afa524f7e330cceede0f12c5c7cef6d25d2d2ff
5
5
  SHA512:
6
- metadata.gz: dc7e4d36f8f5570f134a81cd48d1aeffd647d6a08a6817397b6004ef82111d63f43df1ef01c685da537e1b65102f7abd9f32ea97f3c4ec79254365fad1a417eb
7
- data.tar.gz: 5fb2f92c8bec877f3ff81a0db8c50e2859737c5000e6c97a79935408f9a83486f1dc93b8218e5baeaad915e631e7d139a500f7c4d71b6c61d6a1c9f5e45fbf69
6
+ metadata.gz: 7df1503a55cb62d9f87168a84b8e92fe773b4c9525b143d6f848227e66fe63e5ab75f6191a408b00bb9c8840b44c0f8fdbc36cd0a8d2eb56e02018d09654d269
7
+ data.tar.gz: d5433d839c45ad95a2d0399535ff629112458e90d0e4e1ec89f97afa450b139bcb7855c64c27042c22bce0bde87b2bfa7bd6f8bae5b879dd7eb0c71ff1d20d3c
data/CHANGELOG.md CHANGED
@@ -1,4 +1,22 @@
1
1
  # Changelog
2
+
3
+ ## [4.1.1] - 2026-03-22
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\`.
8
+
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
+ ## [4.1.0] - 2026-03-22
14
+
15
+ ### 🚀 New Features & Improvements
16
+ * **Faraday-style Client API:** Se introdujo el método \`Client#send\` como punto de entrada genérico, permitiendo una sintaxis más familiar y flexible.
17
+ * **Flexible Delivery Modes:** Introducción del atributo \`delivery_mode\` (:rpc o :publish). Ahora es posible configurar la estrategia de envío a nivel de cliente o por cada petición individual.
18
+ * **Smart Request Defaults:** Los métodos \`request\` y \`publish\` ahora delegan internamente en \`send\`, manteniendo la compatibilidad pero beneficiándose de la nueva arquitectura de peticiones.
19
+
2
20
  ## [4.0.1] - 2026-03-13
3
21
 
4
22
  ### 🐛 Bug Fixes
data/README.md CHANGED
@@ -203,6 +203,26 @@ client = BugBunny::Client.new(pool: BUG_BUNNY_POOL) do |stack|
203
203
  stack.use BugBunny::Middleware::JsonResponse
204
204
  end
205
205
 
206
+ # 1. Método genérico 'send' (Estilo Faraday)
207
+ # El comportamiento (RPC o Fire-and-forget) depende de 'delivery_mode'
208
+ client.delivery_mode = :rpc # Default
209
+ client.send('users/1', method: :get)
210
+
211
+ # 2. Configuración flexible del modo de entrega
212
+ # Por cada petición
213
+ client.send('logs', method: :post, body: { msg: 'system_up' }, delivery_mode: :publish)
214
+
215
+ # O mediante un bloque para configuración avanzada
216
+ client.send('users/1') do |req|
217
+ req.method = :get
218
+ req.delivery_mode = :rpc
219
+ req.timeout = 5
220
+ end
221
+
222
+ # 3. Métodos de conveniencia (Atajos)
223
+ client.request('users/1') # Siempre :rpc
224
+ client.publish('events', body: { type: 'click' }) # Siempre :publish
225
+
206
226
  # Ahora el cliente devolverá Hashes y lanzará errores si el worker falla
207
227
  response = client.request('users/1', method: :get)
208
228
  ```
@@ -21,6 +21,9 @@ module BugBunny
21
21
  # @return [BugBunny::Middleware::Stack] La pila de middlewares configurada.
22
22
  attr_reader :stack
23
23
 
24
+ # @return [Symbol] El modo de entrega por defecto para este cliente (:rpc o :publish).
25
+ attr_accessor :delivery_mode
26
+
24
27
  # Inicializa un nuevo cliente.
25
28
  #
26
29
  # @param pool [ConnectionPool] Pool de conexiones a RabbitMQ configurado previamente.
@@ -30,9 +33,22 @@ module BugBunny
30
33
  raise ArgumentError, "BugBunny::Client requiere un 'pool:'" if pool.nil?
31
34
  @pool = pool
32
35
  @stack = BugBunny::Middleware::Stack.new
36
+ @delivery_mode = :rpc
33
37
  yield(@stack) if block_given?
34
38
  end
35
39
 
40
+ # Realiza una petición general al estilo Faraday.
41
+ # El comportamiento (RPC o Fire-and-forget) depende de {#delivery_mode}.
42
+ #
43
+ # @param url [String] La ruta del recurso.
44
+ # @param args [Hash] Opciones de configuración.
45
+ # @yield [req] Bloque para configurar el objeto Request directamente.
46
+ def send(url, **args)
47
+ run_in_pool(url, args) do |req|
48
+ yield req if block_given?
49
+ end
50
+ end
51
+
36
52
  # Realiza una petición Síncrona (RPC / Request-Response).
37
53
  #
38
54
  # Envía un mensaje y bloquea la ejecución del hilo actual hasta recibir respuesta.
@@ -48,7 +64,8 @@ module BugBunny
48
64
  # @yield [req] Bloque para configurar el objeto Request directamente.
49
65
  # @return [Hash] La respuesta del servidor.
50
66
  def request(url, **args)
51
- run_in_pool(:rpc, url, args) do |req|
67
+ send(url, **args) do |req|
68
+ req.delivery_mode = :rpc
52
69
  yield req if block_given?
53
70
  end
54
71
  end
@@ -60,7 +77,8 @@ module BugBunny
60
77
  # @yield [req] Bloque para configurar el objeto Request.
61
78
  # @return [void]
62
79
  def publish(url, **args)
63
- run_in_pool(:fire, url, args) do |req|
80
+ send(url, **args) do |req|
81
+ req.delivery_mode = :publish
64
82
  yield req if block_given?
65
83
  end
66
84
  end
@@ -70,15 +88,16 @@ module BugBunny
70
88
  # Ejecuta la lógica de envío dentro del contexto del Pool.
71
89
  # Mapea los argumentos al objeto Request y ejecuta la cadena de middlewares.
72
90
  #
73
- # @param method_name [Symbol] El método del productor a llamar (:rpc o :fire).
74
91
  # @param url [String] La ruta destino.
75
92
  # @param args [Hash] Argumentos pasados a los métodos públicos.
76
93
  # @yield [req] Bloque para configuración adicional del Request.
77
- def run_in_pool(method_name, url, args)
94
+ def run_in_pool(url, args)
78
95
  # 1. Builder del Request
79
96
  req = BugBunny::Request.new(url)
80
97
 
81
98
  # 2. Syntactic Sugar: Mapeo de argumentos a atributos del Request
99
+ req.delivery_mode = delivery_mode # Default del cliente
100
+ req.delivery_mode = args[:delivery_mode] if args[:delivery_mode]
82
101
  req.method = args[:method] if args[:method]
83
102
  req.body = args[:body] if args[:body]
84
103
  req.exchange = args[:exchange] if args[:exchange]
@@ -101,6 +120,10 @@ module BugBunny
101
120
  producer = BugBunny::Producer.new(session)
102
121
 
103
122
  begin
123
+ # Mapeo de delivery_mode al método del productor (:rpc o :fire)
124
+ # :publish se mapea a :fire por consistencia interna.
125
+ method_name = req.delivery_mode == :publish ? :fire : :rpc
126
+
104
127
  # Onion Architecture: La acción final es llamar al Producer real.
105
128
  final_action = ->(env) { producer.send(method_name, env) }
106
129
 
@@ -44,6 +44,7 @@ module BugBunny
44
44
  # @param connection [Bunny::Session] Conexión nativa de Bunny.
45
45
  def initialize(connection)
46
46
  @session = BugBunny::Session.new(connection)
47
+ @health_timer = nil
47
48
  end
48
49
 
49
50
  # Inicia la suscripción a la cola y comienza el bucle de procesamiento.
@@ -244,12 +245,16 @@ module BugBunny
244
245
  # @param q_name [String] Nombre de la cola a monitorear.
245
246
  # @return [void]
246
247
  def start_health_check(q_name)
248
+ # Detener el timer anterior antes de crear uno nuevo (evita leak en cada retry)
249
+ @health_timer&.shutdown
250
+ @health_timer = nil
251
+
247
252
  file_path = BugBunny.configuration.health_check_file
248
253
 
249
254
  # Toque inicial para indicar al orquestador que el worker arrancó correctamente
250
255
  touch_health_file(file_path) if file_path
251
256
 
252
- Concurrent::TimerTask.new(execution_interval: BugBunny.configuration.health_check_interval) do
257
+ @health_timer = Concurrent::TimerTask.new(execution_interval: BugBunny.configuration.health_check_interval) do
253
258
  # 1. Verificamos la salud de RabbitMQ (si falla, levanta un error y corta la ejecución del bloque)
254
259
  session.channel.queue_declare(q_name, passive: true)
255
260
 
@@ -258,7 +263,8 @@ module BugBunny
258
263
  rescue StandardError => e
259
264
  BugBunny.configuration.logger.warn("[BugBunny::Consumer] ⚠️ Queue check failed: #{e.message}. Reconnecting session...")
260
265
  session.close
261
- end.execute
266
+ end
267
+ @health_timer.execute
262
268
  end
263
269
 
264
270
  # Actualiza la fecha de modificación del archivo de health check (touchfile).
@@ -146,11 +146,6 @@ module BugBunny
146
146
  def process(body)
147
147
  prepare_params(body)
148
148
 
149
- # Inyección de configuración global de logs si el controlador no define propios
150
- if self.class.log_tags.empty? && BugBunny.configuration.log_tags.any?
151
- self.class.log_tags = BugBunny.configuration.log_tags
152
- end
153
-
154
149
  action_name = headers[:action].to_sym
155
150
  current_arounds = resolve_callbacks(self.class.around_actions, action_name)
156
151
 
@@ -245,12 +240,11 @@ module BugBunny
245
240
  if body.is_a?(Hash)
246
241
  params.merge!(body)
247
242
  elsif body.is_a?(String) && headers[:content_type].to_s.include?('json')
248
- parsed = begin
249
- JSON.parse(body)
250
- rescue JSON::ParserError
251
- nil
252
- end
253
- params.merge!(parsed) if parsed
243
+ begin
244
+ params.merge!(JSON.parse(body))
245
+ rescue JSON::ParserError => e
246
+ raise BugBunny::BadRequest, "Invalid JSON in request body: #{e.message}"
247
+ end
254
248
  else
255
249
  self.raw_string = body
256
250
  end
@@ -284,7 +278,8 @@ module BugBunny
284
278
  end
285
279
 
286
280
  def compute_tags
287
- self.class.log_tags.map do |tag|
281
+ tags = self.class.log_tags.presence || BugBunny.configuration.log_tags
282
+ tags.map do |tag|
288
283
  case tag
289
284
  when Proc
290
285
  tag.call(self)
@@ -16,6 +16,7 @@ module BugBunny
16
16
  # @attr routing_key [String] La routing key específica. Si es nil, se usará {#path}.
17
17
  # @attr timeout [Integer] Tiempo máximo en segundos para timeout RPC.
18
18
  #
19
+ # @attr delivery_mode [Symbol] El modo de entrega (:rpc o :publish).
19
20
  # @attr exchange_options [Hash] Opciones específicas para la declaración del Exchange en esta petición.
20
21
  # @attr queue_options [Hash] Opciones específicas para la declaración de la Cola en esta petición.
21
22
  class Request
@@ -27,6 +28,7 @@ module BugBunny
27
28
  attr_accessor :exchange_type
28
29
  attr_accessor :routing_key
29
30
  attr_accessor :timeout
31
+ attr_accessor :delivery_mode
30
32
 
31
33
  # Configuración de Infraestructura Específica
32
34
  attr_accessor :exchange_options
@@ -48,12 +50,12 @@ module BugBunny
48
50
  @timestamp = Time.now.to_i
49
51
  @persistent = false
50
52
  @exchange_type = 'direct'
53
+ @delivery_mode = :rpc
51
54
 
52
55
  # Inicialización de opciones de infraestructura para evitar errores de nil durante el merge.
53
56
  @exchange_options = {}
54
57
  @queue_options = {}
55
58
  end
56
-
57
59
  # Calcula la Routing Key final que se usará en RabbitMQ.
58
60
  #
59
61
  # Principio: "Convention over Configuration".
@@ -154,9 +154,24 @@ module BugBunny
154
154
  end
155
155
 
156
156
  # Proxy para el encadenamiento del método `.with`.
157
+ # Solo puede usarse para UNA llamada de método: el contexto se restaura al finalizar.
157
158
  class ScopeProxy < BasicObject
158
- def initialize(target, keys, old_values); @target = target; @keys = keys; @old_values = old_values; end
159
- def method_missing(method, *args, &block); @target.public_send(method, *args, &block); ensure; @keys.each { |k, v| ::Thread.current[v] = @old_values[k] }; end
159
+ def initialize(target, keys, old_values)
160
+ @target = target
161
+ @keys = keys
162
+ @old_values = old_values
163
+ @used = false
164
+ end
165
+
166
+ def method_missing(method, *args, &block)
167
+ if @used
168
+ ::Kernel.raise ::BugBunny::Error, "ScopeProxy is single-use. Call .with again for a new context."
169
+ end
170
+ @used = true
171
+ @target.public_send(method, *args, &block)
172
+ ensure
173
+ @keys.each { |k, v| ::Thread.current[v] = @old_values[k] }
174
+ end
160
175
  end
161
176
 
162
177
  # Calcula la routing key final.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = "4.0.1"
4
+ VERSION = "4.1.1"
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: 4.0.1
4
+ version: 4.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - gabix
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-13 00:00:00.000000000 Z
11
+ date: 2026-03-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny