bug_bunny 3.0.6 → 3.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.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'concurrent'
2
4
  require 'json'
3
5
  require 'securerandom'
@@ -27,39 +29,30 @@ module BugBunny
27
29
 
28
30
  # Envía un mensaje de forma asíncrona (Fire-and-Forget).
29
31
  #
30
- # Serializa el cuerpo del request, resuelve el exchange y publica el mensaje
31
- # sin esperar confirmación ni respuesta del consumidor.
32
+ # Serializa el cuerpo del request, resuelve el exchange aplicando la cascada de
33
+ # configuración y publica el mensaje sin esperar respuesta.
32
34
  #
33
- # @param request [BugBunny::Request] Objeto con la configuración del envío (body, routing_key, etc).
35
+ # @param request [BugBunny::Request] Objeto con la configuración del envío (body, exchange_options, etc).
34
36
  # @return [void]
35
37
  def fire(request)
36
- x = @session.exchange(name: request.exchange, type: request.exchange_type)
38
+ # Obtenemos el exchange pasando las opciones específicas del request para la fusión en cascada
39
+ x = @session.exchange(
40
+ name: request.exchange,
41
+ type: request.exchange_type,
42
+ opts: request.exchange_options
43
+ )
44
+
37
45
  payload = serialize_message(request.body)
38
46
  opts = request.amqp_options
39
47
 
40
- # LOG ESTRUCTURADO Y LEGIBLE
41
- # Muestra claramente: Verbo, Recurso, Exchange (y su tipo) y la Routing Key usada.
42
- verb = request.method.to_s.upcase
43
- target = request.path
44
- ex_info = "'#{request.exchange}' (Type: #{request.exchange_type})"
45
- rk = request.final_routing_key
46
-
47
- BugBunny.configuration.logger.info("[BugBunny] [#{verb}] '/#{target}' | Exchange: #{ex_info} | Routing Key: '#{rk}'")
48
+ log_request(request, payload)
48
49
 
49
50
  x.publish(payload, opts.merge(routing_key: request.final_routing_key))
50
51
  end
51
52
 
52
53
  # Envía un mensaje y bloquea el hilo actual esperando una respuesta (RPC).
53
54
  #
54
- # Implementa el mecanismo "Direct Reply-to" de RabbitMQ (`amq.rabbitmq.reply-to`)
55
- # para recibir la respuesta directamente sin necesidad de crear colas temporales
56
- # por cada petición, lo cual mejora significativamente el rendimiento.
57
- #
58
- # El flujo es:
59
- # 1. Asegura que hay un consumidor escuchando en `amq.rabbitmq.reply-to`.
60
- # 2. Genera un `correlation_id` único.
61
- # 3. Crea una promesa (`Concurrent::IVar`) y la registra.
62
- # 4. Publica el mensaje y bloquea esperando que la promesa se resuelva.
55
+ # Implementa el mecanismo "Direct Reply-to" de RabbitMQ (`amq.rabbitmq.reply-to`).
63
56
  #
64
57
  # @param request [BugBunny::Request] Objeto request configurado.
65
58
  # @return [Hash] El cuerpo de la respuesta parseado desde JSON.
@@ -71,32 +64,48 @@ module BugBunny
71
64
  request.correlation_id ||= SecureRandom.uuid
72
65
  request.reply_to = 'amq.rabbitmq.reply-to'
73
66
  wait_timeout = request.timeout || BugBunny.configuration.rpc_timeout
67
+ cid = request.correlation_id.to_s
74
68
 
75
69
  # Creamos un futuro (IVar) que actuará como semáforo
76
70
  future = Concurrent::IVar.new
77
- @pending_requests[request.correlation_id] = future
71
+ @pending_requests[cid] = future
78
72
 
79
73
  begin
80
74
  fire(request)
81
75
 
76
+ BugBunny.configuration.logger.debug("[BugBunny::Producer] ⏳ Waiting for RPC response | ID: #{cid} | Timeout: #{wait_timeout}s")
77
+
82
78
  # Bloqueamos el hilo aquí hasta que llegue la respuesta o expire el timeout
83
79
  response_payload = future.value(wait_timeout)
84
80
 
85
81
  if response_payload.nil?
86
- # CORRECCIÓN: Usamos request.path y request.method en lugar de request.action
87
82
  raise BugBunny::RequestTimeout, "Timeout waiting for RPC: #{request.path} [#{request.method}]"
88
83
  end
89
84
 
90
85
  parse_response(response_payload)
91
86
  ensure
92
87
  # Limpieza vital para evitar fugas de memoria en el mapa
93
- @pending_requests.delete(request.correlation_id)
88
+ @pending_requests.delete(cid)
94
89
  end
95
90
  end
96
91
 
97
92
  private
98
93
 
94
+ def log_request(request, payload)
95
+ verb = request.method.to_s.upcase
96
+ target = request.path
97
+ rk = request.final_routing_key
98
+ id = request.correlation_id
99
+
100
+ # INFO: Resumen de una línea (Traffic)
101
+ BugBunny.configuration.logger.info("[BugBunny::Producer] 📤 #{verb} /#{target} | RK: '#{rk}' | ID: #{id}")
102
+
103
+ # DEBUG: Detalle completo (Payload)
104
+ BugBunny.configuration.logger.debug("[BugBunny::Producer] 📦 Payload: #{payload.truncate(300)}") if payload.is_a?(String)
105
+ end
106
+
99
107
  # Serializa el mensaje para su transporte.
