retriable 3.5.1 → 3.6.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.
@@ -9,7 +9,7 @@ describe Retriable do
9
9
 
10
10
  before(:each) do
11
11
  described_class.instance_variable_set(:@config, nil)
12
- described_class.reset_override
12
+ Thread.current.thread_variable_set(Retriable::OVERRIDE_THREAD_KEY, nil)
13
13
  described_class.configure { |c| c.sleep_disabled = true }
14
14
  @tries = 0
15
15
  @next_interval_table = {}
@@ -406,6 +406,16 @@ describe Retriable do
406
406
 
407
407
  expect(@tries).to eq(1)
408
408
  end
409
+
410
+ it "rejects on: Object before invoking the block" do
411
+ block_invoked = false
412
+
413
+ expect do
414
+ described_class.retriable(on: Object) { block_invoked = true }
415
+ end.to raise_error(ArgumentError, /on must be an Exception class/)
416
+
417
+ expect(block_invoked).to be(false)
418
+ end
409
419
  end
410
420
 
411
421
  context "#configure" do
@@ -415,8 +425,7 @@ describe Retriable do
415
425
  with_context
416
426
  configure
417
427
  config
418
- override
419
- reset_override
428
+ with_override
420
429
  ]
421
430
 
422
431
  expect(described_class.singleton_methods(false)).to match_array(public_api_methods)
@@ -427,25 +436,23 @@ describe Retriable do
427
436
  end
428
437
  end
429
438
 
430
- context "#override" do
431
- after(:each) do
432
- described_class.reset_override
433
- end
434
-
439
+ context "#with_override" do
435
440
  it "takes precedence over both global config and local options" do
436
441
  described_class.configure { |c| c.tries = 2 }
437
- described_class.override(tries: 1)
438
442
 
439
- expect { described_class.retriable(tries: 10) { increment_tries_with_exception } }.to raise_error(StandardError)
443
+ described_class.with_override(tries: 1) do
444
+ expect { described_class.retriable(tries: 10) { increment_tries_with_exception } }.to raise_error(StandardError)
445
+ end
446
+
440
447
  expect(@tries).to eq(1)
441
448
  end
442
449
 
443
450
  it "lets override tries take precedence over local intervals" do
444
- described_class.override(tries: 1)
445
-
446
- expect do
447
- described_class.retriable(intervals: [0.5, 1.0]) { increment_tries_with_exception }
448
- end.to raise_error(StandardError)
451
+ described_class.with_override(tries: 1) do
452
+ expect do
453
+ described_class.retriable(intervals: [0.5, 1.0]) { increment_tries_with_exception }
454
+ end.to raise_error(StandardError)
455
+ end
449
456
 
450
457
  expect(@tries).to eq(1)
451
458
  end
@@ -454,9 +461,11 @@ describe Retriable do
454
461
  described_class.configure do |c|
455
462
  c.contexts[:api] = { intervals: [0.5, 1.0] }
456
463
  end
457
- described_class.override(tries: 1)
458
464
 
459
- expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
465
+ described_class.with_override(tries: 1) do
466
+ expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
467
+ end
468
+
460
469
  expect(@tries).to eq(1)
461
470
  end
462
471
 
@@ -464,31 +473,34 @@ describe Retriable do
464
473
  described_class.configure do |c|
465
474
  c.contexts[:api] = { intervals: [0.5, 1.0] }
466
475
  end
467
- described_class.override(contexts: { api: { tries: 1 } })
468
476
 
469
- expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
477
+ described_class.with_override(contexts: { api: { tries: 1 } }) do
478
+ expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
479
+ end
480
+
470
481
  expect(@tries).to eq(1)
471
482
  end
472
483
 
473
484
  it "replaces hash-valued options instead of deep-merging them" do
474
- described_class.override(on: { NonStandardError => nil })
475
-
476
- expect do
477
- described_class.retriable(on: { StandardError => nil }, tries: 2) { increment_tries_with_exception }
478
- end.to raise_error(StandardError)
485
+ described_class.with_override(on: { NonStandardError => nil }) do
486
+ expect do
487
+ described_class.retriable(on: { StandardError => nil }, tries: 2) { increment_tries_with_exception }
488
+ end.to raise_error(StandardError)
489
+ end
479
490
 
