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/resource.rb
CHANGED
|
@@ -9,15 +9,13 @@ require 'rack/utils'
|
|
|
9
9
|
module BugBunny
|
|
10
10
|
# Clase base para modelos remotos que implementan **Active Record over AMQP (RESTful)**.
|
|
11
11
|
#
|
|
12
|
-
# Soporta un esquema híbrido de datos:
|
|
13
|
-
# 1. **
|
|
14
|
-
# 2. **
|
|
15
|
-
#
|
|
16
|
-
# Implementa un sistema de "Dirty Tracking" híbrido para detectar cambios
|
|
17
|
-
# tanto en atributos tipados (via ActiveModel) como dinámicos (via Set manual).
|
|
12
|
+
# Soporta un esquema híbrido de datos y configuración de infraestructura en cascada:
|
|
13
|
+
# 1. **Defaults:** Definidos en la sesión.
|
|
14
|
+
# 2. **Global:** Definidos en BugBunny.configuration.
|
|
15
|
+
# 3. **Específico:** Definidos en la clase del recurso o vía `with`.
|
|
18
16
|
#
|
|
19
17
|
# @author Gabriel
|
|
20
|
-
# @since 3.0
|
|
18
|
+
# @since 3.1.0
|
|
21
19
|
class Resource
|
|
22
20
|
include ActiveModel::API
|
|
23
21
|
include ActiveModel::Attributes
|
|
@@ -31,11 +29,22 @@ module BugBunny
|
|
|
31
29
|
attr_accessor :persisted
|
|
32
30
|
attr_accessor :routing_key, :exchange, :exchange_type
|
|
33
31
|
|
|
32
|
+
# @return [Hash] Opciones específicas de instancia para exchange y queue.
|
|
33
|
+
attr_accessor :exchange_options, :queue_options
|
|
34
|
+
|
|
34
35
|
class << self
|
|
35
36
|
attr_writer :connection_pool, :exchange, :exchange_type, :resource_name, :routing_key, :param_key
|
|
36
37
|
|
|
38
|
+
# @!group Configuración de Infraestructura Específica
|
|
39
|
+
attr_writer :exchange_options, :queue_options
|
|
40
|
+
|
|
41
|
+
# @api private
|
|
37
42
|
def thread_config(key); Thread.current["bb_#{object_id}_#{key}"]; end
|
|
38
43
|
|
|
44
|
+
# Resuelve la configuración buscando en el hilo, luego en la jerarquía de clases.
|
|
45
|
+
# @param key [Symbol] Clave en el Thread.current.
|
|
46
|
+
# @param instance_var [Symbol] Nombre de la variable de instancia en la clase.
|
|
47
|
+
# @return [Object, nil]
|
|
39
48
|
def resolve_config(key, instance_var)
|
|
40
49
|
val = thread_config(key)
|
|
41
50
|
return val if val
|
|
@@ -48,23 +57,38 @@ module BugBunny
|
|
|
48
57
|
nil
|
|
49
58
|
end
|
|
50
59
|
|
|
60
|
+
# @return [ConnectionPool, nil]
|
|
51
61
|
def connection_pool; resolve_config(:pool, :@connection_pool); end
|
|
52
|
-
|
|
62
|
+
|
|
63
|
+
# @return [String] Nombre del exchange actual.
|
|
64
|
+
def current_exchange; resolve_config(:exchange, :@exchange) || raise(ArgumentError, "Exchange not defined for #{name}"); end
|
|
65
|
+
|
|
66
|
+
# @return [String] Tipo de exchange ('direct', 'topic', 'fanout').
|
|
53
67
|
def current_exchange_type; resolve_config(:exchange_type, :@exchange_type) || 'direct'; end
|
|
54
68
|
|
|
69
|
+
# @return [Hash] Opciones de exchange específicas (Nivel 3 de la cascada).
|
|
70
|
+
def current_exchange_options; resolve_config(:exchange_options, :@exchange_options) || {}; end
|
|
71
|
+
|
|
72
|
+
# @return [Hash] Opciones de cola específicas.
|
|
73
|
+
def current_queue_options; resolve_config(:queue_options, :@queue_options) || {}; end
|
|
74
|
+
|
|
75
|
+
# @return [String] Nombre del recurso para la construcción de rutas.
|
|
55
76
|
def resource_name
|
|
56
77
|
resolve_config(:resource_name, :@resource_name) || name.demodulize.underscore.pluralize
|
|
57
78
|
end
|
|
58
79
|
|
|
80
|
+
# @return [String] Clave raíz para envolver el payload en las peticiones.
|
|
59
81
|
def param_key
|
|
60
82
|
resolve_config(:param_key, :@param_key) || model_name.element
|
|
61
83
|
end
|
|
62
84
|
|
|
85
|
+
# @api private
|
|
63
86
|
def client_middleware(&block)
|
|
64
87
|
@client_middleware_stack ||= []
|
|
65
88
|
@client_middleware_stack << block
|
|
66
89
|
end
|
|
67
90
|
|
|
91
|
+
# @api private
|
|
68
92
|
def resolve_middleware_stack
|
|
69
93
|
stack = []
|
|
70
94
|
target = self
|
|
@@ -76,6 +100,8 @@ module BugBunny
|
|
|
76
100
|
stack
|
|
77
101
|
end
|
|
78
102
|
|
|
103
|
+
# Instancia el cliente inyectando los middlewares configurados.
|
|
104
|
+
# @return [BugBunny::Client]
|
|
79
105
|
def bug_bunny_client
|
|
80
106
|
pool = connection_pool
|
|
81
107
|
raise BugBunny::Error, "Connection pool missing for #{name}" unless pool
|
|
@@ -84,8 +110,23 @@ module BugBunny
|
|
|
84
110
|
end
|
|
85
111
|
end
|
|
86
112
|
|
|
87
|
-
|
|
88
|
-
|
|
113
|
+
# Permite configurar dinámicamente el contexto AMQP para una operación.
|
|
114
|
+
#
|
|
115
|
+
# @param exchange [String] Nombre del exchange.
|
|
116
|
+
# @param routing_key [String] Routing key manual.
|
|
117
|
+
# @param exchange_type [String] Tipo de exchange.
|
|
118
|
+
# @param pool [ConnectionPool] Pool de conexiones.
|
|
119
|
+
# @param exchange_options [Hash] Opciones de infraestructura.
|
|
120
|
+
# @param queue_options [Hash] Opciones de cola.
|
|
121
|
+
def with(exchange: nil, routing_key: nil, exchange_type: nil, pool: nil, exchange_options: nil, queue_options: nil)
|
|
122
|
+
keys = {
|
|
123
|
+
exchange: "bb_#{object_id}_exchange",
|
|
124
|
+
exchange_type: "bb_#{object_id}_exchange_type",
|
|
125
|
+
pool: "bb_#{object_id}_pool",
|
|
126
|
+
routing_key: "bb_#{object_id}_routing_key",
|
|
127
|
+
exchange_options: "bb_#{object_id}_exchange_options",
|
|
128
|
+
queue_options: "bb_#{object_id}_queue_options"
|
|
129
|
+
}
|
|
89
130
|
old_values = {}
|
|
90
131
|
keys.each { |k, v| old_values[k] = Thread.current[v] }
|
|
91
132
|
|
|
@@ -93,6 +134,8 @@ module BugBunny
|
|
|
93
134
|
Thread.current[keys[:exchange_type]] = exchange_type if exchange_type
|
|
94
135
|
Thread.current[keys[:pool]] = pool if pool
|
|
95
136
|
Thread.current[keys[:routing_key]] = routing_key if routing_key
|
|
137
|
+
Thread.current[keys[:exchange_options]] = exchange_options if exchange_options
|
|
138
|
+
Thread.current[keys[:queue_options]] = queue_options if queue_options
|
|
96
139
|
|
|
97
140
|
if block_given?
|
|
98
141
|
begin; yield; ensure; keys.each { |k, v| Thread.current[v] = old_values[k] }; end
|
|
@@ -101,11 +144,15 @@ module BugBunny
|
|
|
101
144
|
end
|
|
102
145
|
end
|
|
103
146
|
|
|
147
|
+
# Proxy para el encadenamiento del método `.with`.
|
|
104
148
|
class ScopeProxy < BasicObject
|
|
105
149
|
def initialize(target, keys, old_values); @target = target; @keys = keys; @old_values = old_values; end
|
|
106
150
|
def method_missing(method, *args, &block); @target.public_send(method, *args, &block); ensure; @keys.each { |k, v| ::Thread.current[v] = @old_values[k] }; end
|
|
107
151
|
end
|
|
108
152
|
|
|
153
|
+
# Calcula la routing key final.
|
|
154
|
+
# @param id [String, nil] ID del recurso.
|
|
155
|
+
# @return [String]
|
|
109
156
|
def calculate_routing_key(id = nil)
|
|
110
157
|
manual_rk = thread_config(:routing_key)
|
|
111
158
|
return manual_rk if manual_rk
|
|
@@ -115,11 +162,25 @@ module BugBunny
|
|
|
115
162
|
end
|
|
116
163
|
|
|
117
164
|
# @!group Acciones CRUD RESTful
|
|
165
|
+
|
|
166
|
+
# Realiza una búsqueda filtrada (GET).
|
|
167
|
+
# @param filters [Hash]
|
|
168
|
+
# @return [Array<BugBunny::Resource>]
|
|
118
169
|
def where(filters = {})
|
|
119
170
|
rk = calculate_routing_key
|
|
120
171
|
path = resource_name
|
|
121
172
|
path += "?#{Rack::Utils.build_nested_query(filters)}" if filters.present?
|
|
122
|
-
|
|
173
|
+
|
|
174
|
+
response = bug_bunny_client.request(
|
|
175
|
+
path,
|
|
176
|
+
method: :get,
|
|
177
|
+
exchange: current_exchange,
|
|
178
|
+
exchange_type: current_exchange_type,
|
|
179
|
+
routing_key: rk,
|
|
180
|
+
exchange_options: current_exchange_options,
|
|
181
|
+
queue_options: current_queue_options
|
|
182
|
+
)
|
|
183
|
+
|
|
123
184
|
return [] unless response['body'].is_a?(Array)
|
|
124
185
|
response['body'].map do |attrs|
|
|
125
186
|
inst = new(attrs)
|
|
@@ -129,12 +190,27 @@ module BugBunny
|
|
|
129
190
|
end
|
|
130
191
|
end
|
|
131
192
|
|
|
193
|
+
# Devuelve todos los registros.
|
|
194
|
+
# @return [Array<BugBunny::Resource>]
|
|
132
195
|
def all; where({}); end
|
|
133
196
|
|
|
197
|
+
# Busca un registro por ID (GET).
|
|
198
|
+
# @param id [String, Integer]
|
|
199
|
+
# @return [BugBunny::Resource, nil]
|
|
134
200
|
def find(id)
|
|
135
201
|
rk = calculate_routing_key(id)
|
|
136
202
|
path = "#{resource_name}/#{id}"
|
|
137
|
-
|
|
203
|
+
|
|
204
|
+
response = bug_bunny_client.request(
|
|
205
|
+
path,
|
|
206
|
+
method: :get,
|
|
207
|
+
exchange: current_exchange,
|
|
208
|
+
exchange_type: current_exchange_type,
|
|
209
|
+
routing_key: rk,
|
|
210
|
+
exchange_options: current_exchange_options,
|
|
211
|
+
queue_options: current_queue_options
|
|
212
|
+
)
|
|
213
|
+
|
|
138
214
|
return nil if response.nil? || response['status'] == 404
|
|
139
215
|
return nil unless response['body'].is_a?(Hash)
|
|
140
216
|
instance = new(response['body'])
|
|
@@ -143,6 +219,9 @@ module BugBunny
|
|
|
143
219
|
instance
|
|
144
220
|
end
|
|
145
221
|
|
|
222
|
+
# Crea una nueva instancia y la persiste.
|
|
223
|
+
# @param payload [Hash]
|
|
224
|
+
# @return [BugBunny::Resource]
|
|
146
225
|
def create(payload)
|
|
147
226
|
instance = new(payload)
|
|
148
227
|
instance.save
|
|
@@ -152,13 +231,19 @@ module BugBunny
|
|
|
152
231
|
|
|
153
232
|
# @!group Instancia
|
|
154
233
|
|
|
234
|
+
# Inicializa el recurso.
|
|
235
|
+
# @param attributes [Hash]
|
|
155
236
|
def initialize(attributes = {})
|
|
156
237
|
@remote_attributes = {}.with_indifferent_access
|
|
157
238
|
@dynamic_changes = Set.new # Rastreo manual para atributos dinámicos
|
|
158
239
|
@persisted = false
|
|
240
|
+
|
|
241
|
+
# Contexto de infraestructura
|
|
159
242
|
@routing_key = self.class.thread_config(:routing_key)
|
|
160
243
|
@exchange = self.class.thread_config(:exchange)
|
|
161
244
|
@exchange_type = self.class.thread_config(:exchange_type)
|
|
245
|
+
@exchange_options = self.class.thread_config(:exchange_options) || self.class.current_exchange_options
|
|
246
|
+
@queue_options = self.class.thread_config(:queue_options) || self.class.current_queue_options
|
|
162
247
|
|
|
163
248
|
super()
|
|
164
249
|
assign_attributes(attributes)
|
|
@@ -170,30 +255,46 @@ module BugBunny
|
|
|
170
255
|
@dynamic_changes.clear
|
|
171
256
|
end
|
|
172
257
|
|
|
258
|
+
# Serialización combinada.
|
|
259
|
+
# @return [Hash]
|
|
173
260
|
def attributes_for_serialization
|
|
174
261
|
@remote_attributes.merge(attributes)
|
|
175
262
|
end
|
|
176
263
|
|
|
264
|
+
# @return [String]
|
|
177
265
|
def calculate_routing_key(id=nil); @routing_key || self.class.calculate_routing_key(id); end
|
|
266
|
+
|
|
267
|
+
# @return [String]
|
|
178
268
|
def current_exchange; @exchange || self.class.current_exchange; end
|
|
269
|
+
|
|
270
|
+
# @return [String]
|
|
179
271
|
def current_exchange_type; @exchange_type || self.class.current_exchange_type; end
|
|
272
|
+
|
|
273
|
+
# @return [BugBunny::Client]
|
|
180
274
|
def bug_bunny_client; self.class.bug_bunny_client; end
|
|
275
|
+
|
|
276
|
+
# @return [Boolean]
|
|
181
277
|
def persisted?; !!@persisted; end
|
|
182
278
|
|
|
279
|
+
# Asignación masiva de atributos.
|
|
280
|
+
# @param new_attributes [Hash]
|
|
183
281
|
def assign_attributes(new_attributes)
|
|
184
282
|
return if new_attributes.nil?
|
|
185
283
|
new_attributes.each { |k, v| public_send("#{k}=", v) }
|
|
186
284
|
end
|
|
187
285
|
|
|
286
|
+
# Actualiza y guarda.
|
|
287
|
+
# @param attributes [Hash]
|
|
288
|
+
# @return [Boolean]
|
|
188
289
|
def update(attributes)
|
|
189
290
|
assign_attributes(attributes)
|
|
190
291
|
save
|
|
191
292
|
end
|
|
192
293
|
|
|
193
294
|
# Retorna el hash combinado de cambios (Tipados + Dinámicos).
|
|
295
|
+
# @return [Hash]
|
|
194
296
|
def changes_to_send
|
|
195
297
|
# 1. Cambios de ActiveModel (Tipados)
|
|
196
|
-
# changes returns { 'attr' => [old, new] } -> nos quedamos con new
|
|
197
298
|
payload = changes.transform_values(&:last)
|
|
198
299
|
|
|
199
300
|
# 2. Cambios Dinámicos (Manuales)
|
|
@@ -214,8 +315,6 @@ module BugBunny
|
|
|
214
315
|
key = attribute_name.chop
|
|
215
316
|
val = args.first
|
|
216
317
|
|
|
217
|
-
# Dirty Tracking Manual
|
|
218
|
-
# Si el valor cambia, lo marcamos en nuestro Set
|
|
219
318
|
if @remote_attributes[key] != val
|
|
220
319
|
@dynamic_changes << key
|
|
221
320
|
end
|
|
@@ -230,6 +329,7 @@ module BugBunny
|
|
|
230
329
|
@remote_attributes.key?(method_name.to_s.sub(/=$/, '')) || super
|
|
231
330
|
end
|
|
232
331
|
|
|
332
|
+
# @return [Object] Valor del ID buscando en múltiples nomenclaturas.
|
|
233
333
|
def id
|
|
234
334
|
attributes['id'] || @remote_attributes['id'] || @remote_attributes['ID'] || @remote_attributes['Id'] || @remote_attributes['_id']
|
|
235
335
|
end
|
|
@@ -249,6 +349,8 @@ module BugBunny
|
|
|
249
349
|
|
|
250
350
|
# @!group Persistencia
|
|
251
351
|
|
|
352
|
+
# Guarda el recurso en el servidor remoto vía AMQP (POST o PUT).
|
|
353
|
+
# @return [Boolean]
|
|
252
354
|
def save
|
|
253
355
|
return false unless valid?
|
|
254
356
|
|
|
@@ -268,6 +370,8 @@ module BugBunny
|
|
|
268
370
|
exchange: current_exchange,
|
|
269
371
|
exchange_type: current_exchange_type,
|
|
270
372
|
routing_key: rk,
|
|
373
|
+
exchange_options: @exchange_options,
|
|
374
|
+
queue_options: @queue_options,
|
|
271
375
|
body: wrapped_payload
|
|
272
376
|
)
|
|
273
377
|
|
|
@@ -278,12 +382,24 @@ module BugBunny
|
|
|
278
382
|
false
|
|
279
383
|
end
|
|
280
384
|
|
|
385
|
+
# Elimina el recurso del servidor remoto (DELETE).
|
|
386
|
+
# @return [Boolean]
|
|
281
387
|
def destroy
|
|
282
388
|
return false unless persisted?
|
|
283
389
|
run_callbacks(:destroy) do
|
|
284
390
|
path = "#{self.class.resource_name}/#{id}"
|
|
285
391
|
rk = calculate_routing_key(id)
|
|
286
|
-
|
|
392
|
+
|
|
393
|
+
bug_bunny_client.request(
|
|
394
|
+
path,
|
|
395
|
+
method: :delete,
|
|
396
|
+
exchange: current_exchange,
|
|
397
|
+
exchange_type: current_exchange_type,
|
|
398
|
+
routing_key: rk,
|
|
399
|
+
exchange_options: @exchange_options,
|
|
400
|
+
queue_options: @queue_options
|
|
401
|
+
)
|
|
402
|
+
|
|
287
403
|
self.persisted = false
|
|
288
404
|
end
|
|
289
405
|
true
|
|
@@ -293,6 +409,7 @@ module BugBunny
|
|
|
293
409
|
|
|
294
410
|
private
|
|
295
411
|
|
|
412
|
+
# Maneja la lógica de respuesta para la acción de guardado.
|
|
296
413
|
def handle_save_response(response)
|
|
297
414
|
if response['status'] == 422
|
|
298
415
|
raise BugBunny::UnprocessableEntity.new(response['body']['errors'] || response['body'])
|
|
@@ -301,12 +418,14 @@ module BugBunny
|
|
|
301
418
|
elsif response['status'] >= 400
|
|
302
419
|
raise BugBunny::ClientError
|
|
303
420
|
end
|
|
421
|
+
|
|
304
422
|
assign_attributes(response['body'])
|
|
305
423
|
self.persisted = true
|
|
306
424
|
clear_changes_information
|
|
307
425
|
true
|
|
308
426
|
end
|
|
309
427
|
|
|
428
|
+
# Carga errores remotos en el objeto local.
|
|
310
429
|
def load_remote_rabbit_errors(errors_hash)
|
|
311
430
|
return if errors_hash.nil?
|
|
312
431
|
if errors_hash.is_a?(String)
|
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
|