108
+ #
100
109
  # @param msg [Hash, String, Object] El mensaje a serializar.
101
110
  # @return [String] Cadena JSON o string crudo.
102
111
  def serialize_message(msg)
@@ -104,6 +113,9 @@ module BugBunny
104
113
  end
105
114
 
106
115
  # Intenta parsear la respuesta recibida.
116
+ #
117
+ # @param payload [String] El cuerpo de la respuesta recibida.
118
+ # @return [Hash] El JSON parseado.
107
119
  # @raise [BugBunny::InternalServerError] Si el payload no es JSON válido.
108
120
  def parse_response(payload)
109
121
  JSON.parse(payload)
@@ -114,21 +126,25 @@ module BugBunny
114
126
  # Inicia el consumidor de respuestas RPC de forma perezosa (Lazy Initialization).
115
127
  #
116
128
  # Utiliza un patrón de "Double-Checked Locking" con Mutex para asegurar que
117
- # solo se crea un listener por instancia de Producer, incluso en entornos multi-hilo.
129
+ # solo se crea un listener por instancia de Producer.
118
130
  #
119
- # Escucha en la pseudo-cola `amq.rabbitmq.reply-to`. Cuando llega un mensaje,
120
- # busca el `correlation_id` en el mapa de pendientes y completa el futuro (`IVar`),
121
- # desbloqueando así al hilo que llamó a {#rpc}.
131
+ # @return [void]
122
132
  def ensure_reply_listener!
123
133
  return if @reply_listener_started
124
134
 
125
135
  @reply_listener_mutex.synchronize do
126
136
  return if @reply_listener_started
127
137
 
138
+ BugBunny.configuration.logger.debug("[BugBunny::Producer] 👂 Starting Reply Listener on 'amq.rabbitmq.reply-to'")
139
+
128
140
  # Consumimos sin ack (auto-ack) porque reply-to no soporta acks manuales de forma estándar
129
141
  @session.channel.basic_consume('amq.rabbitmq.reply-to', '', true, false, nil) do |_, props, body|
130
- if (future = @pending_requests[props.correlation_id])
142
+ cid = props.correlation_id.to_s
143
+ BugBunny.configuration.logger.debug("[BugBunny::Producer] 📥 RPC Response matched | ID: #{cid}")
144
+ if (future = @pending_requests[cid])
131
145
  future.set(body)
146
+ else
147
+ BugBunny.configuration.logger.warn("[BugBunny::Producer] ⚠️ Orphaned RPC Response received | ID: #{cid}")
132
148
  end
133
149
  end
134
150
  @reply_listener_started = true
@@ -1,10 +1,11 @@
1
- # lib/bug_bunny/request.rb
1
+ # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
4
  # Encapsula toda la información necesaria para realizar una petición o publicación.
5
5
  #
6
6
  # Actúa como el objeto "Environment" en la arquitectura de middlewares.
7
- # Contiene el cuerpo del mensaje, la configuración de enrutamiento y el **Verbo HTTP**.
7
+ # Contiene el cuerpo del mensaje, la configuración de enrutamiento, el **Verbo HTTP**
8
+ # y las opciones de infraestructura específicas para la petición.
8
9
  #
9
10
  # @attr body [Object] El cuerpo del mensaje (Hash, Array o String).
10
11
  # @attr headers [Hash] Cabeceras personalizadas (Headers AMQP).
@@ -14,6 +15,9 @@ module BugBunny
14
15
  # @attr exchange_type [String] El tipo de exchange ('direct', 'topic', 'fanout').
15
16
  # @attr routing_key [String] La routing key específica. Si es nil, se usará {#path}.
16
17
  # @attr timeout [Integer] Tiempo máximo en segundos para timeout RPC.
18
+ #
19
+ # @attr exchange_options [Hash] Opciones específicas para la declaración del Exchange en esta petición.
20
+ # @attr queue_options [Hash] Opciones específicas para la declaración de la Cola en esta petición.
17
21
  class Request
18
22
  attr_accessor :body
19
23
  attr_accessor :headers
@@ -24,6 +28,10 @@ module BugBunny
24
28
  attr_accessor :routing_key
25
29
  attr_accessor :timeout
26
30
 
31
+ # Configuración de Infraestructura Específica
32
+ attr_accessor :exchange_options
33
+ attr_accessor :queue_options
34
+
27
35
  # Metadatos AMQP Estándar
28
36
  attr_accessor :app_id, :content_type, :content_encoding, :priority,
29
37
  :timestamp, :expiration, :persistent, :reply_to,
@@ -40,6 +48,10 @@ module BugBunny
40
48
  @timestamp = Time.now.to_i
41
49
  @persistent = false
42
50
  @exchange_type = 'direct'
51
+
52
+ # Inicialización de opciones de infraestructura para evitar errores de nil durante el merge.
53
+ @exchange_options = {}
54
+ @queue_options = {}
43
55
  end
44
56
 
45
57
  # Calcula la Routing Key final que se usará en RabbitMQ.