480
491
  expect(@tries).to eq(1)
481
492
  end
482
493
 
483
494
  it "can override local intervals with nil to use configured backoff" do
484
495
  described_class.configure { |c| c.tries = 3 }
485
- described_class.override(intervals: nil)
486
496
 
487
- expect do
488
- described_class.retriable(intervals: [0.5, 1.0], on_retry: time_table_handler) do
489
- increment_tries_with_exception
490
- end
491
- end.to raise_error(StandardError)
497
+ described_class.with_override(intervals: nil) do
498
+ expect do
499
+ described_class.retriable(intervals: [0.5, 1.0], on_retry: time_table_handler) do
500
+ increment_tries_with_exception
501
+ end
502
+ end.to raise_error(StandardError)
503
+ end
492
504
 
493
505
  expect(@tries).to eq(3)
494
506
  expect(@next_interval_table[1]).to be_between(0.0, 1.0)
@@ -499,23 +511,26 @@ describe Retriable do
499
511
  c.contexts[:api] = { tries: 3, base_interval: 1.0 }
500
512
  end
501
513
 
502
- described_class.override(contexts: { api: { tries: 1 } })
514
+ described_class.with_override(contexts: { api: { tries: 1 } }) do
515
+ described_class.with_context(:api, tries: 10) { increment_tries }
516
+ end
503
517
 
504
- described_class.with_context(:api, tries: 10) { increment_tries }
505
518
  expect(@tries).to eq(1)
506
519
  end
507
520
 
508
521
  it "can define a context only in override config" do
509
- described_class.override(contexts: { test_only: { tries: 1 } })
522
+ described_class.with_override(contexts: { test_only: { tries: 1 } }) do
523
+ described_class.with_context(:test_only) { increment_tries }
524
+ end
510
525
 
511
- described_class.with_context(:test_only) { increment_tries }
512
526
  expect(@tries).to eq(1)
513
527
  end
514
528
 
515
529
  it "does not apply context-only overrides to plain retriable calls" do
516
- described_class.override(contexts: { api: { tries: 1 } })
530
+ described_class.with_override(contexts: { api: { tries: 1 } }) do
531
+ expect { described_class.retriable(tries: 3) { increment_tries_with_exception } }.to raise_error(StandardError)
532
+ end
517
533
 
518
- expect { described_class.retriable(tries: 3) { increment_tries_with_exception } }.to raise_error(StandardError)
519
534
  expect(@tries).to eq(3)
520
535
  end
521
536
 
@@ -523,21 +538,24 @@ describe Retriable do
523
538
  described_class.configure do |c|
524
539
  c.contexts[:api] = { tries: 3, on: NonStandardError }
525
540
  end
526
- described_class.override(tries: 1)
527
541
 
528
- expect { described_class.with_context(:api) { increment_tries_with_exception(NonStandardError) } }
529
- .to raise_error(NonStandardError)
542
+ described_class.with_override(tries: 1) do
543
+ expect { described_class.with_context(:api) { increment_tries_with_exception(NonStandardError) } }
544
+ .to raise_error(NonStandardError)
545
+ end
546
+
530
547
  expect(@tries).to eq(1)
531
548
  end
532
549
 
533
550
  it "combines local options with override-only contexts" do
534
- described_class.override(contexts: { api: { tries: 1 } })
551
+ described_class.with_override(contexts: { api: { tries: 1 } }) do
552
+ expect do
553
+ described_class.with_context(:api, on: NonStandardError) do
554
+ increment_tries_with_exception(NonStandardError)
555
+ end
556
+ end.to raise_error(NonStandardError)
557
+ end
535
558
 
536
- expect do
537
- described_class.with_context(:api, on: NonStandardError) do
538
- increment_tries_with_exception(NonStandardError)
539
- end
540
- end.to raise_error(NonStandardError)
541
559
  expect(@tries).to eq(1)
542
560
  end
543
561
 
@@ -546,9 +564,10 @@ describe Retriable do
546
564
  c.contexts[:api] = { tries: 1 }
547
565
  end
548
566
 
