cmdx 1.21.0 → 2.0.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.
Files changed (195) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +118 -1
  3. data/README.md +37 -24
  4. data/lib/cmdx/.DS_Store +0 -0
  5. data/lib/cmdx/callbacks.rb +179 -0
  6. data/lib/cmdx/chain.rb +78 -175
  7. data/lib/cmdx/coercions/array.rb +19 -33
  8. data/lib/cmdx/coercions/big_decimal.rb +12 -29
  9. data/lib/cmdx/coercions/boolean.rb +25 -45
  10. data/lib/cmdx/coercions/coerce.rb +32 -0
  11. data/lib/cmdx/coercions/complex.rb +12 -27
  12. data/lib/cmdx/coercions/date.rb +29 -33
  13. data/lib/cmdx/coercions/date_time.rb +29 -33
  14. data/lib/cmdx/coercions/float.rb +8 -29
  15. data/lib/cmdx/coercions/hash.rb +17 -43
  16. data/lib/cmdx/coercions/integer.rb +8 -32
  17. data/lib/cmdx/coercions/rational.rb +12 -33
  18. data/lib/cmdx/coercions/string.rb +6 -24
  19. data/lib/cmdx/coercions/symbol.rb +12 -26
  20. data/lib/cmdx/coercions/time.rb +31 -35
  21. data/lib/cmdx/coercions.rb +174 -0
  22. data/lib/cmdx/configuration.rb +45 -237
  23. data/lib/cmdx/context.rb +264 -243
  24. data/lib/cmdx/deprecation.rb +67 -0
  25. data/lib/cmdx/deprecators/error.rb +22 -0
  26. data/lib/cmdx/deprecators/log.rb +22 -0
  27. data/lib/cmdx/deprecators/warn.rb +21 -0
  28. data/lib/cmdx/deprecators.rb +101 -0
  29. data/lib/cmdx/errors.rb +145 -79
  30. data/lib/cmdx/executors/fiber.rb +42 -0
  31. data/lib/cmdx/executors/thread.rb +36 -0
  32. data/lib/cmdx/executors.rb +95 -0
  33. data/lib/cmdx/fault.rb +85 -78
  34. data/lib/cmdx/i18n_proxy.rb +104 -0
  35. data/lib/cmdx/input.rb +294 -0
  36. data/lib/cmdx/inputs.rb +218 -0
  37. data/lib/cmdx/log_formatters/json.rb +9 -20
  38. data/lib/cmdx/log_formatters/key_value.rb +10 -21
  39. data/lib/cmdx/log_formatters/line.rb +7 -19
  40. data/lib/cmdx/log_formatters/logstash.rb +8 -21
  41. data/lib/cmdx/log_formatters/raw.rb +8 -20
  42. data/lib/cmdx/logger_proxy.rb +30 -0
  43. data/lib/cmdx/mergers/deep_merge.rb +23 -0
  44. data/lib/cmdx/mergers/last_write_wins.rb +23 -0
  45. data/lib/cmdx/mergers/no_merge.rb +20 -0
  46. data/lib/cmdx/mergers.rb +95 -0
  47. data/lib/cmdx/middlewares.rb +128 -0
  48. data/lib/cmdx/output.rb +115 -0
  49. data/lib/cmdx/outputs.rb +66 -0
  50. data/lib/cmdx/pipeline.rb +144 -131
  51. data/lib/cmdx/railtie.rb +10 -36
  52. data/lib/cmdx/result.rb +247 -524
  53. data/lib/cmdx/retriers/bounded_random.rb +24 -0
  54. data/lib/cmdx/retriers/decorrelated_jitter.rb +28 -0
  55. data/lib/cmdx/retriers/exponential.rb +23 -0
  56. data/lib/cmdx/retriers/fibonacci.rb +39 -0
  57. data/lib/cmdx/retriers/full_random.rb +23 -0
  58. data/lib/cmdx/retriers/half_random.rb +24 -0
  59. data/lib/cmdx/retriers/linear.rb +23 -0
  60. data/lib/cmdx/retriers.rb +106 -0
  61. data/lib/cmdx/retry.rb +117 -138
  62. data/lib/cmdx/runtime.rb +251 -0
  63. data/lib/cmdx/settings.rb +68 -200
  64. data/lib/cmdx/signal.rb +165 -0
  65. data/lib/cmdx/task.rb +443 -343
  66. data/lib/cmdx/telemetry.rb +108 -0
  67. data/lib/cmdx/util.rb +73 -0
  68. data/lib/cmdx/validators/absence.rb +10 -39
  69. data/lib/cmdx/validators/exclusion.rb +33 -52
  70. data/lib/cmdx/validators/format.rb +19 -49
  71. data/lib/cmdx/validators/inclusion.rb +33 -54
  72. data/lib/cmdx/validators/length.rb +125 -127
  73. data/lib/cmdx/validators/numeric.rb +123 -123
  74. data/lib/cmdx/validators/presence.rb +10 -39
  75. data/lib/cmdx/validators/validate.rb +31 -0
  76. data/lib/cmdx/validators.rb +161 -0
  77. data/lib/cmdx/version.rb +2 -4
  78. data/lib/cmdx/workflow.rb +71 -96
  79. data/lib/cmdx.rb +111 -42
  80. data/lib/generators/cmdx/install_generator.rb +7 -17
  81. data/lib/generators/cmdx/task_generator.rb +12 -29
  82. data/lib/generators/cmdx/templates/install.rb +120 -48
  83. data/lib/generators/cmdx/templates/task.rb.tt +1 -1
  84. data/lib/generators/cmdx/templates/workflow.rb.tt +1 -2
  85. data/lib/generators/cmdx/workflow_generator.rb +12 -29
  86. data/lib/locales/en.yml +8 -7
  87. data/mkdocs.yml +25 -23
  88. metadata +39 -138
  89. data/lib/cmdx/attribute.rb +0 -440
  90. data/lib/cmdx/attribute_registry.rb +0 -185
  91. data/lib/cmdx/attribute_value.rb +0 -252
  92. data/lib/cmdx/callback_registry.rb +0 -169
  93. data/lib/cmdx/coercion_registry.rb +0 -138
  94. data/lib/cmdx/deprecator.rb +0 -77
  95. data/lib/cmdx/exception.rb +0 -46
  96. data/lib/cmdx/executor.rb +0 -378
  97. data/lib/cmdx/identifier.rb +0 -30
  98. data/lib/cmdx/locale.rb +0 -78
  99. data/lib/cmdx/middleware_registry.rb +0 -148
  100. data/lib/cmdx/middlewares/correlate.rb +0 -140
  101. data/lib/cmdx/middlewares/runtime.rb +0 -77
  102. data/lib/cmdx/middlewares/timeout.rb +0 -78
  103. data/lib/cmdx/parallelizer.rb +0 -100
  104. data/lib/cmdx/utils/call.rb +0 -53
  105. data/lib/cmdx/utils/condition.rb +0 -71
  106. data/lib/cmdx/utils/format.rb +0 -82
  107. data/lib/cmdx/utils/normalize.rb +0 -52
  108. data/lib/cmdx/utils/wrap.rb +0 -38
  109. data/lib/cmdx/validator_registry.rb +0 -143
  110. data/lib/generators/cmdx/locale_generator.rb +0 -39
  111. data/lib/locales/af.yml +0 -55
  112. data/lib/locales/ar.yml +0 -55
  113. data/lib/locales/az.yml +0 -55
  114. data/lib/locales/be.yml +0 -55
  115. data/lib/locales/bg.yml +0 -55
  116. data/lib/locales/bn.yml +0 -55
  117. data/lib/locales/bs.yml +0 -55
  118. data/lib/locales/ca.yml +0 -55
  119. data/lib/locales/cnr.yml +0 -55
  120. data/lib/locales/cs.yml +0 -55
  121. data/lib/locales/cy.yml +0 -55
  122. data/lib/locales/da.yml +0 -55
  123. data/lib/locales/de.yml +0 -55
  124. data/lib/locales/dz.yml +0 -55
  125. data/lib/locales/el.yml +0 -55
  126. data/lib/locales/eo.yml +0 -55
  127. data/lib/locales/es.yml +0 -55
  128. data/lib/locales/et.yml +0 -55
  129. data/lib/locales/eu.yml +0 -55
  130. data/lib/locales/fa.yml +0 -55
  131. data/lib/locales/fi.yml +0 -55
  132. data/lib/locales/fr.yml +0 -55
  133. data/lib/locales/fy.yml +0 -55
  134. data/lib/locales/gd.yml +0 -55
  135. data/lib/locales/gl.yml +0 -55
  136. data/lib/locales/he.yml +0 -55
  137. data/lib/locales/hi.yml +0 -55
  138. data/lib/locales/hr.yml +0 -55
  139. data/lib/locales/hu.yml +0 -55
  140. data/lib/locales/hy.yml +0 -55
  141. data/lib/locales/id.yml +0 -55
  142. data/lib/locales/is.yml +0 -55
  143. data/lib/locales/it.yml +0 -55
  144. data/lib/locales/ja.yml +0 -55
  145. data/lib/locales/ka.yml +0 -55
  146. data/lib/locales/kk.yml +0 -55
  147. data/lib/locales/km.yml +0 -55
  148. data/lib/locales/kn.yml +0 -55
  149. data/lib/locales/ko.yml +0 -55
  150. data/lib/locales/lb.yml +0 -55
  151. data/lib/locales/lo.yml +0 -55
  152. data/lib/locales/lt.yml +0 -55
  153. data/lib/locales/lv.yml +0 -55
  154. data/lib/locales/mg.yml +0 -55
  155. data/lib/locales/mk.yml +0 -55
  156. data/lib/locales/ml.yml +0 -55
  157. data/lib/locales/mn.yml +0 -55
  158. data/lib/locales/mr-IN.yml +0 -55
  159. data/lib/locales/ms.yml +0 -55
  160. data/lib/locales/nb.yml +0 -55
  161. data/lib/locales/ne.yml +0 -55
  162. data/lib/locales/nl.yml +0 -55
  163. data/lib/locales/nn.yml +0 -55
  164. data/lib/locales/oc.yml +0 -55
  165. data/lib/locales/or.yml +0 -55
  166. data/lib/locales/pa.yml +0 -55
  167. data/lib/locales/pl.yml +0 -55
  168. data/lib/locales/pt.yml +0 -55
  169. data/lib/locales/rm.yml +0 -55
  170. data/lib/locales/ro.yml +0 -55
  171. data/lib/locales/ru.yml +0 -55
  172. data/lib/locales/sc.yml +0 -55
  173. data/lib/locales/sk.yml +0 -55
  174. data/lib/locales/sl.yml +0 -55
  175. data/lib/locales/sq.yml +0 -55
  176. data/lib/locales/sr.yml +0 -55
  177. data/lib/locales/st.yml +0 -55
  178. data/lib/locales/sv.yml +0 -55
  179. data/lib/locales/sw.yml +0 -55
  180. data/lib/locales/ta.yml +0 -55
  181. data/lib/locales/te.yml +0 -55
  182. data/lib/locales/th.yml +0 -55
  183. data/lib/locales/tl.yml +0 -55
  184. data/lib/locales/tr.yml +0 -55
  185. data/lib/locales/tt.yml +0 -55
  186. data/lib/locales/ug.yml +0 -55
  187. data/lib/locales/uk.yml +0 -55
  188. data/lib/locales/ur.yml +0 -55
  189. data/lib/locales/uz.yml +0 -55
  190. data/lib/locales/vi.yml +0 -55
  191. data/lib/locales/wo.yml +0 -55
  192. data/lib/locales/zh-CN.yml +0 -55
  193. data/lib/locales/zh-HK.yml +0 -55
  194. data/lib/locales/zh-TW.yml +0 -55
  195. data/lib/locales/zh-YUE.yml +0 -55
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ class Retriers
5
+ # Bounded-random jitter. Produces a uniform delay in `[delay, 2*delay]`.
6
+ # Guarantees at least the base delay between attempts while still
7
+ # decorrelating retry timing.
8
+ #
9
+ # @api private
10
+ module BoundedRandom
11
+
12
+ extend self
13
+
14
+ # @param _attempt [Integer] ignored
15
+ # @param delay [Float] base delay in seconds
16
+ # @param _prev_delay [Float, nil] ignored
17
+ # @return [Float] computed delay
18
+ def call(_attempt, delay, _prev_delay = nil)
19
+ delay + (rand * delay)
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ class Retriers
5
+ # AWS-recommended decorrelated jitter. Produces a uniform delay in
6
+ # `[delay, prev_delay * 3]`, threading state across attempts via
7
+ # `prev_delay`. When no previous delay exists the upper bound collapses
8
+ # to `3 * delay`, matching the AWS reference implementation.
9
+ #
10
+ # @see https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
11
+ # @api private
12
+ module DecorrelatedJitter
13
+
14
+ extend self
15
+
16
+ # @param _attempt [Integer] ignored
17
+ # @param delay [Float] base delay in seconds (also the lower bound)
18
+ # @param prev_delay [Float, nil] previous computed delay; falls back to
19
+ # `delay` so the first call samples in `[delay, 3*delay]`
20
+ # @return [Float] computed delay
21
+ def call(_attempt, delay, prev_delay = nil)
22
+ base = prev_delay || delay
23
+ delay + (rand * ((base * 3) - delay))
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ class Retriers
5
+ # Exponential backoff. Doubles the base delay every attempt:
6
+ # `delay * (2 ** attempt)`.
7
+ #
8
+ # @api private
9
+ module Exponential
10
+
11
+ extend self
12
+
13
+ # @param attempt [Integer] zero-based retry attempt
14
+ # @param delay [Float] base delay in seconds
15
+ # @param _prev_delay [Float, nil] ignored
16
+ # @return [Float] computed delay
17
+ def call(attempt, delay, _prev_delay = nil)
18
+ delay * (2**attempt)
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ class Retriers
5
+ # Fibonacci backoff. Sleeps `delay * fib(attempt + 1)` where `fib(1) == 1`,
6
+ # `fib(2) == 1`, `fib(3) == 2`, ... Multipliers grow as 1, 1, 2, 3, 5, 8.
7
+ # Slower-growing than exponential, faster-growing than linear.
8
+ #
9
+ # @api private
10
+ module Fibonacci
11
+
12
+ extend self
13
+
14
+ # @param attempt [Integer] zero-based retry attempt
15
+ # @param delay [Float] base delay in seconds
16
+ # @param _prev_delay [Float, nil] ignored
17
+ # @return [Float] computed delay
18
+ def call(attempt, delay, _prev_delay = nil)
19
+ delay * sequence(attempt + 1)
20
+ end
21
+
22
+ private
23
+
24
+ # Iterative Fibonacci. `sequence(1) == 1`, `sequence(2) == 1`,
25
+ # `sequence(3) == 2`, ...
26
+ #
27
+ # @param n [Integer] one-based index into the Fibonacci sequence
28
+ # @return [Integer]
29
+ # @api private
30
+ def sequence(n)
31
+ a = 0
32
+ b = 1
33
+ n.times { a, b = b, a + b }
34
+ a
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ class Retriers
5
+ # Full-random jitter. Produces a uniform delay in `[0, delay]`. Maximizes
6
+ # spread at the cost of occasional very-fast retries.
7
+ #
8
+ # @api private
9
+ module FullRandom
10
+
11
+ extend self
12
+
13
+ # @param _attempt [Integer] ignored
14
+ # @param delay [Float] base delay in seconds
15
+ # @param _prev_delay [Float, nil] ignored
16
+ # @return [Float] computed delay
17
+ def call(_attempt, delay, _prev_delay = nil)
18
+ rand * delay
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ class Retriers
5
+ # Half-random jitter. Produces a uniform delay in `[delay/2, delay]`.
6
+ # Useful when you want a tighter spread than `:full_random` while still
7
+ # decorrelating retries from synchronized clients.
8
+ #
9
+ # @api private
10
+ module HalfRandom
11
+
12
+ extend self
13
+
14
+ # @param _attempt [Integer] ignored
15
+ # @param delay [Float] base delay in seconds
16
+ # @param _prev_delay [Float, nil] ignored
17
+ # @return [Float] computed delay
18
+ def call(_attempt, delay, _prev_delay = nil)
19
+ (delay / 2.0) + (rand * delay / 2.0)
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ class Retriers
5
+ # Linear backoff. Sleeps `delay * (attempt + 1)` — multiples of the base
6
+ # delay grow arithmetically (1x, 2x, 3x, ...).
7
+ #
8
+ # @api private
9
+ module Linear
10
+
11
+ extend self
12
+
13
+ # @param attempt [Integer] zero-based retry attempt
14
+ # @param delay [Float] base delay in seconds
15
+ # @param _prev_delay [Float, nil] ignored
16
+ # @return [Float] computed delay
17
+ def call(attempt, delay, _prev_delay = nil)
18
+ delay * (attempt + 1)
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ # Registry of named retry/jitter strategies used by `Retry` to compute the
5
+ # sleep duration between attempts. Ships with built-ins for `:exponential`,
6
+ # `:half_random`, `:full_random`, `:bounded_random`, `:linear`, `:fibonacci`,
7
+ # and `:decorrelated_jitter`. A retrier is any callable accepting
8
+ # `call(attempt, delay, prev_delay)` that returns the next delay in seconds.
9
+ class Retriers
10
+
11
+ attr_reader :registry
12
+
13
+ def initialize
14
+ @registry = {
15
+ exponential: Retriers::Exponential,
16
+ half_random: Retriers::HalfRandom,
17
+ full_random: Retriers::FullRandom,
18
+ bounded_random: Retriers::BoundedRandom,
19
+ linear: Retriers::Linear,
20
+ fibonacci: Retriers::Fibonacci,
21
+ decorrelated_jitter: Retriers::DecorrelatedJitter
22
+ }
23
+ end
24
+
25
+ # @param source [Retriers] registry to duplicate
26
+ # @return [void]
27
+ def initialize_copy(source)
28
+ @registry = source.registry.dup
29
+ end
30
+
31
+ # Registers a named retrier, overwriting any existing entry.
32
+ #
33
+ # @param name [Symbol]
34
+ # @param callable [#call, nil] pass either this or a block
35
+ # @param block [#call, nil] retrier callable when `callable` is omitted
36
+ # @yield retrier body — `call(attempt, delay, prev_delay)`
37
+ # @return [Retriers] self for chaining
38
+ # @raise [ArgumentError] when both `callable` and a block are given, or
39
+ # when the resolved retrier isn't callable
40
+ def register(name, callable = nil, &block)
41
+ retrier = callable || block
42
+
43
+ if callable && block
44
+ raise ArgumentError, "provide either a callable or a block, not both"
45
+ elsif !retrier.respond_to?(:call)
46
+ raise ArgumentError, "retrier must respond to #call"
47
+ end
48
+
49
+ registry[name.to_sym] = retrier
50
+ self
51
+ end
52
+
53
+ # @param name [Symbol]
54
+ # @return [Retriers] self for chaining
55
+ def deregister(name)
56
+ registry.delete(name.to_sym)
57
+ self
58
+ end
59
+
60
+ # @param name [Symbol]
61
+ # @return [Boolean] whether a retrier is registered under `name`
62
+ def key?(name)
63
+ registry.key?(name.to_sym)
64
+ end
65
+
66
+ # @param name [Symbol]
67
+ # @return [#call] the registered retrier
68
+ # @raise [ArgumentError] when `name` isn't registered
69
+ def lookup(name)
70
+ registry[name] || begin
71
+ raise ArgumentError, "unknown retrier: #{name.inspect}"
72
+ end
73
+ end
74
+
75
+ # Resolves a `:jitter` spec to a concrete callable. Accepts a Symbol
76
+ # (registry lookup) or any object responding to `#call`. `nil` resolves
77
+ # to `nil` so callers can fall back to the unjittered base delay.
78
+ #
79
+ # @param spec [Symbol, #call, nil]
80
+ # @return [#call, nil]
81
+ # @raise [ArgumentError] when `spec` is an unknown symbol or not callable
82
+ def resolve(spec)
83
+ case spec
84
+ when NilClass
85
+ nil
86
+ when Symbol
87
+ lookup(spec)
88
+ else
89
+ return spec if spec.respond_to?(:call)
90
+
91
+ raise ArgumentError, "unknown retrier: #{spec.inspect}"
92
+ end
93
+ end
94
+
95
+ # @return [Boolean]
96
+ def empty?
97
+ registry.empty?
98
+ end
99
+
100
+ # @return [Integer]
101
+ def size
102
+ registry.size
103
+ end
104
+
105
+ end
106
+ end
data/lib/cmdx/retry.rb CHANGED
@@ -1,165 +1,144 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- # Manages retry logic and state for task execution.
5
- #
6
- # The Retry class tracks retry availability, attempt counts, and
7
- # remaining retries for a given task. It also resolves exception
8
- # matching and computes wait times using configurable jitter strategies.
4
+ # Configurable retry-on-exception wrapper around a task's `work`. Supports
5
+ # exception list, attempt `:limit`, base `:delay`, `:max_delay` cap, and
6
+ # `:jitter` strategy (symbol, proc, or a configured block). Declared via
7
+ # `Task.retry_on` and accumulates across inheritance.
9
8
  class Retry
