retriable 3.2.1 → 3.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8df6a3c9b00afd6e5f3e307fdb07d3cedae54c43b4f3537998ebae78dd17ab29
4
- data.tar.gz: 46023d8f2d1731ea9a3c66c61839b04aa49b18d3def72d1ea1fc36d9c9e7a1c3
3
+ metadata.gz: c0e8971cb9e6833e2795f2537ee69950714f7eb85989b497d15fad7f2e2552b6
4
+ data.tar.gz: 222c0e1664bf327dfb22cf161c42a3f3bec07a54ea3787af24a86ebd86abcd50
5
5
  SHA512:
6
- metadata.gz: cd71b6a0a42cb2c6482409bcbc42d57504c5f601aa33e80d8a032dd97e31df582234bbb8959c1cf70eb92b7cf67c650497820be5b219053322317cbb7d748efa
7
- data.tar.gz: 617c7c1785c0b7691655615484251b8b591afa43d211d0ea4ef0d94def9747e84814dbdbe9095c388c112a9957c6050d17ba4c3044bb2e2f299bf4d892f6fd04
6
+ metadata.gz: b768d200c1dd25f9a5218096e5bba26b0ee815833fb87ed6a004d90d7f94208f4520df0fdb1172fc6fdea55209cf1eeb15d6efc39a52bd2f32c0222b5aecca5a
7
+ data.tar.gz: 0a095f55fcdbe49adf71bb06682c062ed98a4328ef0e4648926e6782b93152c337cd78b2f30a2bf5a79affc37c31c3f2b39730600c3c63b5dcb41d5c09b59380
@@ -2,9 +2,9 @@ name: CI
2
2
 
3
3
  on:
4
4
  push:
5
- branches: [master]
5
+ branches: [main]
6
6
  pull_request:
7
- branches: [master]
7
+ branches: [main]
8
8
  types: [opened, synchronize, reopened]
9
9
 
10
10
  jobs:
data/.gitignore CHANGED
@@ -4,6 +4,8 @@
4
4
  /_yardoc/
5
5
  /coverage/
6
6
  /doc/
7
+ /docs/plans/
8
+ /.worktrees/
7
9
  /pkg/
8
10
  /spec/reports/
9
11
  /tmp/
data/.rubocop.yml CHANGED
@@ -1,3 +1,7 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ TargetRubyVersion: 2.3
4
+
1
5
  Style/StringLiterals:
2
6
  EnforcedStyle: double_quotes
3
7
 
@@ -13,12 +17,6 @@ Style/TrailingCommaInArguments:
13
17
  Lint/InheritException:
14
18
  Enabled: false
15
19
 
16
- Layout/FirstArrayElementIndentation:
17
- Enabled: false
18
-
19
- Layout/FirstHashElementIndentation:
20
- Enabled: false
21
-
22
20
  Style/NegatedIf:
23
21
  Enabled: false
24
22
 
data/CHANGELOG.md CHANGED
@@ -1,7 +1,13 @@
1
1
  # HEAD
2
2
 
3
+ ## 3.3.0
4
+
5
+ - Refactor `Retriable.retriable` internals into focused private helpers to improve readability while preserving behavior.
6
+ - Modernize `.rubocop.yml` with explicit modern defaults to enable new cops while preserving existing project style policies.
7
+
3
8
  ## 3.2.1
4
- - Remove executables from gemspec as it was poluting the path for some users. Thanks @hsbt.
9
+
10
+ - Remove executables from gemspec as it was polluting the path for some users. Thanks @hsbt.
5
11
 
6
12
  ## 3.2.0
7
13
 
@@ -2,12 +2,12 @@
2
2
 
3
3
  module Retriable
4
4
  class ExponentialBackoff