549
- described_class.override(tries: 1)
567
+ described_class.with_override(tries: 1) do
568
+ described_class.with_context(:api) { increment_tries }
569
+ end
550
570
 
551
- described_class.with_context(:api) { increment_tries }
552
571
  expect(@tries).to eq(1)
553
572
  end
554
573
 
@@ -556,9 +575,10 @@ describe Retriable do
556
575
  begin
557
576
  described_class.configure { |c| c.contexts = nil }
558
577
 
559
- described_class.override(contexts: { api: { tries: 1 } })
578
+ described_class.with_override(contexts: { api: { tries: 1 } }) do
579
+ described_class.with_context(:api) { increment_tries }
580
+ end
560
581
 
561
- described_class.with_context(:api) { increment_tries }
562
582
  expect(@tries).to eq(1)
563
583
  ensure
564
584
  described_class.configure { |c| c.contexts = {} }
@@ -570,31 +590,54 @@ describe Retriable do
570
590
  c.contexts[:api] = { tries: 1 }
571
591
  end
572
592
 
573
- described_class.override(contexts: nil)
593
+ described_class.with_override(contexts: nil) do
594
+ described_class.with_context(:api) { increment_tries }
595
+ end
574
596
 
575
- described_class.with_context(:api) { increment_tries }
576
597
  expect(@tries).to eq(1)
577
598
  end
578
599
 
579
- it "ignores non-hash override contexts values in with_context" do
580
- described_class.configure do |c|
581
- c.contexts[:api] = { tries: 1 }
582
- end
600
+ it "raises ArgumentError on non-hash override contexts values" do
601
+ block_called = false
583
602
 
584
- described_class.override(contexts: 123)
603
+ expect { described_class.with_override(contexts: 123) { block_called = true } }
604
+ .to raise_error(ArgumentError, /contexts must be a Hash or nil/)
605
+ expect(block_called).to be(false)
606
+ end
585
607
 
586
- described_class.with_context(:api) { increment_tries }
587
- expect(@tries).to eq(1)
608
+ it "raises ArgumentError on non-hash per-context override values" do
609
+ block_called = false
610
+
611
+ expect { described_class.with_override(contexts: { api: 123 }) { block_called = true } }
612
+ .to raise_error(ArgumentError, /contexts\[:api\] must be a Hash/)
613
+ expect(block_called).to be(false)
588
614
  end
589
615
 
590
- it "ignores non-hash per-context override values in with_context" do
616
+ it "preserves outer override after rejected nested override contexts values" do
617
+ described_class.with_override(tries: 2) do
618
+ expect { described_class.with_override(tries: 1, contexts: 123) { :noop } }
619
+ .to raise_error(ArgumentError, /contexts must be a Hash or nil/)
620
+
621
+ expect { described_class.retriable(tries: 10) { increment_tries_with_exception } }
622
+ .to raise_error(StandardError)
623
+ end
624
+
625
+ expect(@tries).to eq(2)
626
+ end
627
+
628
+ it "preserves outer context override after rejected nested per-context values" do
591
629
  described_class.configure do |c|
592
- c.contexts[:api] = { tries: 2 }
630
+ c.contexts[:api] = { tries: 10 }
593
631
  end
594
632
 
595
- described_class.override(contexts: { api: 123 })
633
+ described_class.with_override(contexts: { api: { tries: 2 } }) do
634
+ expect { described_class.with_override(contexts: { api: 123 }) { :noop } }
635
+ .to raise_error(ArgumentError, /contexts\[:api\] must be a Hash/)
636
+
637
+ expect { described_class.with_context(:api) { increment_tries_with_exception } }
638
+ .to raise_error(StandardError)
639
+ end
596
640
 
597
- expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
598
641
  expect(@tries).to eq(2)
599
642
  end
600
643
 
@@ -603,10 +646,10 @@ describe Retriable do
603
646
  c.contexts[:configured] = { tries: 2 }
604
647
  end
605
648
 
606
- described_class.override(contexts: { override_only: { tries: 1 } })
607
-
608
- expect { described_class.with_context(:missing) { increment_tries } }
609
- .to raise_error(ArgumentError, /override_only/)
649
+ described_class.with_override(contexts: { override_only: { tries: 1 } }) do
650
+ expect { described_class.with_context(:missing) { increment_tries } }
651
+ .to raise_error(ArgumentError, /override_only/)
652
+ end
610
653
  end