10
9
 
11
- # Returns the task instance associated with this retry.
12
- #
13
- # @return [Task] the task being retried
14
- #
15
- # @example
16
- # retry_instance.task # => #<CreateUser ...>
17
- #
18
- # @rbs @task: Task
19
- attr_reader :task
10
+ attr_reader :exceptions
20
11
 
21
- # Creates a new Retry instance for the given task.
22
- #
23
- # @param task [Task] the task to manage retries for
24
- #
25
- # @return [Retry] a new Retry instance
26
- #
27
- # @example
28
- # retry_instance = Retry.new(task)
29
- #
30
- # @rbs (Task task) -> void
31
- def initialize(task)
32
- @task = task
12
+ # @param exceptions [Array<Class>] exceptions to retry on
13
+ # @param options [Hash{Symbol => Object}]
14
+ # @param block [#call, nil] optional jitter callable used when `:jitter` isn't set
15
+ # @option options [Integer] :limit (3) maximum retry attempts
16
+ # @option options [Float] :delay (0.5) base delay in seconds between attempts
17
+ # @option options [Float] :max_delay clamp for computed delays
18
+ # @option options [Symbol, Proc, #call] :jitter built-in strategy (`:exponential`,
19
+ # `:half_random`, `:full_random`, `:bounded_random`, `:linear`, `:fibonacci`,
20
+ # `:decorrelated_jitter`) or custom
21
+ # @yieldparam attempt [Integer]
22
+ # @yieldparam delay [Float]
23
+ def initialize(exceptions, options = EMPTY_HASH, &block)
24
+ @exceptions = exceptions.flatten
25
+ @options = options.freeze
26
+ @block = block
33
27
  end
