bug_bunny 3.1.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +182 -350
- data/lib/bug_bunny/client.rb +26 -11
- data/lib/bug_bunny/configuration.rb +28 -2
- data/lib/bug_bunny/consumer.rb +13 -14
- data/lib/bug_bunny/controller.rb +2 -2
- data/lib/bug_bunny/producer.rb +41 -32
- data/lib/bug_bunny/request.rb +14 -2
- data/lib/bug_bunny/resource.rb +135 -16
- data/lib/bug_bunny/session.rb +47 -18
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +1 -1
- data/test/integration/infrastructure_test.rb +61 -0
- data/test/integration/manual_client_test.rb +203 -0
- data/test/test_helper.rb +96 -11
- metadata +18 -16
- data/bin_client.rb +0 -51
- data/bin_suite.rb +0 -106
- data/bin_worker.rb +0 -26
- data/test/integration/fire_and_forget_test.rb +0 -76
- data/test/integration/rpc_flow_test.rb +0 -78
- data/test/unit/configuration_test.rb +0 -40
- data/test/unit/consumer_test.rb +0 -44
- data/test/unit/controller_headers_test.rb +0 -38
- data/test/unit/hybrid_resource_test.rb +0 -60
- data/test/unit/middleware_test.rb +0 -61
- data/test/unit/resource_test.rb +0 -49
- data/test_controller.rb +0 -49
- data/test_helper.rb +0 -20
- data/test_resource.rb +0 -19
data/lib/bug_bunny/client.rb
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require_relative 'middleware/stack'
|
|
3
4
|
|
|
4
5
|
module BugBunny
|
|
5
6
|
# Cliente principal para realizar peticiones a RabbitMQ.
|
|
6
7
|
#
|
|
7
8
|
# Implementa el patrón "Onion Middleware" (Arquitectura de Cebolla) similar a Faraday.
|
|
8
|
-
# Mantiene una interfaz flexible donde el verbo HTTP se pasa como opción
|
|
9
|
+
# Mantiene una interfaz flexible donde el verbo HTTP se pasa como opción y permite
|
|
10
|
+
# configurar la infraestructura AMQP de forma granular por petición.
|
|
9
11
|
#
|
|
10
|
-
# @example Petición RPC (GET)
|
|
11
|
-
# client.request('users/123', method: :get)
|
|
12
|
+
# @example Petición RPC (GET) con opciones de infraestructura
|
|
13
|
+
# client.request('users/123', method: :get, exchange_options: { durable: true })
|
|
12
14
|
#
|
|
13
15
|
# @example Publicación Fire-and-Forget (POST)
|
|
14
16
|
# client.publish('logs', method: :post, body: { msg: 'Error' })
|
|
@@ -41,6 +43,8 @@ module BugBunny
|
|
|
41
43
|
# @option args [Object] :body El cuerpo del mensaje.
|
|
42
44
|
# @option args [Hash] :headers Headers AMQP adicionales.
|
|
43
45
|
# @option args [Integer] :timeout Tiempo máximo de espera.
|
|
46
|
+
# @option args [Hash] :exchange_options Opciones específicas para la declaración del Exchange.
|
|
47
|
+
# @option args [Hash] :queue_options Opciones específicas para la declaración de la Cola.
|
|
44
48
|
# @yield [req] Bloque para configurar el objeto Request directamente.
|
|
45
49
|
# @return [Hash] La respuesta del servidor.
|
|
46
50
|
def request(url, **args)
|
|
@@ -65,18 +69,28 @@ module BugBunny
|
|
|
65
69
|
|
|
66
70
|
# Ejecuta la lógica de envío dentro del contexto del Pool.
|
|
67
71
|
# Mapea los argumentos al objeto Request y ejecuta la cadena de middlewares.
|
|
72
|
+
#
|
|
73
|
+
# @param method_name [Symbol] El método del productor a llamar (:rpc o :fire).
|
|
74
|
+
# @param url [String] La ruta destino.
|
|
75
|
+
# @param args [Hash] Argumentos pasados a los métodos públicos.
|
|
76
|
+
# @yield [req] Bloque para configuración adicional del Request.
|
|
68
77
|
def run_in_pool(method_name, url, args)
|
|
69
78
|
# 1. Builder del Request
|
|
70
79
|
req = BugBunny::Request.new(url)
|
|
71
80
|
|
|
72
81
|
# 2. Syntactic Sugar: Mapeo de argumentos a atributos del Request
|
|
73
|
-
req.method
|
|
74
|
-
req.body
|
|
75
|
-
req.exchange
|
|
76
|
-
req.exchange_type
|
|
77
|
-
req.routing_key
|
|
78
|
-
req.timeout
|
|
79
|
-
|
|
82
|
+
req.method = args[:method] if args[:method]
|
|
83
|
+
req.body = args[:body] if args[:body]
|
|
84
|
+
req.exchange = args[:exchange] if args[:exchange]
|
|
85
|
+
req.exchange_type = args[:exchange_type] if args[:exchange_type]
|
|
86
|
+
req.routing_key = args[:routing_key] if args[:routing_key]
|
|
87
|
+
req.timeout = args[:timeout] if args[:timeout]
|
|
88
|
+
|
|
89
|
+
# Inyección de opciones de infraestructura (Nivel 3 de la cascada)
|
|
90
|
+
req.exchange_options = args[:exchange_options] if args[:exchange_options]
|
|
91
|
+
req.queue_options = args[:queue_options] if args[:queue_options]
|
|
92
|
+
|
|
93
|
+
req.headers.merge!(args[:headers]) if args[:headers]
|
|
80
94
|
|
|
81
95
|
# 3. Configuración del usuario (bloque específico por request)
|
|
82
96
|
yield req if block_given?
|
|
@@ -94,6 +108,7 @@ module BugBunny
|
|
|
94
108
|
app = @stack.build(final_action)
|
|
95
109
|
app.call(req)
|
|
96
110
|
ensure
|
|
111
|
+
# Aseguramos el cierre del canal pero mantenemos la conexión del pool
|
|
97
112
|
session.close
|
|
98
113
|
end
|
|
99
114
|
end
|
|
@@ -4,7 +4,14 @@ require 'logger'
|
|
|
4
4
|
|
|
5
5
|
module BugBunny
|
|
6
6
|
# Clase de configuración global para la gema BugBunny.
|
|
7
|
-
# Almacena las credenciales de conexión, timeouts y parámetros de ajuste de RabbitMQ
|
|
7
|
+
# Almacena las credenciales de conexión, timeouts y parámetros de ajuste de RabbitMQ,
|
|
8
|
+
# así como las opciones por defecto para la declaración de infraestructura AMQP.
|
|
9
|
+
#
|
|
10
|
+
# @example Configuración en un inicializador (e.g., config/initializers/bug_bunny.rb)
|
|
11
|
+
# BugBunny.configure do |config|
|
|
12
|
+
# config.host = '127.0.0.1'
|
|
13
|
+
# config.exchange_options = { durable: true, auto_delete: false }
|
|
14
|
+
# end
|
|
8
15
|
class Configuration
|
|
9
16
|
# @return [String] Host o IP del servidor RabbitMQ (ej: 'localhost').
|
|
10
17
|
attr_accessor :host
|
|
@@ -60,9 +67,23 @@ module BugBunny
|
|
|
60
67
|
# @return [String] Namespace base donde se buscarán los controladores (default: 'Rabbit::Controllers').
|
|
61
68
|
attr_accessor :controller_namespace
|
|
62
69
|
|
|
63
|
-
# @return [Array<Symbol, Proc, String>]
|
|
70
|
+
# @return [Array<Symbol, Proc, String>] Etiquetas para el log estructurado.
|
|
64
71
|
attr_accessor :log_tags
|
|
65
72
|
|
|
73
|
+
# @!group Configuración de Infraestructura Global
|
|
74
|
+
|
|
75
|
+
# @return [Hash] Opciones globales por defecto para la declaración de Exchanges.
|
|
76
|
+
# Estas opciones se fusionarán con los valores por defecto de la gema y las específicas del recurso.
|
|
77
|
+
# @example { durable: true, auto_delete: false }
|
|
78
|
+
attr_accessor :exchange_options
|
|
79
|
+
|
|
80
|
+
# @return [Hash] Opciones globales por defecto para la declaración de Colas.
|
|
81
|
+
# Estas opciones se fusionarán con los valores por defecto de la gema y las específicas del recurso.
|
|
82
|
+
# @example { durable: true, exclusive: false }
|
|
83
|
+
attr_accessor :queue_options
|
|
84
|
+
|
|
85
|
+
# @!endgroup
|
|
86
|
+
|
|
66
87
|
# Inicializa la configuración con valores por defecto seguros.
|
|
67
88
|
def initialize
|
|
68
89
|
@host = '127.0.0.1'
|
|
@@ -91,9 +112,14 @@ module BugBunny
|
|
|
91
112
|
@controller_namespace = 'Rabbit::Controllers'
|
|
92
113
|
|
|
93
114
|
@log_tags = [:uuid]
|
|
115
|
+
|
|
116
|
+
# Inicialización de opciones de infraestructura como hashes vacíos para permitir fusiones posteriores.
|
|
117
|
+
@exchange_options = {}
|
|
118
|
+
@queue_options = {}
|
|
94
119
|
end
|
|
95
120
|
|
|
96
121
|
# Construye la URL de conexión AMQP basada en los atributos configurados.
|
|
122
|
+
# @return [String] URL formateada amqp://user:pass@host:port/vhost
|
|
97
123
|
def url
|
|
98
124
|
"amqp://#{username}:#{password}@#{host}:#{port}/#{vhost}"
|
|
99
125
|
end
|
data/lib/bug_bunny/consumer.rb
CHANGED
|
@@ -61,7 +61,7 @@ module BugBunny
|
|
|
61
61
|
q = session.queue(queue_name, queue_opts)
|
|
62
62
|
q.bind(x, routing_key: routing_key)
|
|
63
63
|
|
|
64
|
-
BugBunny.configuration.logger.info("[Consumer] Listening on #{queue_name}
|
|
64
|
+
BugBunny.configuration.logger.info("[BugBunny::Consumer] 🎧 Listening on '#{queue_name}' | Exchange: '#{exchange_name}' | Routing Key: '#{routing_key}'")
|
|
65
65
|
start_health_check(queue_name)
|
|
66
66
|
|
|
67
67
|
q.subscribe(manual_ack: true, block: block) do |delivery_info, properties, body|
|
|
@@ -78,7 +78,7 @@ module BugBunny
|
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
rescue StandardError => e
|
|
81
|
-
BugBunny.configuration.logger.error("[Consumer] Connection Error: #{e.message}. Retrying...")
|
|
81
|
+
BugBunny.configuration.logger.error("[BugBunny::Consumer] 💥 Connection Error: #{e.message}. Retrying in #{BugBunny.configuration.network_recovery_interval}s...")
|
|
82
82
|
sleep BugBunny.configuration.network_recovery_interval
|
|
83
83
|
retry
|
|
84
84
|
end
|
|
@@ -94,15 +94,11 @@ module BugBunny
|
|
|
94
94
|
# @param body [String] El payload crudo del mensaje.
|
|
95
95
|
# @return [void]
|
|
96
96
|
def process_message(delivery_info, properties, body)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
path = properties.type
|
|
100
|
-
if path.nil? || path.empty?
|
|
101
|
-
path = properties.headers ? properties.headers['path'] : nil
|
|
102
|
-
end
|
|
97
|
+
# 1. Validación de Headers
|
|
98
|
+
path = properties.type || (properties.headers && properties.headers['path'])
|
|
103
99
|
|
|
104
100
|
if path.nil? || path.empty?
|
|
105
|
-
BugBunny.configuration.logger.error("[Consumer] Missing 'type'
|
|
101
|
+
BugBunny.configuration.logger.error("[BugBunny::Consumer] ⛔ Rejected: Missing 'type' header.")
|
|
106
102
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
107
103
|
return
|
|
108
104
|
end
|
|
@@ -114,6 +110,9 @@ module BugBunny
|
|
|
114
110
|
# 3. Router: Inferencia de Controlador y Acción
|
|
115
111
|
route_info = router_dispatch(http_method, path)
|
|
116
112
|
|
|
113
|
+
BugBunny.configuration.logger.info("[BugBunny::Consumer] 📥 Started #{http_method} \"/#{path}\" for Routing Key: #{delivery_info.routing_key}")
|
|
114
|
+
BugBunny.configuration.logger.debug("[BugBunny::Consumer] 📦 Body: #{body.truncate(200)}")
|
|
115
|
+
|
|
117
116
|
request_metadata = {
|
|
118
117
|
type: path,
|
|
119
118
|
http_method: http_method,
|
|
@@ -133,7 +132,7 @@ module BugBunny
|
|
|
133
132
|
controller_name = route_info[:controller].camelize
|
|
134
133
|
|
|
135
134
|
# Construcción: "Messaging::Handlers" + "::" + "Users"
|
|
136
|
-
controller_class_name = "#{namespace}::#{controller_name}"
|
|
135
|
+
controller_class_name = "#{namespace}::#{controller_name}Controller"
|
|
137
136
|
|
|
138
137
|
controller_class = controller_class_name.constantize
|
|
139
138
|
|
|
@@ -141,7 +140,7 @@ module BugBunny
|
|
|
141
140
|
raise BugBunny::SecurityError, "Class #{controller_class} is not a valid BugBunny Controller"
|
|
142
141
|
end
|
|
143
142
|
rescue NameError => _e
|
|
144
|
-
BugBunny.configuration.logger.
|
|
143
|
+
BugBunny.configuration.logger.warn("[BugBunny::Consumer] ⚠️ Controller not found: #{controller_class_name} (Path: #{path})")
|
|
145
144
|
handle_fatal_error(properties, 404, "Not Found", "Controller #{controller_class_name} not found")
|
|
146
145
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
147
146
|
return
|
|
@@ -159,7 +158,7 @@ module BugBunny
|
|
|
159
158
|
session.channel.ack(delivery_info.delivery_tag)
|
|
160
159
|
|
|
161
160
|
rescue StandardError => e
|
|
162
|
-
BugBunny.configuration.logger.error("[Consumer] Execution Error: #{e.message}")
|
|
161
|
+
BugBunny.configuration.logger.error("[BugBunny::Consumer] 💥 Execution Error (#{e.class}): #{e.message}")
|
|
163
162
|
handle_fatal_error(properties, 500, "Internal Server Error", e.message)
|
|
164
163
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
165
164
|
end
|
|
@@ -216,7 +215,7 @@ module BugBunny
|
|
|
216
215
|
# @param correlation_id [String] ID para correlacionar la respuesta con la petición original.
|
|
217
216
|
# @return [void]
|
|
218
217
|
def reply(payload, reply_to, correlation_id)
|
|
219
|
-
BugBunny.configuration.logger.debug("[Consumer] 📤
|
|
218
|
+
BugBunny.configuration.logger.debug("[BugBunny::Consumer] 📤 Sending RPC Reply to #{reply_to} | ID: #{correlation_id}")
|
|
220
219
|
session.channel.default_exchange.publish(
|
|
221
220
|
payload.to_json,
|
|
222
221
|
routing_key: reply_to,
|
|
@@ -247,7 +246,7 @@ module BugBunny
|
|
|
247
246
|
Concurrent::TimerTask.new(execution_interval: BugBunny.configuration.health_check_interval) do
|
|
248
247
|
session.channel.queue_declare(q_name, passive: true)
|
|
249
248
|
rescue StandardError
|
|
250
|
-
BugBunny.configuration.logger.warn("[Consumer] Queue check failed. Reconnecting session...")
|
|
249
|
+
BugBunny.configuration.logger.warn("[BugBunny::Consumer] ⚠️ Queue check failed. Reconnecting session...")
|
|
251
250
|
session.close
|
|
252
251
|
end.execute
|
|
253
252
|
end
|
data/lib/bug_bunny/controller.rb
CHANGED
|
@@ -167,8 +167,8 @@ module BugBunny
|
|
|
167
167
|
return rendered_response if rendered_response
|
|
168
168
|
end
|
|
169
169
|
|
|
170
|
-
BugBunny.configuration.logger.error("Controller
|
|
171
|
-
BugBunny.configuration.logger.error(exception.backtrace.join("\n"))
|
|
170
|
+
BugBunny.configuration.logger.error("[BugBunny::Controller] 💥 Unhandled Exception (#{exception.class}): #{exception.message}")
|
|
171
|
+
BugBunny.configuration.logger.error(exception.backtrace.first(5).join("\n")) # Limitamos a 5 líneas para no ensuciar
|
|
172
172
|
|
|
173
173
|
{
|
|
174
174
|
status: 500,
|
data/lib/bug_bunny/producer.rb
CHANGED
|
@@ -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
|
|
31
|
-
#
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
@@ -80,11 +73,12 @@ module BugBunny
|
|
|
80
73
|
begin
|
|
81
74
|
fire(request)
|
|
82
75
|
|
|
76
|
+
BugBunny.configuration.logger.debug("[BugBunny::Producer] ⏳ Waiting for RPC response | ID: #{cid} | Timeout: #{wait_timeout}s")
|
|
77
|
+
|
|
83
78
|
# Bloqueamos el hilo aquí hasta que llegue la respuesta o expire el timeout
|
|
84
79
|
response_payload = future.value(wait_timeout)
|
|
85
80
|
|
|
86
81
|
if response_payload.nil?
|
|
87
|
-
# CORRECCIÓN: Usamos request.path y request.method en lugar de request.action
|
|
88
82
|
raise BugBunny::RequestTimeout, "Timeout waiting for RPC: #{request.path} [#{request.method}]"
|
|
89
83
|
end
|
|
90
84
|
|
|
@@ -97,7 +91,21 @@ module BugBunny
|
|
|
97
91
|
|
|
98
92
|
private
|
|
99
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
|
+
|
|
100
107
|
# Serializa el mensaje para su transporte.
|
|
108
|
+
#
|
|
101
109
|
# @param msg [Hash, String, Object] El mensaje a serializar.
|
|
102
110
|
# @return [String] Cadena JSON o string crudo.
|
|
103
111
|
def serialize_message(msg)
|
|
@@ -105,6 +113,9 @@ module BugBunny
|
|
|
105
113
|
end
|
|
106
114
|
|
|
107
115
|
# Intenta parsear la respuesta recibida.
|
|
116
|
+
#
|
|
117
|
+
# @param payload [String] El cuerpo de la respuesta recibida.
|
|
118
|
+
# @return [Hash] El JSON parseado.
|
|
108
119
|
# @raise [BugBunny::InternalServerError] Si el payload no es JSON válido.
|
|
109
120
|
def parse_response(payload)
|
|
110
121
|
JSON.parse(payload)
|
|
@@ -115,27 +126,25 @@ module BugBunny
|
|
|
115
126
|
# Inicia el consumidor de respuestas RPC de forma perezosa (Lazy Initialization).
|
|
116
127
|
#
|
|
117
128
|
# Utiliza un patrón de "Double-Checked Locking" con Mutex para asegurar que
|
|
118
|
-
# solo se crea un listener por instancia de Producer
|
|
129
|
+
# solo se crea un listener por instancia de Producer.
|
|
119
130
|
#
|
|
120
|
-
#
|
|
121
|
-
# busca el `correlation_id` en el mapa de pendientes y completa el futuro (`IVar`),
|
|
122
|
-
# desbloqueando así al hilo que llamó a {#rpc}.
|
|
131
|
+
# @return [void]
|
|
123
132
|
def ensure_reply_listener!
|
|
124
133
|
return if @reply_listener_started
|
|
125
134
|
|
|
126
135
|
@reply_listener_mutex.synchronize do
|
|
127
136
|
return if @reply_listener_started
|
|
128
137
|
|
|
129
|
-
BugBunny.configuration.logger.debug("[Producer] 👂
|
|
138
|
+
BugBunny.configuration.logger.debug("[BugBunny::Producer] 👂 Starting Reply Listener on 'amq.rabbitmq.reply-to'")
|
|
130
139
|
|
|
131
140
|
# Consumimos sin ack (auto-ack) porque reply-to no soporta acks manuales de forma estándar
|
|
132
141
|
@session.channel.basic_consume('amq.rabbitmq.reply-to', '', true, false, nil) do |_, props, body|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (future = @pending_requests[
|
|
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])
|
|
136
145
|
future.set(body)
|
|
137
|
-
|
|
138
|
-
|
|
146
|
+
else
|
|
147
|
+
BugBunny.configuration.logger.warn("[BugBunny::Producer] ⚠️ Orphaned RPC Response received | ID: #{cid}")
|
|
139
148
|
end
|
|
140
149
|
end
|
|
141
150
|
@reply_listener_started = true
|
data/lib/bug_bunny/request.rb
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
#
|
|
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
|
|
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.
|