@@ -43,23 +43,159 @@ def perform(arg, kwarg:)
4343 end
4444 end
4545
46- describe ".__should_retry?" do
47- it "returns true if attempts are less than max" do
48- expect ( task_class . __should_retry? ( 4 ) ) . to be ( true )
46+ describe ".__next_retry_in" do
47+ it "returns the next retry interval with exponential backoff" do
48+ expect ( task_class . __next_retry_in ( 0 , nil ) ) . to be_between ( 1 , 5 )
49+ expect ( task_class . __next_retry_in ( 1 , nil ) ) . to be_between ( 1 , 10 )
50+ expect ( task_class . __next_retry_in ( 2 , nil ) ) . to be_between ( 1 , 20 )
51+ expect ( task_class . __next_retry_in ( 3 , nil ) ) . to be_between ( 1 , 40 )
52+ expect ( task_class . __next_retry_in ( 4 , nil ) ) . to be_between ( 1 , 80 )
4953 end
5054
51- it "returns false if attempts are equal to max" do
52- expect ( task_class . __should_retry? ( 5 ) ) . to be ( false )
55+ it "returns nil when attempts exceed max" do
56+ expect ( task_class . __next_retry_in ( 5 , nil ) ) . to be_between ( 1 , 160 )
57+ expect ( task_class . __next_retry_in ( 6 , nil ) ) . to be_nil
5358 end
5459 end
5560
56- describe ".__next_retry_in" do
57- it "returns the next retry interval with exponential backoff" do
58- expect ( task_class . __next_retry_in ( 0 ) ) . to be_between ( 1 , 5 )
59- expect ( task_class . __next_retry_in ( 1 ) ) . to be_between ( 1 , 10 )
60- expect ( task_class . __next_retry_in ( 2 ) ) . to be_between ( 1 , 20 )
61- expect ( task_class . __next_retry_in ( 3 ) ) . to be_between ( 1 , 40 )
62- expect ( task_class . __next_retry_in ( 4 ) ) . to be_between ( 1 , 80 )
61+ describe ".max_retries" do
62+ context "with custom max" do
63+ before { task_class . max_retries ( 3 ) }
64+
65+ it "retries up to custom max" do
66+ expect ( task_class . __next_retry_in ( 3 , StandardError . new ) ) . to be_a ( Numeric )
67+ end
68+
69+ it "stops after custom max" do
70+ expect ( task_class . __next_retry_in ( 4 , StandardError . new ) ) . to be_nil
71+ end
72+
73+ it "means the task is executed up to 4 times total" do
74+ # attempt 1 = original, attempt 2-4 = retries
75+ expect ( task_class . __next_retry_in ( 1 , StandardError . new ) ) . to be_a ( Numeric )
76+ expect ( task_class . __next_retry_in ( 2 , StandardError . new ) ) . to be_a ( Numeric )
77+ expect ( task_class . __next_retry_in ( 3 , StandardError . new ) ) . to be_a ( Numeric )
78+ expect ( task_class . __next_retry_in ( 4 , StandardError . new ) ) . to be_nil
79+ end
80+ end
81+ end
82+
83+ describe ".retry_interval" do
84+ context "default behavior (no override)" do
85+ it "returns an interval when under MAX_ATTEMPTS" do
86+ interval = task_class . retry_interval ( StandardError . new , attempt : 1 )
87+ expect ( interval ) . to be_a ( Integer )
88+ expect ( interval ) . to be >= 1
89+ end
90+
91+ it "returns false when past MAX_ATTEMPTS" do
92+ expect ( task_class . retry_interval ( StandardError . new , attempt : 5 ) ) . to be_a ( Integer )
93+ expect ( task_class . retry_interval ( StandardError . new , attempt : 6 ) ) . to be ( false )
94+ end
95+
96+ it "respects max_retries setting" do
97+ task_class . max_retries ( 2 )
98+ expect ( task_class . retry_interval ( StandardError . new , attempt : 1 ) ) . to be_a ( Integer )
99+ expect ( task_class . retry_interval ( StandardError . new , attempt : 2 ) ) . to be_a ( Integer )
100+ expect ( task_class . retry_interval ( StandardError . new , attempt : 3 ) ) . to be ( false )
101+ end
102+ end
103+
104+ context "with override" do
105+ let ( :temporary_error ) { Class . new ( StandardError ) }
106+ let ( :fatal_error ) { Class . new ( StandardError ) }
107+
108+ before do
109+ tmp_err = temporary_error
110+ fat_err = fatal_error
111+
112+ task_class . define_singleton_method ( :retry_interval ) do |exception , attempt :|
113+ case exception
114+ when tmp_err
115+ 10
116+ when fat_err
117+ false
118+ else
119+ super ( exception , attempt : attempt )
120+ end
121+ end
122+ end
123+
124+ it "returns custom interval for matching exception" do
125+ expect ( task_class . retry_interval ( temporary_error . new , attempt : 1 ) ) . to eq ( 10 )
126+ end
127+
128+ it "returns false for non-retryable exception" do
129+ expect ( task_class . retry_interval ( fatal_error . new , attempt : 1 ) ) . to be ( false )
130+ end
131+
132+ it "falls back to default for unmatched exception" do
133+ interval = task_class . retry_interval ( StandardError . new , attempt : 1 )
134+ expect ( interval ) . to be_a ( Integer )
135+ expect ( interval ) . to be >= 1
136+ end
137+
138+ it "__next_retry_in returns interval for retryable" do
139+ expect ( task_class . __next_retry_in ( 1 , temporary_error . new ) ) . to eq ( 10 )
140+ end
141+
142+ it "__next_retry_in returns nil for non-retryable" do
143+ expect ( task_class . __next_retry_in ( 1 , fatal_error . new ) ) . to be_nil
144+ end
145+
146+ it "__next_retry_in uses default backoff for unmatched" do
147+ interval = task_class . __next_retry_in ( 1 , StandardError . new )
148+ expect ( interval ) . to be_between ( 1 , 10 )
149+ end
150+ end
151+
152+ context "with edge case return values" do
153+ let ( :logger ) { double ( warn : nil ) }
154+
155+ before do
156+ allow ( Rage ) . to receive ( :logger ) . and_return ( logger )
157+ end
158+
159+ it "accepts a Float return value" do
160+ task_class . define_singleton_method ( :retry_interval ) { |_exception , attempt :| 2.5 }
161+ expect ( task_class . __next_retry_in ( 1 , StandardError . new ) ) . to eq ( 2.5 )
162+ end
163+
164+ it "returns nil when retry_interval returns nil" do
165+ task_class . define_singleton_method ( :retry_interval ) { |_exception , attempt :| nil }
166+ expect ( task_class . __next_retry_in ( 1 , StandardError . new ) ) . to be_nil
167+ end
168+
169+ it "returns nil when retry_interval returns false" do
170+ task_class . define_singleton_method ( :retry_interval ) { |_exception , attempt :| false }
171+ expect ( task_class . __next_retry_in ( 1 , StandardError . new ) ) . to be_nil
172+ end
173+
174+ it "accepts zero as a valid interval" do
175+ task_class . define_singleton_method ( :retry_interval ) { |_exception , attempt :| 0 }
176+ expect ( task_class . __next_retry_in ( 1 , StandardError . new ) ) . to eq ( 0 )
177+ end
178+
179+ it "accepts a negative number as a Numeric" do
180+ task_class . define_singleton_method ( :retry_interval ) { |_exception , attempt :| -5 }
181+ expect ( task_class . __next_retry_in ( 1 , StandardError . new ) ) . to eq ( -5 )
182+ end
183+
184+ it "logs a warning and falls back to default backoff for String" do
185+ task_class . define_singleton_method ( :retry_interval ) { |_exception , attempt :| "invalid" }
186+ result = task_class . __next_retry_in ( 1 , StandardError . new )
187+ expect ( result ) . to be_a ( Numeric )
188+ expect ( result ) . to be >= 1
189+ expect ( logger ) . to have_received ( :warn ) . with ( /returned String, expected Numeric/ )
190+ end
191+
192+ it "logs a warning and falls back to default backoff for Array" do
193+ task_class . define_singleton_method ( :retry_interval ) { |_exception , attempt :| [ 10 ] }
194+ result = task_class . __next_retry_in ( 1 , StandardError . new )
195+ expect ( result ) . to be_a ( Numeric )
196+ expect ( result ) . to be >= 1
197+ expect ( logger ) . to have_received ( :warn ) . with ( /returned Array, expected Numeric/ )
198+ end
63199 end
64200 end
65201
@@ -145,8 +281,8 @@ def perform(arg, kwarg:)
145281 expect ( logger ) . to have_received ( :error ) . with ( "Deferred task failed with exception: StandardError (Something went wrong):\n line 1\n line 2" )
146282 end
147283
148- it "returns false " do
149- expect ( task . __perform ( context ) ) . to be ( false )
284+ it "returns the exception " do
285+ expect ( task . __perform ( context ) ) . to be ( error )
150286 end
151287
152288 context "with suppressed exception logging" do
0 commit comments