34
28
 
35
- # Returns the total number of retries configured for the task.
36
- #
37
- # @return [Integer] the configured retry count
38
- #
39
- # @example
40
- # retry_instance.available # => 3
41
- #
42
- # @rbs () -> Integer
43
- def available
44
- Integer(task.class.settings.retries || 0)
45
- end
29
+ # Returns a new Retry layering `new_exceptions` and `new_options` onto the
30
+ # current one. Used for inheritance so subclasses extend rather than
31
+ # replace.
32
+ #
33
+ # @param new_exceptions [Array<Class>]
34
+ # @param new_options [Hash{Symbol => Object}]
35
+ # @param block [#call, nil] replacement jitter callable (falls back to the prior block)
36
+ # @yield [attempt, delay] optional replacement jitter block
37
+ # @return [Retry]
38
+ def build(new_exceptions, new_options, &block)
39
+ return self if new_exceptions.empty?
46
40
 
47
- # Checks if the task has any retries configured.
48
- #
49
- # @return [Boolean] true if retries are configured
50
- #
51
- # @example
52
- # retry_instance.available? # => true
53
- #
54
- # @rbs () -> bool
55
- def available?
56
- available.positive?
41
+ merged_exceptions = exceptions | new_exceptions.flatten
42
+ merged_options = @options.merge(new_options)
43
+
44
+ self.class.new(merged_exceptions, merged_options, &block || @block)
57
45
  end
