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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +204 -148
- data/Rakefile +10 -6
- data/lib/bug_bunny/client.rb +26 -11
- data/lib/bug_bunny/configuration.rb +40 -3
- data/lib/bug_bunny/consumer.rb +56 -29
- data/lib/bug_bunny/controller.rb +137 -93
- data/lib/bug_bunny/exception.rb +4 -0
- data/lib/bug_bunny/producer.rb +45 -29
- data/lib/bug_bunny/request.rb +14 -2
- data/lib/bug_bunny/resource.rb +176 -138
- data/lib/bug_bunny/session.rb +97 -47
- 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 +109 -0
- metadata +47 -8
- data/bin_client.rb +0 -51
- data/bin_suite.rb +0 -106
- data/bin_worker.rb +0 -26
- data/test_controller.rb +0 -49
- data/test_helper.rb +0 -20
- data/test_resource.rb +0 -19
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.
|
|
@@ -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[
|
|
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(
|
|
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
|
|
129
|
+
# solo se crea un listener por instancia de Producer.
|
|
118
130
|
#
|
|
119
|
-
#
|
|
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
|
-
|
|
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
|
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.
|