bug_bunny 3.1.0 → 3.1.2
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 +25 -0
- data/README.md +197 -345
- data/lib/bug_bunny/client.rb +26 -11
- data/lib/bug_bunny/configuration.rb +28 -2
- data/lib/bug_bunny/consumer.rb +27 -16
- data/lib/bug_bunny/controller.rb +142 -70
- data/lib/bug_bunny/producer.rb +51 -32
- data/lib/bug_bunny/request.rb +14 -2
- data/lib/bug_bunny/resource.rb +152 -18
- 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/session.rb
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module BugBunny
|
|
4
4
|
# Clase interna que encapsula una unidad de trabajo sobre una conexión RabbitMQ.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# 2. Resiliencia: Intenta recuperar la conexión TCP si está cerrada.
|
|
6
|
+
# Implementa la lógica de "Configuración en Cascada" para Exchanges y Colas,
|
|
7
|
+
# gestionando el ciclo de vida de un `Bunny::Channel` con resiliencia y carga perezosa.
|
|
9
8
|
#
|
|
10
9
|
# @api private
|
|
11
10
|
class Session
|
|
12
|
-
# Opciones por
|
|
11
|
+
# @!group Opciones por Defecto (Nivel 1: Gema)
|
|
12
|
+
|
|
13
|
+
# Opciones predeterminadas de la gema para Exchanges.
|
|
13
14
|
DEFAULT_EXCHANGE_OPTIONS = { durable: false, auto_delete: false }.freeze
|
|
15
|
+
|
|
16
|
+
# Opciones predeterminadas de la gema para Colas.
|
|
14
17
|
DEFAULT_QUEUE_OPTIONS = { exclusive: false, durable: false, auto_delete: true }.freeze
|
|
15
18
|
|
|
19
|
+
# @!endgroup
|
|
20
|
+
|
|
16
21
|
# @return [Bunny::Session] La conexión TCP subyacente.
|
|
17
22
|
attr_reader :connection
|
|
18
23
|
|
|
@@ -42,30 +47,50 @@ module BugBunny
|
|
|
42
47
|
@channel
|
|
43
48
|
end
|
|
44
49
|
|
|
45
|
-
# Factory method para declarar o recuperar un Exchange.
|
|
46
|
-
#
|
|
50
|
+
# Factory method para declarar o recuperar un Exchange aplicando la cascada de configuración.
|
|
51
|
+
#
|
|
52
|
+
# Jerarquía de fusión:
|
|
53
|
+
# 1. Defaults de la gema (`DEFAULT_EXCHANGE_OPTIONS`)
|
|
54
|
+
# 2. Configuración global (`BugBunny.configuration.exchange_options`)
|
|
55
|
+
# 3. Opciones específicas pasadas como argumento (`opts`)
|
|
47
56
|
#
|
|
48
57
|
# @param name [String, nil] Nombre del exchange.
|
|
49
|
-
# @param type [String, Symbol] Tipo de exchange.
|
|
50
|
-
# @param opts [Hash] Opciones
|
|
58
|
+
# @param type [String, Symbol] Tipo de exchange ('direct', 'topic', 'fanout').
|
|
59
|
+
# @param opts [Hash] Opciones específicas de infraestructura para este intercambio.
|
|
60
|
+
# @return [Bunny::Exchange] El objeto exchange de Bunny configurado.
|
|
51
61
|
def exchange(name: nil, type: 'direct', opts: {})
|
|
52
62
|
return channel.default_exchange if name.nil? || name.empty?
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
# Aplicación de la lógica de fusión en cascada
|
|
65
|
+
merged_opts = DEFAULT_EXCHANGE_OPTIONS
|
|
66
|
+
.merge(BugBunny.configuration.exchange_options || {})
|
|
67
|
+
.merge(opts)
|
|
68
|
+
|
|
69
|
+
# public_send permite llamar a :topic, :direct, etc. dinámicamente según el tipo
|
|
56
70
|
channel.public_send(type, name, merged_opts)
|
|
57
71
|
end
|
|
58
72
|
|
|
59
|
-
# Factory method para declarar o recuperar una Cola.
|
|
60
|
-
#
|
|
73
|
+
# Factory method para declarar o recuperar una Cola aplicando la cascada de configuración.
|
|
74
|
+
#
|
|
75
|
+
# Jerarquía de fusión:
|
|
76
|
+
# 1. Defaults de la gema (`DEFAULT_QUEUE_OPTIONS`)
|
|
77
|
+
# 2. Configuración global (`BugBunny.configuration.queue_options`)
|
|
78
|
+
# 3. Opciones específicas pasadas como argumento (`opts`)
|
|
61
79
|
#
|
|
62
80
|
# @param name [String] Nombre de la cola.
|
|
63
|
-
# @param opts [Hash] Opciones
|
|
81
|
+
# @param opts [Hash] Opciones específicas de infraestructura para esta cola.
|
|
82
|
+
# @return [Bunny::Queue] El objeto cola de Bunny configurado.
|
|
64
83
|
def queue(name, opts = {})
|
|
65
|
-
|
|
84
|
+
# Aplicación de la lógica de fusión en cascada
|
|
85
|
+
merged_opts = DEFAULT_QUEUE_OPTIONS
|
|
86
|
+
.merge(BugBunny.configuration.queue_options || {})
|
|
87
|
+
.merge(opts)
|
|
88
|
+
|
|
89
|
+
channel.queue(name.to_s, merged_opts)
|
|
66
90
|
end
|
|
67
91
|
|
|
68
92
|
# Cierra el canal asociado a esta sesión de forma segura.
|
|
93
|
+
# @return [void]
|
|
69
94
|
def close
|
|
70
95
|
@channel&.close if @channel&.open?
|
|
71
96
|
@channel = nil
|
|
@@ -73,8 +98,10 @@ module BugBunny
|
|
|
73
98
|
|
|
74
99
|
private
|
|
75
100
|
|
|
76
|
-
# Crea y configura un nuevo canal.
|
|
101
|
+
# Crea y configura un nuevo canal con las preferencias globales.
|
|
77
102
|
# Asume que la conexión ya ha sido verificada por `ensure_connection!`.
|
|
103
|
+
#
|
|
104
|
+
# @raise [BugBunny::CommunicationError] Si falla la creación del canal.
|
|
78
105
|
def create_channel!
|
|
79
106
|
@channel = @connection.create_channel
|
|
80
107
|
|
|
@@ -90,13 +117,15 @@ module BugBunny
|
|
|
90
117
|
|
|
91
118
|
# Garantiza que la conexión TCP esté abierta.
|
|
92
119
|
# Si está cerrada, intenta reconectarla (Reconexión Transparente).
|
|
120
|
+
#
|
|
121
|
+
# @raise [BugBunny::CommunicationError] Si falla la reconexión.
|
|
93
122
|
def ensure_connection!
|
|
94
123
|
return if @connection.open?
|
|
95
124
|
|
|
96
|
-
BugBunny.configuration.logger.warn("[BugBunny] Connection lost. Attempting to reconnect...")
|
|
125
|
+
BugBunny.configuration.logger.warn("[BugBunny::Session] ⚠️ Connection lost. Attempting to reconnect...")
|
|
97
126
|
@connection.start
|
|
98
127
|
rescue StandardError => e
|
|
99
|
-
BugBunny.configuration.logger.error("[BugBunny] Critical connection failure: #{e.message}")
|
|
128
|
+
BugBunny.configuration.logger.error("[BugBunny::Session] ❌ Critical connection failure: #{e.message}")
|
|
100
129
|
raise BugBunny::CommunicationError, "Could not reconnect to RabbitMQ: #{e.message}"
|
|
101
130
|
end
|
|
102
131
|
end
|
data/lib/bug_bunny/version.rb
CHANGED
data/lib/bug_bunny.rb
CHANGED
|
@@ -75,7 +75,7 @@ module BugBunny
|
|
|
75
75
|
|
|
76
76
|
@global_connection.close if @global_connection.open?
|
|
77
77
|
@global_connection = nil
|
|
78
|
-
configuration.logger.info('[BugBunny] Global connection closed.')
|
|
78
|
+
configuration.logger.info('[BugBunny] 🔌 Global connection closed.')
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
# @api private
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../test_helper'
|
|
4
|
+
|
|
5
|
+
# --- CLASES DE PRUEBA (Namespace Aislado) ---
|
|
6
|
+
module InfraTest
|
|
7
|
+
class PingController < BugBunny::Controller
|
|
8
|
+
# Agregamos SHOW para soportar el .find del test
|
|
9
|
+
def show
|
|
10
|
+
render status: 200, json: { id: params[:id], message: 'pong', namespace: 'InfraTest' }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def index
|
|
14
|
+
render status: 200, json: { message: 'pong_index', namespace: 'InfraTest' }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class InfraResource < BugBunny::Resource
|
|
20
|
+
self.resource_name = 'ping'
|
|
21
|
+
self.exchange = 'test_infra_exchange'
|
|
22
|
+
self.exchange_type = 'topic'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# --- SUITE DE INFRAESTRUCTURA ---
|
|
26
|
+
class InfrastructureTest < Minitest::Test
|
|
27
|
+
include IntegrationHelper
|
|
28
|
+
|
|
29
|
+
def setup
|
|
30
|
+
skip "RabbitMQ no disponible" unless IntegrationHelper.rabbitmq_available?
|
|
31
|
+
@queue = "test_infra_queue_#{SecureRandom.hex(4)}"
|
|
32
|
+
@exchange = "test_infra_exchange"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_00_worker_lifecycle
|
|
36
|
+
with_running_worker(queue: @queue, exchange: @exchange) do
|
|
37
|
+
assert true, "El worker levantó y cedió el control al bloque"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_01_dynamic_namespace_resolution
|
|
42
|
+
BugBunny.configure do |c|
|
|
43
|
+
c.controller_namespace = 'InfraTest'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
with_running_worker(queue: @queue, exchange: @exchange) do
|
|
47
|
+
# Enviamos GET ping/123 -> InfraTest::PingController#show
|
|
48
|
+
resource = InfraResource.find('123')
|
|
49
|
+
|
|
50
|
+
# Verificamos que volvió el objeto construido
|
|
51
|
+
assert_equal '123', resource.id
|
|
52
|
+
assert_equal 'InfraTest', resource.namespace
|
|
53
|
+
assert_equal 'pong', resource.message
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
ensure
|
|
57
|
+
BugBunny.configure do |c|
|
|
58
|
+
c.controller_namespace = 'Rabbit::Controllers'
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../test_helper'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
module ManualTest
|
|
7
|
+
class EchoController < BugBunny::Controller
|
|
8
|
+
def index
|
|
9
|
+
render status: 200, json: {
|
|
10
|
+
received: params[:message],
|
|
11
|
+
type: headers[:type],
|
|
12
|
+
via: 'ManualTest::EchoController'
|
|
13
|
+
}
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class ManualClientTest < Minitest::Test
|
|
19
|
+
include IntegrationHelper
|
|
20
|
+
|
|
21
|
+
def setup
|
|
22
|
+
skip "RabbitMQ no disponible" unless IntegrationHelper.rabbitmq_available?
|
|
23
|
+
|
|
24
|
+
# 1. NOMBRES ÚNICOS: Evitamos colisiones entre tests de diferentes tipos (Direct/Fanout)
|
|
25
|
+
@queue = "test_manual_q_#{SecureRandom.hex(4)}"
|
|
26
|
+
@exchange = "test_manual_x_#{SecureRandom.hex(4)}"
|
|
27
|
+
|
|
28
|
+
@client = BugBunny::Client.new(pool: TEST_POOL)
|
|
29
|
+
BugBunny.configure { |c| c.controller_namespace = 'ManualTest' }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def teardown
|
|
33
|
+
BugBunny.configure { |c| c.controller_namespace = 'Rabbit::Controllers' }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# ==========================================
|
|
37
|
+
# GRUPO 1: PUBLICACIÓN ASÍNCRONA (PUBLISH)
|
|
38
|
+
# ==========================================
|
|
39
|
+
|
|
40
|
+
def test_publish_topic
|
|
41
|
+
puts "\n -> [Manual] Publish (Topic)..."
|
|
42
|
+
with_spy_worker(queue: @queue, exchange: @exchange, exchange_type: 'topic', routing_key: 'logs.#') do |messages|
|
|
43
|
+
|
|
44
|
+
@client.publish('logs.error', exchange: @exchange, exchange_type: 'topic', body: { a: 1 })
|
|
45
|
+
|
|
46
|
+
msg = wait_for_message(messages)
|
|
47
|
+
assert_equal 'logs.error', msg[:routing_key]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_publish_direct
|
|
52
|
+
puts " -> [Manual] Publish (Direct)..."
|
|
53
|
+
with_spy_worker(queue: @queue, exchange: @exchange, exchange_type: 'direct', routing_key: 'alert') do |messages|
|
|
54
|
+
|
|
55
|
+
@client.publish('alert', exchange: @exchange, exchange_type: 'direct', body: { a: 1 })
|
|
56
|
+
|
|
57
|
+
msg = wait_for_message(messages)
|
|
58
|
+
assert_equal 'alert', msg[:routing_key]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_publish_fanout
|
|
63
|
+
puts " -> [Manual] Publish (Fanout)..."
|
|
64
|
+
with_spy_worker(queue: @queue, exchange: @exchange, exchange_type: 'fanout', routing_key: '') do |messages|
|
|
65
|
+
|
|
66
|
+
@client.publish('ignored.key', exchange: @exchange, exchange_type: 'fanout', body: { a: 1 })
|
|
67
|
+
|
|
68
|
+
msg = wait_for_message(messages)
|
|
69
|
+
assert_equal 'ignored.key', msg[:routing_key]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# ==========================================
|
|
74
|
+
# GRUPO 2: RPC SÍNCRONO (REQUEST)
|
|
75
|
+
# ==========================================
|
|
76
|
+
|
|
77
|
+
def test_request_topic
|
|
78
|
+
puts " -> [Manual] RPC (Topic)..."
|
|
79
|
+
with_running_worker(queue: @queue, exchange: @exchange, exchange_type: 'topic', routing_key: 'echo') do
|
|
80
|
+
|
|
81
|
+
response = @client.request('echo',
|
|
82
|
+
method: :get, exchange: @exchange, exchange_type: 'topic',
|
|
83
|
+
body: { message: 'topic_rpc' }
|
|
84
|
+
)
|
|
85
|
+
assert_equal 'topic_rpc', response['body']['received']
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_request_direct
|
|
90
|
+
puts " -> [Manual] RPC (Direct)..."
|
|
91
|
+
direct_key = 'rpc.direct'
|
|
92
|
+
|
|
93
|
+
with_running_worker(queue: @queue, exchange: @exchange, exchange_type: 'direct', routing_key: direct_key) do
|
|
94
|
+
|
|
95
|
+
response = @client.request('echo',
|
|
96
|
+
method: :get,
|
|
97
|
+
routing_key: direct_key,
|
|
98
|
+
exchange: @exchange,
|
|
99
|
+
exchange_type: 'direct',
|
|
100
|
+
body: { message: 'direct_rpc' }
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
assert_equal 200, response['status']
|
|
104
|
+
assert_equal 'direct_rpc', response['body']['received']
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def test_request_fanout
|
|
109
|
+
puts " -> [Manual] RPC (Fanout)..."
|
|
110
|
+
with_running_worker(queue: @queue, exchange: @exchange, exchange_type: 'fanout', routing_key: '') do
|
|
111
|
+
|
|
112
|
+
response = @client.request('echo',
|
|
113
|
+
method: :get,
|
|
114
|
+
routing_key: 'random.ignored',
|
|
115
|
+
exchange: @exchange,
|
|
116
|
+
exchange_type: 'fanout',
|
|
117
|
+
body: { message: 'fanout_rpc' }
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
assert_equal 200, response['status']
|
|
121
|
+
assert_equal 'fanout_rpc', response['body']['received']
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# ==========================================
|
|
126
|
+
# GRUPO 3: OPCIONES DE INFRAESTRUCTURA (CASCADA NIVEL 3)
|
|
127
|
+
# ==========================================
|
|
128
|
+
|
|
129
|
+
def test_publish_with_custom_exchange_options
|
|
130
|
+
puts " -> [Manual] Publish (Custom Options Nivel 3)..."
|
|
131
|
+
custom_exchange = "custom_opts_x_#{SecureRandom.hex(4)}"
|
|
132
|
+
|
|
133
|
+
# 1. Pre-creamos el exchange exigiendo que sea DURABLE (contrario a la config global)
|
|
134
|
+
conn = BugBunny.create_connection
|
|
135
|
+
ch = conn.create_channel
|
|
136
|
+
ch.topic(custom_exchange, durable: true, auto_delete: true)
|
|
137
|
+
|
|
138
|
+
begin
|
|
139
|
+
# 2. El cliente publica inyectando opciones dinámicas.
|
|
140
|
+
# Si esto no funcionara, RabbitMQ nos tiraría PRECONDITION_FAILED
|
|
141
|
+
# porque la configuración global (Nivel 2) dice durable: false.
|
|
142
|
+
@client.publish('logs',
|
|
143
|
+
exchange: custom_exchange,
|
|
144
|
+
exchange_type: 'topic',
|
|
145
|
+
exchange_options: { durable: true, auto_delete: true }, # Nivel 3 sobrescribe Nivel 2
|
|
146
|
+
body: { test: 'options' }
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Si la ejecución llega aquí, significa que la Cascada funcionó perfecto.
|
|
150
|
+
assert true
|
|
151
|
+
ensure
|
|
152
|
+
ch&.exchange_delete(custom_exchange) rescue nil
|
|
153
|
+
conn&.close
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def test_request_with_custom_exchange_options
|
|
158
|
+
puts " -> [Manual] RPC (Custom Options Nivel 3)..."
|
|
159
|
+
custom_exchange = "custom_opts_rpc_x_#{SecureRandom.hex(4)}"
|
|
160
|
+
|
|
161
|
+
# 1. Exigimos DURABLE
|
|
162
|
+
conn = BugBunny.create_connection
|
|
163
|
+
ch = conn.create_channel
|
|
164
|
+
ch.direct(custom_exchange, durable: true, auto_delete: true)
|
|
165
|
+
|
|
166
|
+
# 2. Levantamos un worker usando esas configuraciones nativas
|
|
167
|
+
worker_thread = Thread.new do
|
|
168
|
+
q = ch.queue('', exclusive: true)
|
|
169
|
+
q.bind(custom_exchange, routing_key: 'custom.rpc')
|
|
170
|
+
q.subscribe(block: true) do |delivery, props, _body|
|
|
171
|
+
# Respuesta manual
|
|
172
|
+
ch.default_exchange.publish('{"status":200, "body":"ok"}', routing_key: props.reply_to, correlation_id: props.correlation_id)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
sleep 0.5
|
|
176
|
+
|
|
177
|
+
begin
|
|
178
|
+
# 3. El cliente hace el request inyectando las opciones
|
|
179
|
+
response = @client.request('test',
|
|
180
|
+
exchange: custom_exchange,
|
|
181
|
+
exchange_type: 'direct',
|
|
182
|
+
routing_key: 'custom.rpc',
|
|
183
|
+
exchange_options: { durable: true, auto_delete: true }, # Nivel 3
|
|
184
|
+
body: { req: 'data' }
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
assert_equal 200, response['status']
|
|
188
|
+
assert_equal 'ok', response['body']
|
|
189
|
+
ensure
|
|
190
|
+
worker_thread&.kill
|
|
191
|
+
ch&.exchange_delete(custom_exchange) rescue nil
|
|
192
|
+
conn&.close
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
private
|
|
197
|
+
|
|
198
|
+
def wait_for_message(queue, timeout_sec = 2)
|
|
199
|
+
Timeout.timeout(timeout_sec) { queue.pop }
|
|
200
|
+
rescue Timeout::Error
|
|
201
|
+
flunk "Timeout: No llegó el mensaje al Worker en #{timeout_sec}s"
|
|
202
|
+
end
|
|
203
|
+
end
|
data/test/test_helper.rb
CHANGED
|
@@ -1,24 +1,109 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
require 'bundler/setup'
|
|
3
|
-
|
|
4
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
5
|
-
require 'bug_bunny'
|
|
6
2
|
|
|
3
|
+
require 'bundler/setup'
|
|
7
4
|
require 'minitest/autorun'
|
|
8
|
-
require '
|
|
9
|
-
require '
|
|
5
|
+
# require 'minitest/reporters'
|
|
6
|
+
require 'bug_bunny'
|
|
7
|
+
require 'connection_pool'
|
|
8
|
+
require 'securerandom'
|
|
9
|
+
require 'socket'
|
|
10
10
|
|
|
11
11
|
BugBunny.configure do |config|
|
|
12
|
-
config.
|
|
13
|
-
config.
|
|
12
|
+
config.host = ENV.fetch('RABBITMQ_HOST', 'localhost')
|
|
13
|
+
config.username = ENV.fetch('RABBITMQ_USER', 'guest')
|
|
14
|
+
config.password = ENV.fetch('RABBITMQ_PASS', 'guest')
|
|
15
|
+
config.vhost = '/'
|
|
16
|
+
config.logger = Logger.new($stdout)
|
|
17
|
+
config.logger.level = Logger::WARN
|
|
18
|
+
|
|
19
|
+
# ========================================================
|
|
20
|
+
# LA MAGIA DE LA CASCADA (Nivel 2: Configuración Global)
|
|
21
|
+
# ========================================================
|
|
22
|
+
# Para los tests, queremos que los exchanges y queues sean efímeros
|
|
23
|
+
config.exchange_options = { durable: false, auto_delete: true }
|
|
24
|
+
config.queue_options = { exclusive: false, durable: false, auto_delete: true }
|
|
14
25
|
end
|
|
15
26
|
|
|
16
|
-
|
|
27
|
+
TEST_POOL = ConnectionPool.new(size: 5, timeout: 5) { BugBunny.create_connection }
|
|
28
|
+
BugBunny::Resource.connection_pool = TEST_POOL
|
|
29
|
+
|
|
30
|
+
module IntegrationHelper
|
|
17
31
|
def self.rabbitmq_available?
|
|
18
|
-
socket = TCPSocket.new(
|
|
32
|
+
socket = TCPSocket.new(BugBunny.configuration.host, 5672)
|
|
19
33
|
socket.close
|
|
20
34
|
true
|
|
21
|
-
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT
|
|
35
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError
|
|
22
36
|
false
|
|
23
37
|
end
|
|
38
|
+
|
|
39
|
+
def with_running_worker(queue:, exchange:, exchange_type: 'topic', routing_key: '#')
|
|
40
|
+
conn = BugBunny.create_connection
|
|
41
|
+
|
|
42
|
+
worker_thread = Thread.new do
|
|
43
|
+
ch = conn.create_channel
|
|
44
|
+
|
|
45
|
+
x_opts = BugBunny.configuration.exchange_options || {}
|
|
46
|
+
q_opts = BugBunny.configuration.queue_options || {}
|
|
47
|
+
|
|
48
|
+
# FIX: Usamos la API de alto nivel (public_send) idéntica a BugBunny::Session
|
|
49
|
+
ch.public_send(exchange_type, exchange, x_opts)
|
|
50
|
+
ch.close
|
|
51
|
+
|
|
52
|
+
BugBunny::Consumer.subscribe(
|
|
53
|
+
connection: conn,
|
|
54
|
+
queue_name: queue,
|
|
55
|
+
exchange_name: exchange,
|
|
56
|
+
exchange_type: exchange_type,
|
|
57
|
+
routing_key: routing_key,
|
|
58
|
+
queue_opts: q_opts,
|
|
59
|
+
block: true
|
|
60
|
+
)
|
|
61
|
+
rescue => e
|
|
62
|
+
puts "❌ WORKER CRASHED: #{e.message}"
|
|
63
|
+
puts e.backtrace.join("\n")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
sleep 0.5
|
|
67
|
+
yield
|
|
68
|
+
ensure
|
|
69
|
+
conn&.close
|
|
70
|
+
worker_thread&.kill
|
|
71
|
+
sleep 0.1
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def with_spy_worker(queue:, exchange:, exchange_type: 'topic', routing_key: '#')
|
|
75
|
+
captured_messages = Thread::Queue.new
|
|
76
|
+
conn = BugBunny.create_connection
|
|
77
|
+
|
|
78
|
+
worker_thread = Thread.new do
|
|
79
|
+
ch = conn.create_channel
|
|
80
|
+
|
|
81
|
+
x_opts = BugBunny.configuration.exchange_options || {}
|
|
82
|
+
q_opts = BugBunny.configuration.queue_options || {}
|
|
83
|
+
|
|
84
|
+
# FIX DEFINITIVO: Ahora 'x' es un hermoso objeto Bunny::Exchange
|
|
85
|
+
# que la función .bind() entiende perfectamente.
|
|
86
|
+
x = ch.public_send(exchange_type, exchange, x_opts)
|
|
87
|
+
q = ch.queue(queue, q_opts)
|
|
88
|
+
|
|
89
|
+
# Bindeamos el objeto al queue
|
|
90
|
+
q.bind(x, routing_key: routing_key)
|
|
91
|
+
|
|
92
|
+
q.subscribe(block: true) do |delivery, props, body|
|
|
93
|
+
captured_messages << {
|
|
94
|
+
body: body,
|
|
95
|
+
routing_key: delivery.routing_key,
|
|
96
|
+
headers: props.headers
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
rescue => e
|
|
100
|
+
puts "SPY WORKER ERROR: #{e.message}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
sleep 0.5
|
|
104
|
+
yield(captured_messages)
|
|
105
|
+
ensure
|
|
106
|
+
conn&.close
|
|
107
|
+
worker_thread&.kill
|
|
108
|
+
end
|
|
24
109
|
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.
|
|
4
|
+
version: 3.1.2
|
|
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-
|
|
11
|
+
date: 2026-02-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|
|
@@ -206,6 +206,20 @@ dependencies:
|
|
|
206
206
|
- - "~>"
|
|
207
207
|
- !ruby/object:Gem::Version
|
|
208
208
|
version: '2.0'
|
|
209
|
+
- !ruby/object:Gem::Dependency
|
|
210
|
+
name: minitest-reporters
|
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
|
212
|
+
requirements:
|
|
213
|
+
- - "~>"
|
|
214
|
+
- !ruby/object:Gem::Version
|
|
215
|
+
version: '1.6'
|
|
216
|
+
type: :development
|
|
217
|
+
prerelease: false
|
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
219
|
+
requirements:
|
|
220
|
+
- - "~>"
|
|
221
|
+
- !ruby/object:Gem::Version
|
|
222
|
+
version: '1.6'
|
|
209
223
|
description: BugBunny is a lightweight RPC framework for Ruby on Rails over RabbitMQ.
|
|
210
224
|
It simulates a RESTful architecture with an intelligent router, Active Record-like
|
|
211
225
|
resources, and middleware support.
|
|
@@ -218,9 +232,6 @@ files:
|
|
|
218
232
|
- CHANGELOG.md
|
|
219
233
|
- README.md
|
|
220
234
|
- Rakefile
|
|
221
|
-
- bin_client.rb
|
|
222
|
-
- bin_suite.rb
|
|
223
|
-
- bin_worker.rb
|
|
224
235
|
- initializer_example.rb
|
|
225
236
|
- lib/bug_bunny.rb
|
|
226
237
|
- lib/bug_bunny/client.rb
|
|
@@ -241,18 +252,9 @@ files:
|
|
|
241
252
|
- lib/generators/bug_bunny/install/install_generator.rb
|
|
242
253
|
- lib/generators/bug_bunny/install/templates/initializer.rb
|
|
243
254
|
- sig/bug_bunny.rbs
|
|
244
|
-
- test/integration/
|
|
245
|
-
- test/integration/
|
|
255
|
+
- test/integration/infrastructure_test.rb
|
|
256
|
+
- test/integration/manual_client_test.rb
|
|
246
257
|
- test/test_helper.rb
|
|
247
|
-
- test/unit/configuration_test.rb
|
|
248
|
-
- test/unit/consumer_test.rb
|
|
249
|
-
- test/unit/controller_headers_test.rb
|
|
250
|
-
- test/unit/hybrid_resource_test.rb
|
|
251
|
-
- test/unit/middleware_test.rb
|
|
252
|
-
- test/unit/resource_test.rb
|
|
253
|
-
- test_controller.rb
|
|
254
|
-
- test_helper.rb
|
|
255
|
-
- test_resource.rb
|
|
256
258
|
homepage: https://github.com/gedera/bug_bunny
|
|
257
259
|
licenses:
|
|
258
260
|
- MIT
|
data/bin_client.rb
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# bin_client.rb
|
|
2
|
-
require_relative 'lib/bug_bunny'
|
|
3
|
-
$stdout.sync = true # <--- Agrega esto
|
|
4
|
-
|
|
5
|
-
# 1. Configuración
|
|
6
|
-
BugBunny.configure do |config|
|
|
7
|
-
config.host = 'localhost'
|
|
8
|
-
config.username = 'wisproMQ'
|
|
9
|
-
config.password = 'wisproMQ'
|
|
10
|
-
config.vhost = 'sync.devel'
|
|
11
|
-
config.logger = Logger.new(STDOUT)
|
|
12
|
-
config.rpc_timeout = 5
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# 2. Pool
|
|
16
|
-
POOL = ConnectionPool.new(size: 2, timeout: 5) do
|
|
17
|
-
BugBunny.create_connection
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# 3. Cliente
|
|
21
|
-
client = BugBunny.new(pool: POOL)
|
|
22
|
-
|
|
23
|
-
# --- PRUEBA 1: Publish ---
|
|
24
|
-
puts "\n[1] Enviando mensaje asíncrono (Publish)..."
|
|
25
|
-
|
|
26
|
-
# AGREGADO: exchange_type: 'topic'
|
|
27
|
-
client.publish('test/ping', exchange: 'test_exchange', exchange_type: 'topic', routing_key: 'test.ping') do |req|
|
|
28
|
-
req.body = { msg: 'Hola, soy invisible' }
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
puts " -> Enviado."
|
|
32
|
-
sleep 1
|
|
33
|
-
|
|
34
|
-
# --- PRUEBA 2: RPC ---
|
|
35
|
-
puts "\n[2] Enviando petición síncrona (Request)..."
|
|
36
|
-
|
|
37
|
-
begin
|
|
38
|
-
# AGREGADO: exchange_type: 'topic'
|
|
39
|
-
response = client.request('test/123/ping', exchange: 'test_exchange', exchange_type: 'topic', routing_key: 'test.ping') do |req|
|
|
40
|
-
req.body = { data: 'Importante' }
|
|
41
|
-
req.timeout = 3
|
|
42
|
-
req.headers['X-Source'] = 'Terminal'
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
puts " -> ✅ RESPUESTA RECIBIDA:"
|
|
46
|
-
puts " Status: #{response['status']}"
|
|
47
|
-
puts " Body: #{response['body']}"
|
|
48
|
-
|
|
49
|
-
rescue BugBunny::RequestTimeout
|
|
50
|
-
puts " -> ❌ Error: Timeout esperando respuesta."
|
|
51
|
-
end
|