58
46
 
59
- # Returns the number of retry attempts already made.
60
- #
61
- # @return [Integer] the current retry attempt count
62
- #
63
- # @example
64
- # retry_instance.attempts # => 1
65
- #
66
- # @rbs () -> Integer
67
- def attempts
68
- Integer(task.result.retries || 0)
47
+ # @return [Integer]
48
+ def limit
49
+ @options[:limit] || 3
69
50
  end
70
51
 
71
- # Checks if the task has been retried at least once.
72
- #
73
- # @return [Boolean] true if at least one retry has occurred
74
- #
75
- # @example
76
- # retry_instance.retried? # => true
77
- #
78
- # @rbs () -> bool
79
- def retried?
80
- attempts.positive?
52
+ # @return [Float] base delay in seconds
53
+ def delay
54
+ @options[:delay] || 0.5
81
55
  end
82
56
 
83
- # Returns the number of retries still available.
84
- #
85
- # @return [Integer] the remaining retry count
86
- #
87
- # @example
88
- # retry_instance.remaining # => 2
89
- #
90
- # @rbs () -> Integer
91
- def remaining
92
- available - attempts
57
+ # @return [Float, nil] upper bound for computed delays
58
+ def max_delay
59
+ @options[:max_delay]
93
60
  end
94
61
 
