bug_bunny 4.14.0 → 4.16.0
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 +33 -0
- data/README.md +35 -4
- data/lib/bug_bunny/client.rb +37 -0
- data/lib/bug_bunny/configuration.rb +14 -0
- data/lib/bug_bunny/exception.rb +61 -0
- data/lib/bug_bunny/producer.rb +104 -0
- data/lib/bug_bunny/request.rb +5 -1
- data/lib/bug_bunny/session.rb +98 -3
- data/lib/bug_bunny/version.rb +1 -1
- data/skill/SKILL.md +42 -7
- data/skill/references/client-middleware.md +63 -15
- data/skill/references/errores.md +7 -0
- data/spec/integration/publisher_confirms_spec.rb +304 -0
- data/spec/spec_helper.rb +4 -1
- data/spec/support/integration_helper.rb +8 -1
- data/spec/unit/client_session_pool_spec.rb +72 -0
- data/spec/unit/configuration_spec.rb +12 -0
- data/spec/unit/producer_spec.rb +207 -0
- data/spec/unit/request_spec.rb +16 -0
- data/spec/unit/session_spec.rb +72 -0
- metadata +4 -3
|
@@ -174,6 +174,18 @@ RSpec.describe BugBunny::Configuration do
|
|
|
174
174
|
end
|
|
175
175
|
end
|
|
176
176
|
|
|
177
|
+
describe 'return_raise flag' do
|
|
178
|
+
it 'tiene default true (raise PublishUnroutable en basic.return)' do
|
|
179
|
+
expect(BugBunny::Configuration.new.return_raise).to be(true)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it 'acepta false para opt-out (modo legacy)' do
|
|
183
|
+
configure_with(return_raise: false)
|
|
184
|
+
|
|
185
|
+
expect(BugBunny.configuration.return_raise).to be(false)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
177
189
|
describe '.validate! directamente' do
|
|
178
190
|
it 'es invocable directamente sobre la instancia' do
|
|
179
191
|
config = BugBunny::Configuration.new
|
data/spec/unit/producer_spec.rb
CHANGED
|
@@ -248,6 +248,12 @@ RSpec.describe BugBunny::Producer do
|
|
|
248
248
|
s = instance_double(BugBunny::Session)
|
|
249
249
|
allow(s).to receive(:exchange).and_return(fake_exchange)
|
|
250
250
|
allow(s).to receive(:channel).and_return(mock_channel)
|
|
251
|
+
# Return registry stubs: tests que activan `mandatory: true` con `return_raise`
|
|
252
|
+
# default invocan estos hooks. Devolvemos un event/slot vacío (no return).
|
|
253
|
+
allow(s).to receive(:register_return_listener) do |_cid|
|
|
254
|
+
[Concurrent::Event.new, { event: Concurrent::Event.new, info: nil }]
|
|
255
|
+
end
|
|
256
|
+
allow(s).to receive(:unregister_return_listener)
|
|
251
257
|
s
|
|
252
258
|
end
|
|
253
259
|
|
|
@@ -384,5 +390,206 @@ RSpec.describe BugBunny::Producer do
|
|
|
384
390
|
expect { confirmed_producer.confirmed(build_request) }
|
|
385
391
|
.to raise_error(BugBunny::CommunicationError, 'chan dead')
|
|
386
392
|
end
|
|
393
|
+
|
|
394
|
+
describe 'return_raise (basic.return + mandatory)' do
|
|
395
|
+
# Dispara el `basic.return` desde el reader-thread simulado. El stub de
|
|
396
|
+
# wait_for_confirms invoca este lambda *antes* de retornar true, simulando
|
|
397
|
+
# el orden AMQP wire: return precede al ack.
|
|
398
|
+
def stub_return_during_wait(producer, return_info)
|
|
399
|
+
allow(mock_channel).to receive(:wait_for_confirms) do
|
|
400
|
+
props = double('properties', correlation_id: producer_pending_cid(producer))
|
|
401
|
+
producer.instance_variable_get(:@session)
|
|
402
|
+
.send(:handle_broker_return, return_info, props, 'payload')
|
|
403
|
+
true
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def producer_pending_cid(producer)
|
|
408
|
+
session = producer.instance_variable_get(:@session)
|
|
409
|
+
registry = session.instance_variable_get(:@pending_returns)
|
|
410
|
+
cids = []
|
|
411
|
+
registry.each_pair { |cid, _slot| cids << cid }
|
|
412
|
+
cids.first
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
let(:return_info) do
|
|
416
|
+
Struct.new(:reply_code, :reply_text, :exchange, :routing_key)
|
|
417
|
+
.new(312, 'NO_ROUTE', 'acct_x', 'acct.unbound')
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
let(:real_session) do
|
|
421
|
+
s = BugBunny::Session.new(BunnyMocks::FakeConnection.new(true, mock_channel))
|
|
422
|
+
allow(s).to receive(:channel).and_return(mock_channel)
|
|
423
|
+
allow(s).to receive(:exchange).and_return(fake_exchange)
|
|
424
|
+
s
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
let(:return_producer) do
|
|
428
|
+
p = described_class.new(real_session)
|
|
429
|
+
allow(p).to receive(:safe_log) do |level, event, **kwargs|
|
|
430
|
+
logged_events << { level: level, event: event, kwargs: kwargs }
|
|
431
|
+
end
|
|
432
|
+
p
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
before do
|
|
436
|
+
BugBunny.configuration.on_return = nil
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
after do
|
|
440
|
+
BugBunny.configuration.on_return = nil
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def build_mandatory_request
|
|
444
|
+
req = BugBunny::Request.new('acct.start')
|
|
445
|
+
req.exchange = 'acct_x'
|
|
446
|
+
req.method = :post
|
|
447
|
+
req.body = { tenant: 42 }
|
|
448
|
+
req.mandatory = true
|
|
449
|
+
req
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
it 'levanta PublishUnroutable cuando llega basic.return + ack y mandatory:true (default)' do
|
|
453
|
+
stub_return_during_wait(return_producer, return_info)
|
|
454
|
+
|
|
455
|
+
req = build_mandatory_request
|
|
456
|
+
|
|
457
|
+
expect { return_producer.confirmed(req) }.to raise_error(BugBunny::PublishUnroutable) do |err|
|
|
458
|
+
expect(err.path).to eq('acct.start')
|
|
459
|
+
expect(err.exchange).to eq('acct_x')
|
|
460
|
+
expect(err.routing_key).to eq('acct.unbound')
|
|
461
|
+
expect(err.reply_code).to eq(312)
|
|
462
|
+
expect(err.reply_text).to eq('NO_ROUTE')
|
|
463
|
+
expect(err.correlation_id).to eq(req.correlation_id)
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
it 'logea producer.publish_unroutable antes de levantar' do
|
|
468
|
+
stub_return_during_wait(return_producer, return_info)
|
|
469
|
+
|
|
470
|
+
expect { return_producer.confirmed(build_mandatory_request) }
|
|
471
|
+
.to raise_error(BugBunny::PublishUnroutable)
|
|
472
|
+
|
|
473
|
+
ev = logged_events.find { |e| e[:event] == 'producer.publish_unroutable' }
|
|
474
|
+
expect(ev).not_to be_nil
|
|
475
|
+
expect(ev[:level]).to eq(:warn)
|
|
476
|
+
expect(ev[:kwargs]).to include(
|
|
477
|
+
path: 'acct.start',
|
|
478
|
+
exchange: 'acct_x',
|
|
479
|
+
routing_key: 'acct.unbound',
|
|
480
|
+
reply_code: 312
|
|
481
|
+
)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
it 'auto-asigna correlation_id cuando falta y return_raise está activo' do
|
|
485
|
+
stub_return_during_wait(return_producer, return_info)
|
|
486
|
+
req = build_mandatory_request
|
|
487
|
+
expect(req.correlation_id).to be_nil
|
|
488
|
+
|
|
489
|
+
expect { return_producer.confirmed(req) }.to raise_error(BugBunny::PublishUnroutable)
|
|
490
|
+
expect(req.correlation_id).to be_a(String)
|
|
491
|
+
expect(req.correlation_id).not_to be_empty
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
it 'limpia el listener del registry tras un return (no leak)' do
|
|
495
|
+
stub_return_during_wait(return_producer, return_info)
|
|
496
|
+
req = build_mandatory_request
|
|
497
|
+
|
|
498
|
+
expect { return_producer.confirmed(req) }.to raise_error(BugBunny::PublishUnroutable)
|
|
499
|
+
|
|
500
|
+
registry = real_session.instance_variable_get(:@pending_returns)
|
|
501
|
+
expect(registry.size).to eq(0)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
it 'limpia el listener del registry tras un ack normal sin return' do
|
|
505
|
+
# mock_channel.wait_for_confirms default ya devuelve true sin disparar return
|
|
506
|
+
req = build_mandatory_request
|
|
507
|
+
result = return_producer.confirmed(req)
|
|
508
|
+
|
|
509
|
+
expect(result).to eq('status' => 202, 'body' => nil)
|
|
510
|
+
registry = real_session.instance_variable_get(:@pending_returns)
|
|
511
|
+
expect(registry.size).to eq(0)
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
it 'limpia el listener del registry tras timeout en wait_for_confirms' do
|
|
515
|
+
allow(mock_channel).to receive(:wait_for_confirms) {
|
|
516
|
+
sleep 1
|
|
517
|
+
true
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
req = build_mandatory_request
|
|
521
|
+
req.confirm_timeout = 0.05
|
|
522
|
+
|
|
523
|
+
expect { return_producer.confirmed(req) }.to raise_error(BugBunny::RequestTimeout)
|
|
524
|
+
registry = real_session.instance_variable_get(:@pending_returns)
|
|
525
|
+
expect(registry.size).to eq(0)
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
it 'no levanta cuando el request override `return_raise: false`' do
|
|
529
|
+
# No stub de return — el listener no se registra siquiera.
|
|
530
|
+
req = build_mandatory_request
|
|
531
|
+
req.return_raise = false
|
|
532
|
+
|
|
533
|
+
result = return_producer.confirmed(req)
|
|
534
|
+
expect(result).to eq('status' => 202, 'body' => nil)
|
|
535
|
+
# Sin listener registrado (flag off) → no hubo set up
|
|
536
|
+
registry = real_session.instance_variable_get(:@pending_returns)
|
|
537
|
+
expect(registry.size).to eq(0)
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
it 'no levanta cuando la config global tiene return_raise=false (aunque mandatory esté on)' do
|
|
541
|
+
allow(BugBunny.configuration).to receive(:return_raise).and_return(false)
|
|
542
|
+
|
|
543
|
+
req = build_mandatory_request
|
|
544
|
+
result = return_producer.confirmed(req)
|
|
545
|
+
|
|
546
|
+
expect(result).to eq('status' => 202, 'body' => nil)
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
it 'override per-request gana sobre la config global' do
|
|
550
|
+
allow(BugBunny.configuration).to receive(:return_raise).and_return(false)
|
|
551
|
+
stub_return_during_wait(return_producer, return_info)
|
|
552
|
+
|
|
553
|
+
req = build_mandatory_request
|
|
554
|
+
req.return_raise = true
|
|
555
|
+
|
|
556
|
+
expect { return_producer.confirmed(req) }.to raise_error(BugBunny::PublishUnroutable)
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
it 'flag inerte cuando mandatory:false aunque return_raise=true' do
|
|
560
|
+
req = BugBunny::Request.new('acct.start')
|
|
561
|
+
req.exchange = 'acct_x'
|
|
562
|
+
req.method = :post
|
|
563
|
+
req.body = { tenant: 42 }
|
|
564
|
+
req.mandatory = false
|
|
565
|
+
req.return_raise = true
|
|
566
|
+
|
|
567
|
+
result = return_producer.confirmed(req)
|
|
568
|
+
expect(result).to eq('status' => 202, 'body' => nil)
|
|
569
|
+
registry = real_session.instance_variable_get(:@pending_returns)
|
|
570
|
+
expect(registry.size).to eq(0)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
it 'invoca el callback global on_return antes de levantar PublishUnroutable' do
|
|
574
|
+
captured = nil
|
|
575
|
+
BugBunny.configuration.on_return = lambda { |ri, _props, _body|
|
|
576
|
+
captured = { rk: ri.routing_key }
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
stub_return_during_wait(return_producer, return_info)
|
|
580
|
+
|
|
581
|
+
expect { return_producer.confirmed(build_mandatory_request) }
|
|
582
|
+
.to raise_error(BugBunny::PublishUnroutable)
|
|
583
|
+
expect(captured).to eq(rk: 'acct.unbound')
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
it 'levanta PublishUnroutable aunque el user_cb on_return explote' do
|
|
587
|
+
BugBunny.configuration.on_return = ->(_, _, _) { raise 'boom in user cb' }
|
|
588
|
+
stub_return_during_wait(return_producer, return_info)
|
|
589
|
+
|
|
590
|
+
expect { return_producer.confirmed(build_mandatory_request) }
|
|
591
|
+
.to raise_error(BugBunny::PublishUnroutable)
|
|
592
|
+
end
|
|
593
|
+
end
|
|
387
594
|
end
|
|
388
595
|
end
|
data/spec/unit/request_spec.rb
CHANGED
|
@@ -76,5 +76,21 @@ RSpec.describe BugBunny::Request do
|
|
|
76
76
|
expect(req.mandatory).to be(true)
|
|
77
77
|
expect(req.confirm_timeout).to eq(0.5)
|
|
78
78
|
end
|
|
79
|
+
|
|
80
|
+
it 'tiene return_raise=nil por defecto (delega a config global)' do
|
|
81
|
+
req = described_class.new('foo')
|
|
82
|
+
|
|
83
|
+
expect(req.return_raise).to be_nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'permite asignar return_raise' do
|
|
87
|
+
req = described_class.new('foo')
|
|
88
|
+
|
|
89
|
+
req.return_raise = false
|
|
90
|
+
expect(req.return_raise).to be(false)
|
|
91
|
+
|
|
92
|
+
req.return_raise = true
|
|
93
|
+
expect(req.return_raise).to be(true)
|
|
94
|
+
end
|
|
79
95
|
end
|
|
80
96
|
end
|
data/spec/unit/session_spec.rb
CHANGED
|
@@ -151,6 +151,78 @@ RSpec.describe BugBunny::Session do
|
|
|
151
151
|
end
|
|
152
152
|
end
|
|
153
153
|
|
|
154
|
+
describe '#register_return_listener / #unregister_return_listener' do
|
|
155
|
+
let(:properties_for) do
|
|
156
|
+
->(cid) { double('properties', correlation_id: cid) }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
let(:return_info) do
|
|
160
|
+
Struct.new(:reply_code, :reply_text, :exchange, :routing_key)
|
|
161
|
+
.new(312, 'NO_ROUTE', 'evt_x', 'rk')
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
before do
|
|
165
|
+
BugBunny.configuration.on_return = nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
after do
|
|
169
|
+
BugBunny.configuration.on_return = nil
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it 'devuelve un Concurrent::Event y un slot que se setean al disparar el return' do
|
|
173
|
+
event, slot = session.register_return_listener('corr-1')
|
|
174
|
+
|
|
175
|
+
expect(event).to be_a(Concurrent::Event)
|
|
176
|
+
expect(slot[:info]).to be_nil
|
|
177
|
+
|
|
178
|
+
session.send(:handle_broker_return, return_info, properties_for.call('corr-1'), 'payload')
|
|
179
|
+
|
|
180
|
+
expect(event.set?).to be(true)
|
|
181
|
+
expect(slot[:info]).to eq(return_info)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'no toca otros listeners cuando llega un return de otro correlation_id' do
|
|
185
|
+
event_a, slot_a = session.register_return_listener('corr-A')
|
|
186
|
+
event_b, slot_b = session.register_return_listener('corr-B')
|
|
187
|
+
|
|
188
|
+
session.send(:handle_broker_return, return_info, properties_for.call('corr-A'), 'p')
|
|
189
|
+
|
|
190
|
+
expect(event_a.set?).to be(true)
|
|
191
|
+
expect(slot_a[:info]).to eq(return_info)
|
|
192
|
+
expect(event_b.set?).to be(false)
|
|
193
|
+
expect(slot_b[:info]).to be_nil
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it 'ignora returns sin correlation_id en properties' do
|
|
197
|
+
event, slot = session.register_return_listener('corr-1')
|
|
198
|
+
props = double('properties', correlation_id: nil)
|
|
199
|
+
|
|
200
|
+
expect { session.send(:handle_broker_return, return_info, props, 'p') }.not_to raise_error
|
|
201
|
+
expect(event.set?).to be(false)
|
|
202
|
+
expect(slot[:info]).to be_nil
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it '#unregister_return_listener limpia el slot del registry' do
|
|
206
|
+
session.register_return_listener('corr-1')
|
|
207
|
+
session.unregister_return_listener('corr-1')
|
|
208
|
+
|
|
209
|
+
registry = session.instance_variable_get(:@pending_returns)
|
|
210
|
+
expect(registry['corr-1']).to be_nil
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it 'setea el event ANTES de invocar el callback global on_return (resiliencia a user_cb que explota)' do
|
|
214
|
+
BugBunny.configuration.on_return = ->(_, _, _) { raise 'boom in user callback' }
|
|
215
|
+
|
|
216
|
+
event, slot = session.register_return_listener('corr-1')
|
|
217
|
+
|
|
218
|
+
expect { session.send(:handle_broker_return, return_info, properties_for.call('corr-1'), 'p') }
|
|
219
|
+
.not_to raise_error
|
|
220
|
+
|
|
221
|
+
expect(event.set?).to be(true)
|
|
222
|
+
expect(slot[:info]).to eq(return_info)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
154
226
|
describe '#close' do
|
|
155
227
|
it 'cierra el canal y lo nilifica' do
|
|
156
228
|
session.channel
|
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: 4.
|
|
4
|
+
version: 4.16.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gabix
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|
|
@@ -275,6 +275,7 @@ files:
|
|
|
275
275
|
- spec/integration/controller_spec.rb
|
|
276
276
|
- spec/integration/error_handling_spec.rb
|
|
277
277
|
- spec/integration/infrastructure_spec.rb
|
|
278
|
+
- spec/integration/publisher_confirms_spec.rb
|
|
278
279
|
- spec/integration/resource_spec.rb
|
|
279
280
|
- spec/spec_helper.rb
|
|
280
281
|
- spec/support/bunny_mocks.rb
|
|
@@ -303,7 +304,7 @@ metadata:
|
|
|
303
304
|
homepage_uri: https://github.com/gedera/bug_bunny
|
|
304
305
|
source_code_uri: https://github.com/gedera/bug_bunny
|
|
305
306
|
changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
|
|
306
|
-
documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.
|
|
307
|
+
documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.16.0/skill
|
|
307
308
|
post_install_message:
|
|
308
309
|
rdoc_options: []
|
|
309
310
|
require_paths:
|