retriable 4.2.0 → 5.0.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/AGENTS.md +15 -0
- data/CHANGELOG.md +71 -1
- data/Gemfile +2 -0
- data/README.md +70 -44
- data/benchmark/config_publication.rb +79 -0
- data/docs/adr/0001-copy-on-write-config-publication.md +124 -0
- data/docs/agents/domain.md +38 -0
- data/docs/agents/issue-tracker.md +45 -0
- data/docs/agents/triage-labels.md +17 -0
- data/docs/migration.md +84 -0
- data/lib/retriable/config.rb +104 -0
- data/lib/retriable/version.rb +1 -1
- data/lib/retriable.rb +105 -19
- data/sig/retriable.rbs +1 -1
- data/spec/config_spec.rb +149 -0
- data/spec/retriable_spec.rb +321 -9
- metadata +9 -2
data/spec/retriable_spec.rb
CHANGED
|
@@ -8,7 +8,10 @@ describe Retriable do
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
before(:each) do
|
|
11
|
-
|
|
11
|
+
# Reset to a pristine published snapshot, matching what load-time init
|
|
12
|
+
# publishes. Resetting to nil instead would force #config to carry a
|
|
13
|
+
# nil-guard that only the suite can reach.
|
|
14
|
+
described_class.instance_variable_set(:@config, Retriable::Config.new.freeze)
|
|
12
15
|
Thread.current.thread_variable_set(Retriable::OVERRIDE_THREAD_KEY, nil)
|
|
13
16
|
described_class.configure { |c| c.sleep_disabled = true }
|
|
14
17
|
@tries = 0
|
|
@@ -25,6 +28,31 @@ describe Retriable do
|
|
|
25
28
|
raise exception_class, "#{exception_class} occurred"
|
|
26
29
|
end
|
|
27
30
|
|
|
31
|
+
# Wall clock appears in the concurrency specs only as a deadlock backstop,
|
|
32
|
+
# never as a timing assertion. Every worker they start finishes in
|
|
33
|
+
# milliseconds, so this budget is approached only when a regression leaves a
|
|
34
|
+
# thread genuinely stuck.
|
|
35
|
+
let(:deadlock_backstop_seconds) { 10 }
|
|
36
|
+
|
|
37
|
+
# Joins each thread within the backstop, failing the example instead of hanging
|
|
38
|
+
# the suite when one never finishes. A bare Thread#join turns a serialization
|
|
39
|
+
# regression into a run that never terminates, which CI reports as a timeout
|
|
40
|
+
# with no failing example to point at.
|
|
41
|
+
#
|
|
42
|
+
# The kill matters for the examples that follow: Ruby releases a Mutex held by
|
|
43
|
+
# a killed thread, so one stuck worker cannot wedge every later `configure`.
|
|
44
|
+
# Kill is asynchronous, so wait briefly for it to land before failing —
|
|
45
|
+
# otherwise the mutex is still held when the next example starts.
|
|
46
|
+
def join_without_deadlock(*threads)
|
|
47
|
+
threads.flatten.compact.each do |thread|
|
|
48
|
+
next if thread.join(deadlock_backstop_seconds)
|
|
49
|
+
|
|
50
|
+
thread.kill
|
|
51
|
+
thread.join(1)
|
|
52
|
+
raise "a worker thread did not finish within #{deadlock_backstop_seconds}s; suspect a deadlock"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
28
56
|
context "global scope extension" do
|
|
29
57
|
it "cannot be called in the global scope without requiring the core_ext/kernel" do
|
|
30
58
|
script = "require 'retriable'; begin; retriable {}; rescue NoMethodError; exit 0; end; exit 1"
|
|
@@ -105,6 +133,19 @@ describe Retriable do
|
|
|
105
133
|
expect(@tries).to eq(3)
|
|
106
134
|
end
|
|
107
135
|
|
|
136
|
+
it "re-raises StopIteration after the configured tries" do
|
|
137
|
+
failure = StopIteration.new("end of input")
|
|
138
|
+
|
|
139
|
+
expect do
|
|
140
|
+
described_class.retriable(tries: 2) do
|
|
141
|
+
increment_tries
|
|
142
|
+
raise failure
|
|
143
|
+
end
|
|
144
|
+
end.to(raise_error { |error| expect(error).to equal(failure) })
|
|
145
|
+
|
|
146
|
+
expect(@tries).to eq(2)
|
|
147
|
+
end
|
|
148
|
+
|
|
108
149
|
it "makes only 1 try when exception raised is not descendent of StandardError" do
|
|
109
150
|
expect do
|
|
110
151
|
described_class.retriable { increment_tries_with_exception(NonStandardError) }
|
|
@@ -768,6 +809,242 @@ describe Retriable do
|
|
|
768
809
|
it "raises NoMethodError on invalid configuration" do
|
|
769
810
|
expect { described_class.configure { |c| c.does_not_exist = 123 } }.to raise_error(NoMethodError)
|
|
770
811
|
end
|
|
812
|
+
|
|
813
|
+
it "returns the configure block's result" do
|
|
814
|
+
result = described_class.configure do |c|
|
|
815
|
+
c.tries = 7
|
|
816
|
+
:configured
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
expect(result).to eq(:configured)
|
|
820
|
+
expect(described_class.config.tries).to eq(7)
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
context "#configure thread safety (copy-on-write)" do
|
|
825
|
+
it "eagerly initializes @config at load time, before any configure/config call" do
|
|
826
|
+
script = "require 'retriable'; " \
|
|
827
|
+
"exit(Retriable.instance_variable_get(:@config).is_a?(Retriable::Config) ? 0 : 1)"
|
|
828
|
+
expect(system(RbConfig.ruby, "-Ilib", "-e", script)).to be(true)
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
it "publishes a new Config object on configure instead of mutating in place" do
|
|
832
|
+
before = described_class.config
|
|
833
|
+
described_class.configure { |c| c.tries = 7 }
|
|
834
|
+
after = described_class.config
|
|
835
|
+
|
|
836
|
+
expect(after).not_to equal(before)
|
|
837
|
+
expect(after.tries).to eq(7)
|
|
838
|
+
expect(before.tries).not_to eq(7)
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
it "keeps an already-captured snapshot stable across later configures" do
|
|
842
|
+
described_class.configure { |c| c.contexts[:sql] = { tries: 1 } }
|
|
843
|
+
snapshot = described_class.config
|
|
844
|
+
|
|
845
|
+
described_class.configure { |c| c.contexts[:http] = { tries: 2 } }
|
|
846
|
+
described_class.configure { |c| c.contexts[:sql][:tries] = 99 }
|
|
847
|
+
|
|
848
|
+
expect(snapshot.contexts).to eq(sql: { tries: 1 })
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
it "publishes a frozen snapshot, so direct mutation fails loudly" do
|
|
852
|
+
described_class.configure { |c| c.contexts[:api] = { tries: 1 } }
|
|
853
|
+
|
|
854
|
+
expect { described_class.config.tries = 99 }.to raise_error(FrozenError)
|
|
855
|
+
expect { described_class.config.contexts[:api][:tries] = 99 }.to raise_error(FrozenError)
|
|
856
|
+
expect(described_class.config.contexts[:api]).to eq(tries: 1)
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
it "still hands the configure block a mutable candidate" do
|
|
860
|
+
expect do
|
|
861
|
+
described_class.configure do |c|
|
|
862
|
+
c.contexts[:api] = { tries: 1 }
|
|
863
|
+
c.contexts[:api][:tries] = 2
|
|
864
|
+
c.on = [StandardError]
|
|
865
|
+
c.on << ArgumentError
|
|
866
|
+
end
|
|
867
|
+
end.not_to raise_error
|
|
868
|
+
|
|
869
|
+
expect(described_class.config.contexts[:api]).to eq(tries: 2)
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
it "does not freeze collections the caller still owns" do
|
|
873
|
+
caller_owned = [StandardError]
|
|
874
|
+
|
|
875
|
+
described_class.configure { |c| c.on = caller_owned }
|
|
876
|
+
|
|
877
|
+
expect(caller_owned).not_to be_frozen
|
|
878
|
+
expect(described_class.config.on).to be_frozen
|
|
879
|
+
expect(described_class.config.on).not_to equal(caller_owned)
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
it "owns and freezes a copy of a mutable Hash default" do
|
|
883
|
+
fallback = []
|
|
884
|
+
contexts = Hash.new(fallback)
|
|
885
|
+
contexts[:api] = { tries: 1 }
|
|
886
|
+
|
|
887
|
+
described_class.configure { |c| c.contexts = contexts }
|
|
888
|
+
published_default = described_class.config.contexts.default
|
|
889
|
+
|
|
890
|
+
expect(published_default).not_to equal(fallback)
|
|
891
|
+
expect(published_default).to be_frozen
|
|
892
|
+
expect(fallback).not_to be_frozen
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
it "does not let a published default leak mutations into the caller's object" do
|
|
896
|
+
fallback = []
|
|
897
|
+
described_class.configure { |c| c.contexts = Hash.new(fallback) }
|
|
898
|
+
|
|
899
|
+
expect { described_class.config.contexts[:absent] << :leaked }.to raise_error(FrozenError)
|
|
900
|
+
expect(fallback).to be_empty
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
it "does not drop updates when configured concurrently from many threads" do
|
|
904
|
+
keys = (0...50).map { |i| :"ctx_#{i}" }
|
|
905
|
+
release = Queue.new
|
|
906
|
+
|
|
907
|
+
threads = keys.map do |key|
|
|
908
|
+
Thread.new do
|
|
909
|
+
release.pop
|
|
910
|
+
described_class.configure { |c| c.contexts[key] = { tries: 1 } }
|
|
911
|
+
end
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
keys.size.times { release << true }
|
|
915
|
+
join_without_deadlock(threads)
|
|
916
|
+
|
|
917
|
+
expect(described_class.config.contexts.keys).to match_array(keys)
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
it "lets nested configure calls join the outer transaction" do
|
|
921
|
+
inner_candidate = nil
|
|
922
|
+
|
|
923
|
+
result = described_class.configure do |outer|
|
|
924
|
+
outer.tries = 7
|
|
925
|
+
nested_result = described_class.configure do |inner|
|
|
926
|
+
inner_candidate = inner
|
|
927
|
+
inner.base_interval = 1
|
|
928
|
+
:nested_result
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
expect(nested_result).to eq(:nested_result)
|
|
932
|
+
expect(inner_candidate).to equal(outer)
|
|
933
|
+
:outer_result
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
expect(result).to eq(:outer_result)
|
|
937
|
+
expect(described_class.config.tries).to eq(7)
|
|
938
|
+
expect(described_class.config.base_interval).to eq(1)
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
it "rolls back nested changes when the outer configure raises" do
|
|
942
|
+
described_class.configure { |c| c.tries = 4 }
|
|
943
|
+
|
|
944
|
+
expect do
|
|
945
|
+
described_class.configure do |outer|
|
|
946
|
+
outer.tries = 7
|
|
947
|
+
described_class.configure { |inner| inner.base_interval = 1 }
|
|
948
|
+
raise "outer failed"
|
|
949
|
+
end
|
|
950
|
+
end.to raise_error(RuntimeError, "outer failed")
|
|
951
|
+
|
|
952
|
+
expect(described_class.config.tries).to eq(4)
|
|
953
|
+
expect(described_class.config.base_interval).to eq(0.5)
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
it "lets configure calls from a fiber join the outer transaction" do
|
|
957
|
+
described_class.configure do |outer|
|
|
958
|
+
fiber_candidate = Fiber.new do
|
|
959
|
+
described_class.configure do |inner|
|
|
960
|
+
inner.tries = 9
|
|
961
|
+
inner
|
|
962
|
+
end
|
|
963
|
+
end.resume
|
|
964
|
+
|
|
965
|
+
expect(fiber_candidate).to equal(outer)
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
expect(described_class.config.tries).to eq(9)
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
it "exposes the in-progress config to the configuring thread" do
|
|
972
|
+
described_class.configure do |c|
|
|
973
|
+
c.tries = 7
|
|
974
|
+
expect(described_class.config.tries).to eq(7)
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
expect(described_class.config.tries).to eq(7)
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
it "uses the in-progress config for retriable" do
|
|
981
|
+
attempts = 0
|
|
982
|
+
|
|
983
|
+
described_class.configure do |c|
|
|
984
|
+
c.tries = 1
|
|
985
|
+
c.sleep_disabled = true
|
|
986
|
+
|
|
987
|
+
expect do
|
|
988
|
+
described_class.retriable do
|
|
989
|
+
attempts += 1
|
|
990
|
+
raise StandardError
|
|
991
|
+
end
|
|
992
|
+
end.to raise_error(StandardError)
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
expect(attempts).to eq(1)
|
|
996
|
+
end
|
|
997
|
+
|
|
998
|
+
it "uses candidate-only contexts for with_context" do
|
|
999
|
+
described_class.configure do |c|
|
|
1000
|
+
c.contexts[:candidate] = { tries: 1 }
|
|
1001
|
+
|
|
1002
|
+
expect(described_class.with_context(:candidate) { :found }).to eq(:found)
|
|
1003
|
+
end
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
it "shares the in-progress config with fibers in the configuring thread" do
|
|
1007
|
+
described_class.configure do |c|
|
|
1008
|
+
c.tries = 7
|
|
1009
|
+
|
|
1010
|
+
expect(Fiber.new { described_class.config.tries }.resume).to eq(7)
|
|
1011
|
+
end
|
|
1012
|
+
end
|
|
1013
|
+
|
|
1014
|
+
it "does not block readers while configure is in progress" do
|
|
1015
|
+
published_tries = described_class.config.tries
|
|
1016
|
+
published_base_interval = described_class.config.base_interval
|
|
1017
|
+
configuring = Queue.new
|
|
1018
|
+
release = Queue.new
|
|
1019
|
+
writer = Thread.new do
|
|
1020
|
+
described_class.configure do |c|
|
|
1021
|
+
c.tries = published_tries + 1
|
|
1022
|
+
described_class.configure { |inner| inner.base_interval = published_base_interval + 1 }
|
|
1023
|
+
configuring << true
|
|
1024
|
+
release.pop
|
|
1025
|
+
end
|
|
1026
|
+
end
|
|
1027
|
+
reader = nil
|
|
1028
|
+
|
|
1029
|
+
begin
|
|
1030
|
+
configuring.pop
|
|
1031
|
+
reader = Thread.new do
|
|
1032
|
+
snapshot = described_class.config
|
|
1033
|
+
[snapshot.tries, snapshot.base_interval]
|
|
1034
|
+
end
|
|
1035
|
+
# The timeout is a deadlock backstop, not the assertion. A reader that
|
|
1036
|
+
# is not blocked returns immediately, so this budget is only ever
|
|
1037
|
+
# approached if #config starts waiting on the in-progress #configure.
|
|
1038
|
+
expect(reader.join(deadlock_backstop_seconds)).to be(reader),
|
|
1039
|
+
"reader blocked while configure was in progress"
|
|
1040
|
+
# Thread#value re-raises anything the reader raised, so a broken reader
|
|
1041
|
+
# fails loudly instead of hanging on an empty queue.
|
|
1042
|
+
expect(reader.value).to eq([published_tries, published_base_interval])
|
|
1043
|
+
ensure
|
|
1044
|
+
release << true
|
|
1045
|
+
join_without_deadlock(writer, reader)
|
|
1046
|
+
end
|
|
1047
|
+
end
|
|
771
1048
|
end
|
|
772
1049
|
|
|
773
1050
|
context "#retriable tries/intervals precedence" do
|
|
@@ -1120,7 +1397,7 @@ describe Retriable do
|
|
|
1120
1397
|
|
|
1121
1398
|
2.times { ready.pop }
|
|
1122
1399
|
2.times { proceed << true }
|
|
1123
|
-
threads
|
|
1400
|
+
join_without_deadlock(threads)
|
|
1124
1401
|
|
|
1125
1402
|
expect(results).to eq(1 => 1, 2 => 2)
|
|
1126
1403
|
end
|
|
@@ -1151,7 +1428,7 @@ describe Retriable do
|
|
|
1151
1428
|
sibling_done << true
|
|
1152
1429
|
end
|
|
1153
1430
|
|
|
1154
|
-
|
|
1431
|
+
join_without_deadlock(setter, sibling)
|
|
1155
1432
|
expect(sibling_tries).to eq(3)
|
|
1156
1433
|
end
|
|
1157
1434
|
|
|
@@ -1159,7 +1436,7 @@ describe Retriable do
|
|
|
1159
1436
|
child_tries = nil
|
|
1160
1437
|
|
|
1161
1438
|
described_class.with_override(tries: 1) do
|
|
1162
|
-
Thread.new do
|
|
1439
|
+
child = Thread.new do
|
|
1163
1440
|
tries = 0
|
|
1164
1441
|
begin
|
|
1165
1442
|
described_class.retriable(tries: 3) do
|
|
@@ -1169,7 +1446,8 @@ describe Retriable do
|
|
|
1169
1446
|
rescue StandardError
|
|
1170
1447
|
child_tries = tries
|
|
1171
1448
|
end
|
|
1172
|
-
end
|
|
1449
|
+
end
|
|
1450
|
+
join_without_deadlock(child)
|
|
1173
1451
|
end
|
|
1174
1452
|
|
|
1175
1453
|
expect(child_tries).to eq(3)
|
|
@@ -1178,7 +1456,7 @@ describe Retriable do
|
|
|
1178
1456
|
it "shares the active override with fibers in the same thread" do
|
|
1179
1457
|
fiber_tries = nil
|
|
1180
1458
|
|
|
1181
|
-
Thread.new do
|
|
1459
|
+
worker = Thread.new do
|
|
1182
1460
|
described_class.with_override(tries: 1) do
|
|
1183
1461
|
Fiber.new do
|
|
1184
1462
|
tries = 0
|
|
@@ -1192,7 +1470,8 @@ describe Retriable do
|
|
|
1192
1470
|
end
|
|
1193
1471
|
end.resume
|
|
1194
1472
|
end
|
|
1195
|
-
end
|
|
1473
|
+
end
|
|
1474
|
+
join_without_deadlock(worker)
|
|
1196
1475
|
|
|
1197
1476
|
expect(fiber_tries).to eq(1)
|
|
1198
1477
|
end
|
|
@@ -1201,7 +1480,7 @@ describe Retriable do
|
|
|
1201
1480
|
other_thread_tries = nil
|
|
1202
1481
|
|
|
1203
1482
|
described_class.with_override(tries: 1) do
|
|
1204
|
-
Thread.new do
|
|
1483
|
+
other = Thread.new do
|
|
1205
1484
|
tries = 0
|
|
1206
1485
|
begin
|
|
1207
1486
|
described_class.retriable(tries: 3) do
|
|
@@ -1211,7 +1490,8 @@ describe Retriable do
|
|
|
1211
1490
|
rescue StandardError
|
|
1212
1491
|
other_thread_tries = tries
|
|
1213
1492
|
end
|
|
1214
|
-
end
|
|
1493
|
+
end
|
|
1494
|
+
join_without_deadlock(other)
|
|
1215
1495
|
end
|
|
1216
1496
|
|
|
1217
1497
|
expect(other_thread_tries).to eq(3)
|
|
@@ -1325,5 +1605,37 @@ describe Retriable do
|
|
|
1325
1605
|
|
|
1326
1606
|
expect(callback_called).to be(true)
|
|
1327
1607
|
end
|
|
1608
|
+
|
|
1609
|
+
it "resolves the context against a single config snapshot" do
|
|
1610
|
+
with_ctx = Retriable::Config.new(sleep_disabled: true, contexts: { api: { tries: 1 } })
|
|
1611
|
+
without_ctx = Retriable::Config.new(sleep_disabled: true)
|
|
1612
|
+
|
|
1613
|
+
# Simulate a concurrent #configure publishing a new config between
|
|
1614
|
+
# with_context's existence check and its option resolution: the first
|
|
1615
|
+
# config read sees the context, later reads do not. with_context must
|
|
1616
|
+
# read config once so the context options are never silently dropped.
|
|
1617
|
+
allow(described_class).to receive(:config).and_return(with_ctx, without_ctx)
|
|
1618
|
+
|
|
1619
|
+
expect { described_class.with_context(:api) { increment_tries_with_exception } }
|
|
1620
|
+
.to raise_error(StandardError)
|
|
1621
|
+
|
|
1622
|
+
expect(@tries).to eq(1)
|
|
1623
|
+
end
|
|
1624
|
+
|
|
1625
|
+
it "resolves global options against the same snapshot used for the context" do
|
|
1626
|
+
special_error = Class.new(StandardError)
|
|
1627
|
+
with_ctx = Retriable::Config.new(sleep_disabled: true, on: [special_error], contexts: { api: { tries: 2 } })
|
|
1628
|
+
swapped = Retriable::Config.new(sleep_disabled: true, on: [ArgumentError])
|
|
1629
|
+
|
|
1630
|
+
# A concurrent #configure swaps the global config (here, the retriable
|
|
1631
|
+
# `on` list) after with_context has captured its snapshot. The whole
|
|
1632
|
+
# context execution must use the captured snapshot, not the swapped one.
|
|
1633
|
+
allow(described_class).to receive(:config).and_return(with_ctx, swapped)
|
|
1634
|
+
|
|
1635
|
+
expect { described_class.with_context(:api) { increment_tries_with_exception(special_error) } }
|
|
1636
|
+
.to raise_error(special_error)
|
|
1637
|
+
|
|
1638
|
+
expect(@tries).to eq(2)
|
|
1639
|
+
end
|
|
1328
1640
|
end
|
|
1329
1641
|
end
|
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:
|
|
4
|
+
version: 5.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jack Chu
|
|
@@ -24,14 +24,21 @@ files:
|
|
|
24
24
|
- ".hound.yml"
|
|
25
25
|
- ".rspec"
|
|
26
26
|
- ".rubocop.yml"
|
|
27
|
+
- AGENTS.md
|
|
27
28
|
- CHANGELOG.md
|
|
28
29
|
- CODE_OF_CONDUCT.md
|
|
29
30
|
- Gemfile
|
|
30
31
|
- LICENSE
|
|
31
32
|
- README.md
|
|
32
33
|
- Rakefile
|
|
34
|
+
- benchmark/config_publication.rb
|
|
33
35
|
- bin/console
|
|
34
36
|
- bin/setup
|
|
37
|
+
- docs/adr/0001-copy-on-write-config-publication.md
|
|
38
|
+
- docs/agents/domain.md
|
|
39
|
+
- docs/agents/issue-tracker.md
|
|
40
|
+
- docs/agents/triage-labels.md
|
|
41
|
+
- docs/migration.md
|
|
35
42
|
- docs/testing.md
|
|
36
43
|
- lib/retriable.rb
|
|
37
44
|
- lib/retriable/config.rb
|
|
@@ -65,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
65
72
|
- !ruby/object:Gem::Version
|
|
66
73
|
version: '0'
|
|
67
74
|
requirements: []
|
|
68
|
-
rubygems_version:
|
|
75
|
+
rubygems_version: 4.0.16
|
|
69
76
|
specification_version: 4
|
|
70
77
|
summary: Retriable is a simple DSL to retry failed code blocks with randomized exponential
|
|
71
78
|
backoff
|