95
- # Checks if there are retries still available.
96
- #
97
- # @return [Boolean] true if remaining retries exist
98
- #
99
- # @example
100
- # retry_instance.remaining? # => true
101
- #
102
- # @rbs () -> bool
103
- def remaining?
104
- remaining.positive?
62
+ # @return [Symbol, Proc, #call, nil] jitter strategy or the block given to {#initialize}
63
+ def jitter
64
+ @options[:jitter] || @block
105
65
  end
106
66
 
107
- # Returns the list of exception classes eligible for retry.
108
- #
109
- # @return [Array<Class>] exception classes that trigger a retry
110
- #
111
- # @example
112
- # retry_instance.exceptions # => [StandardError, CMDx::TimeoutError]
113
- #
114
- # @rbs () -> Array[Class]
115
- def exceptions
116
- @exceptions ||= Utils::Wrap.array(
117
- task.class.settings.retry_on ||
118
- [StandardError, CMDx::TimeoutError]
119
- )
67
+ # Sleeps `attempt`'s jittered/bounded delay. No-op when the base delay is zero.
68
+ #
69
+ # @param attempt [Integer] zero-based retry attempt number
70
+ # @param task [Task, nil] used as receiver for Symbol/Proc jitter strategies
71
+ # @param prev_delay [Float, nil] previous computed delay; only consumed by
72
+ # `:decorrelated_jitter` to thread state across attempts
73
+ # @return [Float, nil] the computed (and possibly clamped) delay, or `nil` when
74
+ # `delay` is zero
75
+ def wait(attempt, task = nil, prev_delay = nil)
76
+ return unless delay.positive?
77
+
78
+ d =
79
+ case jitter
80
+ when NilClass
81
+ delay
82
+ when Symbol
83
+ registry = retriers_registry(task)
84
+
85
+ if registry.key?(jitter)
86
+ registry.lookup(jitter).call(attempt, delay, prev_delay)
87
+ else
88
+ task.send(jitter, attempt, delay)
89
+ end
90
+ when Proc
91
+ task.instance_exec(attempt, delay, &jitter)
92
+ else
93
+ if jitter.respond_to?(:call)
94
+ jitter.call(attempt, delay)
95
+ else
96
+ delay
97
+ end
98
+ end
99
+
100
+ d = d.clamp(0, max_delay) if max_delay
101
+ Kernel.sleep(d) if d.positive?
102
+ d
120
103
  end
