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/session.rb
CHANGED
|
@@ -1,82 +1,132 @@
|
|
|
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
|
-
# Esta clase toma una conexión abierta del Pool, abre un canal exclusivo para esta sesión,
|
|
9
|
-
# configura el QoS y facilita la creación de Exchanges y Colas.
|
|
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.
|
|
10
8
|
#
|
|
11
9
|
# @api private
|
|
12
10
|
class Session
|
|
13
|
-
# Opciones por
|
|
11
|
+
# @!group Opciones por Defecto (Nivel 1: Gema)
|
|
12
|
+
|
|
13
|
+
# Opciones predeterminadas de la gema para Exchanges.
|
|
14
14
|
DEFAULT_EXCHANGE_OPTIONS = { durable: false, auto_delete: false }.freeze
|
|
15
15
|
|
|
16
|
-
# Opciones
|
|
17
|
-
# @note Por defecto las colas son volátiles (`auto_delete: true`). Para workers persistentes,
|
|
18
|
-
# se debe pasar explícitamente `durable: true, auto_delete: false`.
|
|
16
|
+
# Opciones predeterminadas de la gema para Colas.
|
|
19
17
|
DEFAULT_QUEUE_OPTIONS = { exclusive: false, durable: false, auto_delete: true }.freeze
|
|
20
18
|
|
|
19
|
+
# @!endgroup
|
|
20
|
+
|
|
21
21
|
# @return [Bunny::Session] La conexión TCP subyacente.
|
|
22
22
|
attr_reader :connection
|
|
23
23
|
|
|
24
|
-
#
|
|
25
|
-
|
|
24
|
+
# Inicializa una nueva sesión sin abrir canales todavía.
|
|
25
|
+
#
|
|
26
|
+
# @param connection [Bunny::Session] Una conexión (puede estar abierta o cerrada temporalmente).
|
|
27
|
+
def initialize(connection)
|
|
28
|
+
@connection = connection
|
|
29
|
+
@channel = nil
|
|
30
|
+
end
|
|
26
31
|
|
|
27
|
-
#
|
|
32
|
+
# Obtiene el canal actual o crea uno nuevo si es necesario.
|
|
28
33
|
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
# 3. Habilita "Publisher Confirms" para garantizar que los mensajes lleguen al broker.
|
|
32
|
-
# 4. Configura el "Prefetch" (QoS) global para este canal.
|
|
34
|
+
# Este método es el punto central de la robustez. Verifica la salud
|
|
35
|
+
# de la conexión y del canal antes de devolverlo.
|
|
33
36
|
#
|
|
34
|
-
# @
|
|
35
|
-
# @raise [BugBunny::
|
|
36
|
-
def
|
|
37
|
-
|
|
37
|
+
# @return [Bunny::Channel] Un canal abierto y configurado.
|
|
38
|
+
# @raise [BugBunny::CommunicationError] Si no se puede restablecer la conexión.
|
|
39
|
+
def channel
|
|
40
|
+
# Si el canal existe y está abierto, lo devolvemos rápido.
|
|
41
|
+
return @channel if @channel&.open?
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@channel
|
|
43
|
+
# Si no, intentamos asegurar la conexión y crear el canal.
|
|
44
|
+
ensure_connection!
|
|
45
|
+
create_channel!
|
|
46
|
+
|
|
47
|
+
@channel
|
|
44
48
|
end
|
|
45
49
|
|
|
46
|
-
# Factory method para declarar o recuperar un Exchange.
|
|
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
|
-
# @param name [String, nil]
|
|
49
|
-
# @param type [String, Symbol]
|
|
50
|
-
# @param opts [Hash] Opciones de
|
|
51
|
-
# @return [Bunny::Exchange]
|
|
57
|
+
# @param name [String, nil] Nombre del exchange.
|
|
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.
|
|
52
61
|
def exchange(name: nil, type: 'direct', opts: {})
|
|
53
62
|
return channel.default_exchange if name.nil? || name.empty?
|
|
54
63
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
end
|
|
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
|
|
70
|
+
channel.public_send(type, name, merged_opts)
|
|
63
71
|
end
|
|
64
72
|
|
|
65
|
-
# Factory method para declarar o recuperar una Cola.
|
|
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`)
|
|
66
79
|
#
|
|
67
|
-
# @param name [String]
|
|
68
|
-
# @param opts [Hash] Opciones de
|
|
69
|
-
# @return [Bunny::Queue]
|
|
80
|
+
# @param name [String] Nombre de la cola.
|
|
81
|
+
# @param opts [Hash] Opciones específicas de infraestructura para esta cola.
|
|
82
|
+
# @return [Bunny::Queue] El objeto cola de Bunny configurado.
|
|
70
83
|
def queue(name, opts = {})
|
|
71
|
-
|
|
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)
|
|
72
90
|
end
|
|
73
91
|
|
|
74
|
-
# Cierra el canal asociado a esta sesión.
|
|
75
|
-
# No cierra la conexión TCP (ya que esta pertenece al Pool), solo libera el canal virtual.
|
|
76
|
-
#
|
|
92
|
+
# Cierra el canal asociado a esta sesión de forma segura.
|
|
77
93
|
# @return [void]
|
|
78
94
|
def close
|
|
79
|
-
@channel
|
|
95
|
+
@channel&.close if @channel&.open?
|
|
96
|
+
@channel = nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
# Crea y configura un nuevo canal con las preferencias globales.
|
|
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.
|
|
105
|
+
def create_channel!
|
|
106
|
+
@channel = @connection.create_channel
|
|
107
|
+
|
|
108
|
+
# Configuraciones globales de BugBunny
|
|
109
|
+
@channel.confirm_select
|
|
110
|
+
|
|
111
|
+
if BugBunny.configuration.channel_prefetch
|
|
112
|
+
@channel.prefetch(BugBunny.configuration.channel_prefetch)
|
|
113
|
+
end
|
|
114
|
+
rescue StandardError => e
|
|
115
|
+
raise BugBunny::CommunicationError, "Failed to create channel: #{e.message}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Garantiza que la conexión TCP esté abierta.
|
|
119
|
+
# Si está cerrada, intenta reconectarla (Reconexión Transparente).
|
|
120
|
+
#
|
|
121
|
+
# @raise [BugBunny::CommunicationError] Si falla la reconexión.
|
|
122
|
+
def ensure_connection!
|
|
123
|
+
return if @connection.open?
|
|
124
|
+
|
|
125
|
+
BugBunny.configuration.logger.warn("[BugBunny::Session] ⚠️ Connection lost. Attempting to reconnect...")
|
|
126
|
+
@connection.start
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
BugBunny.configuration.logger.error("[BugBunny::Session] ❌ Critical connection failure: #{e.message}")
|
|
129
|
+
raise BugBunny::CommunicationError, "Could not reconnect to RabbitMQ: #{e.message}"
|
|
80
130
|
end
|
|
81
131
|
end
|
|
82
132
|
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
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
require 'minitest/autorun'
|
|
5
|
+
# require 'minitest/reporters'
|
|
6
|
+
require 'bug_bunny'
|
|
7
|
+
require 'connection_pool'
|
|
8
|
+
require 'securerandom'
|
|
9
|
+
require 'socket'
|
|
10
|
+
|
|
11
|
+
BugBunny.configure do |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 }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
TEST_POOL = ConnectionPool.new(size: 5, timeout: 5) { BugBunny.create_connection }
|
|
28
|
+
BugBunny::Resource.connection_pool = TEST_POOL
|
|
29
|
+
|
|
30
|
+
module IntegrationHelper
|
|
31
|
+
def self.rabbitmq_available?
|
|
32
|
+
socket = TCPSocket.new(BugBunny.configuration.host, 5672)
|
|
33
|
+
socket.close
|
|
34
|
+
true
|
|
35
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError
|
|
36
|
+
false
|
|
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
|
|
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.
|
|
4
|
+
version: 3.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-02-
|
|
11
|
+
date: 2026-02-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|
|
@@ -178,6 +178,48 @@ dependencies:
|
|
|
178
178
|
- - "~>"
|
|
179
179
|
- !ruby/object:Gem::Version
|
|
180
180
|
version: '0.9'
|
|
181
|
+
- !ruby/object:Gem::Dependency
|
|
182
|
+
name: minitest
|
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
|
184
|
+
requirements:
|
|
185
|
+
- - "~>"
|
|
186
|
+
- !ruby/object:Gem::Version
|
|
187
|
+
version: '5.0'
|
|
188
|
+
type: :development
|
|
189
|
+
prerelease: false
|
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
191
|
+
requirements:
|
|
192
|
+
- - "~>"
|
|
193
|
+
- !ruby/object:Gem::Version
|
|
194
|
+
version: '5.0'
|
|
195
|
+
- !ruby/object:Gem::Dependency
|
|
196
|
+
name: mocha
|
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
|
198
|
+
requirements:
|
|
199
|
+
- - "~>"
|
|
200
|
+
- !ruby/object:Gem::Version
|
|
201
|
+
version: '2.0'
|
|
202
|
+
type: :development
|
|
203
|
+
prerelease: false
|
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
205
|
+
requirements:
|
|
206
|
+
- - "~>"
|
|
207
|
+
- !ruby/object:Gem::Version
|
|
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'
|
|
181
223
|
description: BugBunny is a lightweight RPC framework for Ruby on Rails over RabbitMQ.
|
|
182
224
|
It simulates a RESTful architecture with an intelligent router, Active Record-like
|
|
183
225
|
resources, and middleware support.
|
|
@@ -190,9 +232,6 @@ files:
|
|
|
190
232
|
- CHANGELOG.md
|
|
191
233
|
- README.md
|
|
192
234
|
- Rakefile
|
|
193
|
-
- bin_client.rb
|
|
194
|
-
- bin_suite.rb
|
|
195
|
-
- bin_worker.rb
|
|
196
235
|
- initializer_example.rb
|
|
197
236
|
- lib/bug_bunny.rb
|
|
198
237
|
- lib/bug_bunny/client.rb
|
|
@@ -213,9 +252,9 @@ files:
|
|
|
213
252
|
- lib/generators/bug_bunny/install/install_generator.rb
|
|
214
253
|
- lib/generators/bug_bunny/install/templates/initializer.rb
|
|
215
254
|
- sig/bug_bunny.rbs
|
|
216
|
-
-
|
|
217
|
-
-
|
|
218
|
-
-
|
|
255
|
+
- test/integration/infrastructure_test.rb
|
|
256
|
+
- test/integration/manual_client_test.rb
|
|
257
|
+
- test/test_helper.rb
|
|
219
258
|
homepage: https://github.com/gedera/bug_bunny
|
|
220
259
|
licenses:
|
|
221
260
|
- MIT
|