5
- ATTRIBUTES = [
6
- :tries,
7
- :base_interval,
8
- :multiplier,
9
- :max_interval,
10
- :rand_factor,
5
+ ATTRIBUTES = %i[
6
+ tries
7
+ base_interval
8
+ multiplier
9
+ max_interval
10
+ rand_factor
11
11
  ].freeze
12
12
 
13
13
  attr_accessor(*ATTRIBUTES)
@@ -21,6 +21,7 @@ module Retriable
21
21
 
22
22
  opts.each do |k, v|
23
23
  raise ArgumentError, "#{k} is not a valid option" if !ATTRIBUTES.include?(k)
24
+
24
25
  instance_variable_set(:"@#{k}", v)
25
26
  end
26
27
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Retriable
4
- VERSION = "3.2.1"
4
+ VERSION = "3.3.0"
5
5
  end
data/lib/retriable.rb CHANGED
@@ -21,61 +21,113 @@ module Retriable
21
21
  raise ArgumentError, "#{context_key} not found in Retriable.config.contexts. Available contexts: #{config.contexts.keys}"
22
22
  end
23
23
 
24
- retriable(config.contexts[context_key].merge(options), &block) if block
24
+ return unless block_given?
25
+
26
+ retriable(config.contexts[context_key].merge(options), &block)
25
27
  end
26
28
 
27
- def retriable(opts = {})
29
+ def retriable(opts = {}, &block)
28
30
  local_config = opts.empty? ? config : Config.new(config.to_h.merge(opts))
29
31
 
30
- tries = local_config.tries
31
- base_interval = local_config.base_interval
32
- max_interval = local_config.max_interval
33
- rand_factor = local_config.rand_factor
34
- multiplier = local_config.multiplier
35
- max_elapsed_time = local_config.max_elapsed_time
36
- intervals = local_config.intervals
37
- timeout = local_config.timeout
38
- on = local_config.on
39
- on_retry = local_config.on_retry
40
- sleep_disabled = local_config.sleep_disabled
32
+ tries = local_config.tries
33
+ intervals = build_intervals(local_config, tries)
34
+ timeout = local_config.timeout
35
+ on = local_config.on
36
+ on_retry = local_config.on_retry
37
+ sleep_disabled = local_config.sleep_disabled
38
+ max_elapsed_time = local_config.max_elapsed_time
41
39
 
42
40
  exception_list = on.is_a?(Hash) ? on.keys : on
41
+ exception_list = [*exception_list]
43
42
  start_time = Time.now
44
43
  elapsed_time = -> { Time.now - start_time }
45
44
 
46
- if !intervals
47
- intervals = ExponentialBackoff.new(
48
- tries: tries - 1,
49
- base_interval: base_interval,
50
- multiplier: multiplier,
51
- max_interval: max_interval,
52
- rand_factor: rand_factor
53
- ).intervals
54
- end
55
-
56
45
  tries = intervals.size + 1
57
46
 
47
+ execute_tries(
48
+ tries: tries, intervals: intervals, timeout: timeout,
49
+ exception_list: exception_list, on: on, on_retry: on_retry,
50
+ elapsed_time: elapsed_time, max_elapsed_time: max_elapsed_time,
51
+ sleep_disabled: sleep_disabled, &block
52
+ )
53
+ end
54
+
55
+ def execute_tries( # rubocop:disable Metrics/ParameterLists
56
+ tries:, intervals:, timeout:, exception_list:,
57
+ on:, on_retry:, elapsed_time:, max_elapsed_time:, sleep_disabled:, &block
58
+ )
58
59
  tries.times do |index|
59
60
  try = index + 1
60
61
 
61
62
  begin
62
- return Timeout.timeout(timeout) { return yield(try) } if timeout
63
-
64
- return yield(try)
65
- rescue *[*exception_list] => exception
66
- if on.is_a?(Hash)
67
- raise unless exception_list.any? do |e|
68
- exception.is_a?(e) && ([*on[e]].empty? || [*on[e]].any? { |pattern| exception.message =~ pattern })
69
- end
70
- end
63
+ return call_with_timeout(timeout, try, &block)
64
+ rescue *exception_list => e
65
+ raise unless retriable_exception?(e, on, exception_list)
71
66
 
72
67
  interval = intervals[index]
73
- on_retry.call(exception, try, elapsed_time.call, interval) if on_retry
68
+ call_on_retry(on_retry, e, try, elapsed_time.call, interval)
74
69
 
75
- raise if try >= tries || (elapsed_time.call + interval) > max_elapsed_time
70
+ raise unless can_retry?(try, tries, elapsed_time.call, interval, max_elapsed_time)
76
71
 
77
72
  sleep interval if sleep_disabled != true
78
73
  end
79
74
  end
80
75
  end
76
+
77
+ def build_intervals(local_config, tries)
78
+ return local_config.intervals if local_config.intervals
79
+
80
+ ExponentialBackoff.new(
81
+ tries: tries - 1,
82
+ base_interval: local_config.base_interval,
83
+ multiplier: local_config.multiplier,
84
+ max_interval: local_config.max_interval,
85
+ rand_factor: local_config.rand_factor,
86
+ ).intervals
87
+ end
88
+
89
+ def call_with_timeout(timeout, try)
90
+ return Timeout.timeout(timeout) { yield(try) } if timeout
91
+
92
+ yield(try)
93
+ end
94
+
95
+ def call_on_retry(on_retry, exception, try, elapsed_time, interval)
96
+ return unless on_retry
97
+
98
+ on_retry.call(exception, try, elapsed_time, interval)
99
+ end
100
+
101
+ def can_retry?(try, tries, elapsed_time, interval, max_elapsed_time)
102
+ try < tries && (elapsed_time + interval) <= max_elapsed_time
103
+ end
104
+
105
+ # When `on` is a Hash, we need to verify the exception matches a pattern.
106
+ # For any non-Hash `on` value (e.g., Array of classes, single Exception class,
107
+ # or Module), the `rescue *exception_list` clause already guarantees the
108
+ # exception is retriable, so we return true unconditionally.
109
+ def retriable_exception?(exception, on, exception_list)
110
+ return false if on.is_a?(Hash) && !hash_exception_match?(exception, on, exception_list)
111
+
112
+ true
113
+ end
114
+
115
+ def hash_exception_match?(exception, on, exception_list)
116
+ exception_list.any? do |error_class|
117
+ next false unless exception.is_a?(error_class)
118
+
119
+ patterns = [*on[error_class]]
120
+ patterns.empty? || patterns.any? { |pattern| exception.message =~ pattern }
121
+ end
122
+ end
123
+
124
+ private_class_method(
125
+ :execute_tries,
126
+ :build_intervals,
127
+ :call_with_timeout,
128
+ :call_on_retry,
129
+ :can_retry?,
130
+ :retriable_exception?,
131
+ :hash_exception_match?,
132
+ )
81
133
  end
data/retriable.gemspec CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
- lib = File.expand_path("../lib", __FILE__)
2
+
3
+ lib = File.expand_path("lib", __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require "retriable/version"
5
6
 
@@ -9,7 +10,9 @@ Gem::Specification.new do |spec|
9
10
  spec.authors = ["Jack Chu"]
10
11
  spec.email = ["jack@jackchu.com"]
11
12
  spec.summary = "Retriable is a simple DSL to retry failed code blocks with randomized exponential backoff"
12
- spec.description = "Retriable is a simple DSL to retry failed code blocks with randomized exponential backoff. This is especially useful when interacting external api/services or file system calls."
13
+ spec.description = "Retriable is a simple DSL to retry failed code blocks with randomized " \
14
+ "exponential backoff. This is especially useful when interacting with external " \
15
+ "APIs/services or file system calls."
13
16
  spec.homepage = "https://github.com/kamui/retriable"
14
17
  spec.license = "MIT"
15
18
 
@@ -96,6 +96,28 @@ describe Retriable do
96
96
  expect(@tries).to eq(10)
97
97
  end
98
98
 
99
+ it "does not call on_retry when explicitly set to false" do
100
+ callback_called = false
101
+ original_on_retry = described_class.config.on_retry
102
+
103
+ begin
104
+ described_class.configure do |c|
105
+ c.on_retry = proc { |_exception, _try, _elapsed_time, _next_interval| callback_called = true }
106
+ end
107
+
108
+ expect do
109
+ described_class.retriable(on_retry: false, tries: 3) { increment_tries_with_exception }
110
+ end.to raise_error(StandardError)
111
+
112
+ expect(@tries).to eq(3)
113
+ expect(callback_called).to be(false)
114
+ ensure
115
+ described_class.configure do |c|
116
+ c.on_retry = original_on_retry
117
+ end
118
+ end
119
+ end
120
+
99
121
  context "with rand_factor 0.0 and an on_retry handler" do
100
122
  let(:tries) { 6 }
101
123
  let(:no_rand_timetable) { { 1 => 0.5, 2 => 0.75, 3 => 1.125 } }
@@ -255,6 +277,17 @@ describe Retriable do
255
277
  end
256
278
 
257
279
  context "#configure" do
280
+ it "exposes only the intended public API" do
281
+ public_api_methods = %i[
282
+ retriable
283
+ with_context
284
+ configure
285
+ config
286
+ ]
287
+
288
+ expect(described_class.singleton_methods(false)).to match_array(public_api_methods)
289
+ end
290
+
258
291
  it "raises NoMethodError on invalid configuration" do
259
292
  expect { described_class.configure { |c| c.does_not_exist = 123 } }.to raise_error(NoMethodError)
260
293
  end
@@ -275,6 +308,22 @@ describe Retriable do
275
308
  expect(@tries).to eq(1)
276
309
  end
277
310
 
311
+ it "returns nil when called without a block" do
312
+ expect(described_class.with_context(:sql)).to be_nil
313
+ expect(@tries).to eq(0)
314
+ end
315
+
316
+ it "passes try count through to the context block" do
317
+ seen_tries = []
318
+
319
+ described_class.with_context(:api) do |try|
320
+ seen_tries << try
321
+ raise StandardError if try < 3
322
+ end
323
+
324
+ expect(seen_tries).to eq([1, 2, 3])
325
+ end
326
+
278
327
  it "respects the context options" do
279
328
  expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
280
329
  expect(@tries).to eq(api_tries)
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.2.1
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jack Chu
@@ -52,7 +52,7 @@ dependencies:
52
52
  - !ruby/object:Gem::Version
53
53
  version: '3.1'
54
54
  description: Retriable is a simple DSL to retry failed code blocks with randomized
55
- exponential backoff. This is especially useful when interacting external api/services
55
+ exponential backoff. This is especially useful when interacting with external APIs/services
56
56
  or file system calls.
57
57
  email:
58
58
  - jack@jackchu.com