121
104
 
122
- # Checks if the given exception matches any configured retry exception.
123
- #
124
- # @param exception [Exception] the exception to check
125
- #
126
- # @return [Boolean] true if the exception qualifies for retry
127
- #
128
- # @example
129
- # retry_instance.exception?(RuntimeError.new("fail")) # => true
130
- #
131
- # @rbs (Exception exception) -> bool
132
- def exception?(exception)
133
- exceptions.any? { |e| exception.class <= e }
105
+ # Executes the block up to `limit + 1` times. Re-raises the last
106
+ # exception when attempts are exhausted.
107
+ #
108
+ # @param task [Task, nil] passed to {#wait} so jitter strategies can use it
109
+ # @yieldparam attempt [Integer] zero-based attempt index
110
+ # @yieldreturn [Object] the block's successful return value
111
+ # @return [Object] the block's successful return value
112
+ # @raise [Exception] the last caught exception once retries exhaust
113
+ def process(task = nil, &)
114
+ return yield(0) if exceptions.empty? || !limit.positive?
115
+
116
+ prev_delay = nil
117
+ (limit + 1).times do |attempt|
118
+ return yield(attempt)
119
+ rescue *exceptions => e
120
+ raise(e) if attempt >= limit
121
+ raise(e) unless Util.satisfied?(@options[:if], @options[:unless], task, e, attempt)
122
+
123
+ prev_delay = wait(attempt, task, prev_delay)
124
+ end
134
125
  end