611
654
 
612
655
  it "does not snapshot configured contexts when adding override-only contexts" do
@@ -614,37 +657,200 @@ describe Retriable do
614
657
  c.contexts[:api] = { tries: 2 }
615
658
  end
616
659
 
617
- described_class.override(contexts: { test_only: { tries: 1 } })
660
+ described_class.with_override(contexts: { test_only: { tries: 1 } }) do
661
+ described_class.configure do |c|
662
+ c.contexts[:api] = { tries: 5 }
663
+ end
618
664
 
619
- described_class.configure do |c|
620
- c.contexts[:api] = { tries: 5 }
665
+ expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
621
666
  end
622
667
 
623
- expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
624
668
  expect(@tries).to eq(5)
625
669
  end
626
670
 
627
671
  it "raises ArgumentError on invalid override options" do
628
- expect { described_class.override(does_not_exist: 123) }.to raise_error(ArgumentError)
672
+ expect { described_class.with_override(does_not_exist: 123) { :noop } }.to raise_error(ArgumentError)
629
673
  end
630
674
 
631
675
  it "raises ArgumentError on empty override options" do
632
- expect { described_class.override({}) }.to raise_error(ArgumentError, /empty override/)
676
+ expect { described_class.with_override({}) { :noop } }.to raise_error(ArgumentError, /empty override/)
677
+ end
678
+
679
+ it "raises ArgumentError when called without a block" do
680
+ expect { described_class.with_override(tries: 1) }.to raise_error(ArgumentError, /requires a block/)
633
681
  end
634
682
 
635
683
  it "raises ArgumentError on invalid context override options" do
636
- expect { described_class.override(contexts: { api: { does_not_exist: 123 } }) }
684
+ expect { described_class.with_override(contexts: { api: { does_not_exist: 123 } }) { :noop } }
637
685
  .to raise_error(ArgumentError, /does_not_exist is not a valid option/)
638
686
  end
639
687
 
640
- it "does not copy the provided override options" do
641
- opts = { tries: 1 }
642
- described_class.override(opts)
688
+ it "clears the override after the block returns" do
689
+ described_class.with_override(tries: 1) do
690
+ # active here
691
+ end
643
692
 
644
- opts[:tries] = 2
693
+ expect { described_class.retriable(tries: 3) { increment_tries_with_exception } }.to raise_error(StandardError)
694
+ expect(@tries).to eq(3)
695
+ end
645
696
 
646
- expect { described_class.retriable(tries: 10) { increment_tries_with_exception } }.to raise_error(StandardError)
647
- expect(@tries).to eq(2)
697
+ it "clears the override when the block raises" do
698
+ expect do
699
+ described_class.with_override(tries: 1) { raise "boom" }
700
+ end.to raise_error(RuntimeError, "boom")
701
+
702
+ expect { described_class.retriable(tries: 3) { increment_tries_with_exception } }.to raise_error(StandardError)
703
+ expect(@tries).to eq(3)
704
+ end
705
+
706
+ it "returns the block's return value" do
707
+ result = described_class.with_override(tries: 1) { :return_value }
708
+ expect(result).to eq(:return_value)
709
+ end
710
+
711
+ it "restores the outer override when nested blocks exit" do
712
+ tries_seen = []
713
+ handler = ->(_exception, try, _elapsed, _next) { tries_seen << [Thread.current.object_id, try] }
714
+
715
+ described_class.with_override(tries: 2, on_retry: handler) do
716
+ described_class.with_override(tries: 4, on_retry: handler) do
717
+ expect { described_class.retriable { increment_tries_with_exception } }.to raise_error(StandardError)
718
+ end
719
+
720
+ # After the inner block exits, the outer tries: 2 override is restored.
721
+ @tries = 0
722
+ expect { described_class.retriable { increment_tries_with_exception } }.to raise_error(StandardError)
723
+ expect(@tries).to eq(2)
724
+ end
725
+ end
726
+ end
727
+
728
+ context "#with_override thread safety" do
729
+ # Coordinate threads with queues rather than sleep so tests are deterministic.
730
+ # sleep_disabled is already set to true in the top-level before(:each), so
731
+ # retriable calls do not actually sleep between attempts.
732
+
733
+ it "isolates overrides between threads" do
734
+ ready = Queue.new
735
+ proceed = Queue.new
736
+ results = {}
737
+ mutex = Mutex.new
738
+
739
+ threads = [1, 2].map do |id|
740
+ Thread.new do
741
+ described_class.with_override(tries: id) do
742
+ ready << true
743
+ proceed.pop
744
+ tries = 0
745
+ begin
746
+ described_class.retriable do
747
+ tries += 1
748
+ raise StandardError
749
+ end
750
+ rescue StandardError
751
+ mutex.synchronize { results[id] = tries }
752
+ end
753
+ end
754
+ end
755
+ end
756
+
757
+ 2.times { ready.pop }
758
+ 2.times { proceed << true }
759
+ threads.each(&:join)
760
+
761
+ expect(results).to eq(1 => 1, 2 => 2)
762
+ end
763
+
764
+ it "does not leak an active override into a sibling thread" do
765
+ override_active = Queue.new
766
+ sibling_done = Queue.new
767
+ sibling_tries = nil
768
+
769
+ setter = Thread.new do
770
+ described_class.with_override(tries: 1) do
771
+ override_active << true
772
+ sibling_done.pop
773
+ end
774
+ end
775
+
776
+ sibling = Thread.new do
777
+ override_active.pop
778
+ tries = 0
779
+ begin
780
+ described_class.retriable(tries: 3) do
781
+ tries += 1
782
+ raise StandardError
783
+ end
784
+ rescue StandardError
785
+ sibling_tries = tries
786
+ end
787
+ sibling_done << true
788
+ end
789
+
790
+ [setter, sibling].each(&:join)
791
+ expect(sibling_tries).to eq(3)
792
+ end
793
+
794
+ it "does not propagate an active override to a child thread" do
795
+ child_tries = nil
796
+
797
+ described_class.with_override(tries: 1) do
798
+ Thread.new do
799
+ tries = 0
800
+ begin
801
+ described_class.retriable(tries: 3) do
802
+ tries += 1
803
+ raise StandardError
804
+ end
805
+ rescue StandardError
806
+ child_tries = tries
807
+ end
808
+ end.join
809
+ end
810
+
811
+ expect(child_tries).to eq(3)
812
+ end
813
+
814
+ it "shares the active override with fibers in the same thread" do
815
+ fiber_tries = nil
816
+
817
+ Thread.new do
818
+ described_class.with_override(tries: 1) do
819
+ Fiber.new do
820
+ tries = 0
821
+ begin
822
+ described_class.retriable(tries: 10) do
823
+ tries += 1
824
+ raise StandardError
825
+ end
826
+ rescue StandardError
827
+ fiber_tries = tries
828
+ end
829
+ end.resume
830
+ end
831
+ end.join
832
+
833
+ expect(fiber_tries).to eq(1)
834
+ end
835
+
836
+ it "does not treat a main-thread override as a global default for other threads" do
837
+ other_thread_tries = nil
838
+
839
+ described_class.with_override(tries: 1) do
840
+ Thread.new do
841
+ tries = 0
842
+ begin
843
+ described_class.retriable(tries: 3) do
844
+ tries += 1
845
+ raise StandardError
846
+ end
847
+ rescue StandardError
848
+ other_thread_tries = tries
849
+ end
850
+ end.join
851
+ end
852
+
853
+ expect(other_thread_tries).to eq(3)
648
854
  end
649
855
  end
650
856
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: retriable
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.5.1
4
+ version: 3.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jack Chu
@@ -74,6 +74,8 @@ files:
74
74
  - Rakefile
75
75
  - bin/console
76
76
  - bin/setup
77
+ - docs/superpowers/specs/2026-05-26-on-give-up-callback-followups-design.md
78
+ - docs/testing.md
77
79
  - lib/retriable.rb
78
80
  - lib/retriable/config.rb
79
81
  - lib/retriable/core_ext/kernel.rb