135
126
 
136
- # Computes the wait time before the next retry attempt.
137
- #
138
- # Supports multiple jitter strategies: a Symbol calls a task method,
139
- # a Proc is evaluated in the task instance context, a callable object
140
- # receives the task and attempts, and a Numeric is multiplied by the
141
- # attempt count.
142
- #
143
- # @return [Float] the wait duration in seconds
144
- #
145
- # @example With numeric jitter (0.5 * attempts)
146
- # retry_instance.wait # => 1.0
147
- # @example With symbol jitter referencing a task method
148
- # retry_instance.wait # => 2.5
149
- #
150
- # @rbs () -> Float
151
- def wait
152
- jitter = task.class.settings.retry_jitter
153
-
154
- if jitter.is_a?(Symbol)
155
- task.send(jitter, attempts)
156
- elsif jitter.is_a?(Proc)
157
- task.instance_exec(attempts, &jitter)
158
- elsif jitter.respond_to?(:call)
159
- jitter.call(task, attempts)
127
+ private
128
+
129
+ # Resolves the retriers registry to consult for built-in jitter strategies.
130
+ # Prefers the task class's registry (so per-task `register(:retrier, ...)`
131
+ # overrides take effect) and falls back to the global configuration when
132
+ # no task is supplied.
133
+ #
134
+ # @param task [Task, nil]
135
+ # @return [Retriers]
136
+ def retriers_registry(task)
137
+ if task && task.class.respond_to?(:retriers)
138
+ task.class.retriers
160
139
  else
161
- jitter.to_f * attempts
162
- end.to_f
140
+ CMDx.configuration.retriers
141
+ end
163
142
  end
164
143
 
165
144
  end