cmdx 1.20.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +131 -1
- data/README.md +37 -24
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callbacks.rb +179 -0
- data/lib/cmdx/chain.rb +78 -175
- data/lib/cmdx/coercions/array.rb +19 -33
- data/lib/cmdx/coercions/big_decimal.rb +12 -29
- data/lib/cmdx/coercions/boolean.rb +25 -45
- data/lib/cmdx/coercions/coerce.rb +32 -0
- data/lib/cmdx/coercions/complex.rb +12 -27
- data/lib/cmdx/coercions/date.rb +29 -33
- data/lib/cmdx/coercions/date_time.rb +29 -33
- data/lib/cmdx/coercions/float.rb +8 -29
- data/lib/cmdx/coercions/hash.rb +17 -43
- data/lib/cmdx/coercions/integer.rb +8 -32
- data/lib/cmdx/coercions/rational.rb +12 -33
- data/lib/cmdx/coercions/string.rb +6 -24
- data/lib/cmdx/coercions/symbol.rb +12 -26
- data/lib/cmdx/coercions/time.rb +31 -35
- data/lib/cmdx/coercions.rb +174 -0
- data/lib/cmdx/configuration.rb +45 -225
- data/lib/cmdx/context.rb +263 -242
- data/lib/cmdx/deprecation.rb +67 -0
- data/lib/cmdx/deprecators/error.rb +22 -0
- data/lib/cmdx/deprecators/log.rb +22 -0
- data/lib/cmdx/deprecators/warn.rb +21 -0
- data/lib/cmdx/deprecators.rb +101 -0
- data/lib/cmdx/errors.rb +145 -79
- data/lib/cmdx/executors/fiber.rb +42 -0
- data/lib/cmdx/executors/thread.rb +36 -0
- data/lib/cmdx/executors.rb +95 -0
- data/lib/cmdx/fault.rb +85 -78
- data/lib/cmdx/i18n_proxy.rb +104 -0
- data/lib/cmdx/input.rb +294 -0
- data/lib/cmdx/inputs.rb +218 -0
- data/lib/cmdx/log_formatters/json.rb +9 -20
- data/lib/cmdx/log_formatters/key_value.rb +10 -21
- data/lib/cmdx/log_formatters/line.rb +7 -19
- data/lib/cmdx/log_formatters/logstash.rb +8 -21
- data/lib/cmdx/log_formatters/raw.rb +8 -20
- data/lib/cmdx/logger_proxy.rb +30 -0
- data/lib/cmdx/mergers/deep_merge.rb +23 -0
- data/lib/cmdx/mergers/last_write_wins.rb +23 -0
- data/lib/cmdx/mergers/no_merge.rb +20 -0
- data/lib/cmdx/mergers.rb +95 -0
- data/lib/cmdx/middlewares.rb +128 -0
- data/lib/cmdx/output.rb +115 -0
- data/lib/cmdx/outputs.rb +66 -0
- data/lib/cmdx/pipeline.rb +144 -131
- data/lib/cmdx/railtie.rb +10 -36
- data/lib/cmdx/result.rb +252 -473
- data/lib/cmdx/retriers/bounded_random.rb +24 -0
- data/lib/cmdx/retriers/decorrelated_jitter.rb +28 -0
- data/lib/cmdx/retriers/exponential.rb +23 -0
- data/lib/cmdx/retriers/fibonacci.rb +39 -0
- data/lib/cmdx/retriers/full_random.rb +23 -0
- data/lib/cmdx/retriers/half_random.rb +24 -0
- data/lib/cmdx/retriers/linear.rb +23 -0
- data/lib/cmdx/retriers.rb +106 -0
- data/lib/cmdx/retry.rb +117 -138
- data/lib/cmdx/runtime.rb +251 -0
- data/lib/cmdx/settings.rb +68 -196
- data/lib/cmdx/signal.rb +165 -0
- data/lib/cmdx/task.rb +443 -336
- data/lib/cmdx/telemetry.rb +108 -0
- data/lib/cmdx/util.rb +73 -0
- data/lib/cmdx/validators/absence.rb +10 -39
- data/lib/cmdx/validators/exclusion.rb +33 -52
- data/lib/cmdx/validators/format.rb +19 -49
- data/lib/cmdx/validators/inclusion.rb +33 -54
- data/lib/cmdx/validators/length.rb +125 -127
- data/lib/cmdx/validators/numeric.rb +123 -123
- data/lib/cmdx/validators/presence.rb +10 -39
- data/lib/cmdx/validators/validate.rb +31 -0
- data/lib/cmdx/validators.rb +161 -0
- data/lib/cmdx/version.rb +2 -4
- data/lib/cmdx/workflow.rb +74 -82
- data/lib/cmdx.rb +111 -42
- data/lib/generators/cmdx/install_generator.rb +7 -17
- data/lib/generators/cmdx/task_generator.rb +12 -29
- data/lib/generators/cmdx/templates/install.rb +128 -52
- data/lib/generators/cmdx/templates/task.rb.tt +1 -1
- data/lib/generators/cmdx/templates/workflow.rb.tt +1 -2
- data/lib/generators/cmdx/workflow_generator.rb +12 -29
- data/lib/locales/en.yml +9 -6
- data/mkdocs.yml +25 -23
- metadata +39 -138
- data/lib/cmdx/attribute.rb +0 -440
- data/lib/cmdx/attribute_registry.rb +0 -185
- data/lib/cmdx/attribute_value.rb +0 -252
- data/lib/cmdx/callback_registry.rb +0 -169
- data/lib/cmdx/coercion_registry.rb +0 -138
- data/lib/cmdx/deprecator.rb +0 -77
- data/lib/cmdx/exception.rb +0 -46
- data/lib/cmdx/executor.rb +0 -374
- data/lib/cmdx/identifier.rb +0 -30
- data/lib/cmdx/locale.rb +0 -78
- data/lib/cmdx/middleware_registry.rb +0 -148
- data/lib/cmdx/middlewares/correlate.rb +0 -140
- data/lib/cmdx/middlewares/runtime.rb +0 -62
- data/lib/cmdx/middlewares/timeout.rb +0 -78
- data/lib/cmdx/parallelizer.rb +0 -100
- data/lib/cmdx/utils/call.rb +0 -53
- data/lib/cmdx/utils/condition.rb +0 -71
- data/lib/cmdx/utils/format.rb +0 -82
- data/lib/cmdx/utils/normalize.rb +0 -52
- data/lib/cmdx/utils/wrap.rb +0 -38
- data/lib/cmdx/validator_registry.rb +0 -143
- data/lib/generators/cmdx/locale_generator.rb +0 -39
- data/lib/locales/af.yml +0 -53
- data/lib/locales/ar.yml +0 -53
- data/lib/locales/az.yml +0 -53
- data/lib/locales/be.yml +0 -53
- data/lib/locales/bg.yml +0 -53
- data/lib/locales/bn.yml +0 -53
- data/lib/locales/bs.yml +0 -53
- data/lib/locales/ca.yml +0 -53
- data/lib/locales/cnr.yml +0 -53
- data/lib/locales/cs.yml +0 -53
- data/lib/locales/cy.yml +0 -53
- data/lib/locales/da.yml +0 -53
- data/lib/locales/de.yml +0 -53
- data/lib/locales/dz.yml +0 -53
- data/lib/locales/el.yml +0 -53
- data/lib/locales/eo.yml +0 -53
- data/lib/locales/es.yml +0 -53
- data/lib/locales/et.yml +0 -53
- data/lib/locales/eu.yml +0 -53
- data/lib/locales/fa.yml +0 -53
- data/lib/locales/fi.yml +0 -53
- data/lib/locales/fr.yml +0 -53
- data/lib/locales/fy.yml +0 -53
- data/lib/locales/gd.yml +0 -53
- data/lib/locales/gl.yml +0 -53
- data/lib/locales/he.yml +0 -53
- data/lib/locales/hi.yml +0 -53
- data/lib/locales/hr.yml +0 -53
- data/lib/locales/hu.yml +0 -53
- data/lib/locales/hy.yml +0 -53
- data/lib/locales/id.yml +0 -53
- data/lib/locales/is.yml +0 -53
- data/lib/locales/it.yml +0 -53
- data/lib/locales/ja.yml +0 -53
- data/lib/locales/ka.yml +0 -53
- data/lib/locales/kk.yml +0 -53
- data/lib/locales/km.yml +0 -53
- data/lib/locales/kn.yml +0 -53
- data/lib/locales/ko.yml +0 -53
- data/lib/locales/lb.yml +0 -53
- data/lib/locales/lo.yml +0 -53
- data/lib/locales/lt.yml +0 -53
- data/lib/locales/lv.yml +0 -53
- data/lib/locales/mg.yml +0 -53
- data/lib/locales/mk.yml +0 -53
- data/lib/locales/ml.yml +0 -53
- data/lib/locales/mn.yml +0 -53
- data/lib/locales/mr-IN.yml +0 -53
- data/lib/locales/ms.yml +0 -53
- data/lib/locales/nb.yml +0 -53
- data/lib/locales/ne.yml +0 -53
- data/lib/locales/nl.yml +0 -53
- data/lib/locales/nn.yml +0 -53
- data/lib/locales/oc.yml +0 -53
- data/lib/locales/or.yml +0 -53
- data/lib/locales/pa.yml +0 -53
- data/lib/locales/pl.yml +0 -53
- data/lib/locales/pt.yml +0 -53
- data/lib/locales/rm.yml +0 -53
- data/lib/locales/ro.yml +0 -53
- data/lib/locales/ru.yml +0 -53
- data/lib/locales/sc.yml +0 -53
- data/lib/locales/sk.yml +0 -53
- data/lib/locales/sl.yml +0 -53
- data/lib/locales/sq.yml +0 -53
- data/lib/locales/sr.yml +0 -53
- data/lib/locales/st.yml +0 -53
- data/lib/locales/sv.yml +0 -53
- data/lib/locales/sw.yml +0 -53
- data/lib/locales/ta.yml +0 -53
- data/lib/locales/te.yml +0 -53
- data/lib/locales/th.yml +0 -53
- data/lib/locales/tl.yml +0 -53
- data/lib/locales/tr.yml +0 -53
- data/lib/locales/tt.yml +0 -53
- data/lib/locales/ug.yml +0 -53
- data/lib/locales/uk.yml +0 -53
- data/lib/locales/ur.yml +0 -53
- data/lib/locales/uz.yml +0 -53
- data/lib/locales/vi.yml +0 -53
- data/lib/locales/wo.yml +0 -53
- data/lib/locales/zh-CN.yml +0 -53
- data/lib/locales/zh-HK.yml +0 -53
- data/lib/locales/zh-TW.yml +0 -53
- data/lib/locales/zh-YUE.yml +0 -53
|
@@ -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
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# @param
|
|
24
|
-
#
|
|
25
|
-
# @
|
|
26
|
-
#
|
|
27
|
-
# @
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# @
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
# @
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
# @
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
#
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
#
|
|
108
|
-
#
|
|
109
|
-
# @
|
|
110
|
-
#
|
|
111
|
-
# @
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
def
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
#
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
# @
|
|
127
|
-
#
|
|
128
|
-
# @
|
|
129
|
-
#
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
# @
|
|
144
|
-
#
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
162
|
-
end
|
|
140
|
+
CMDx.configuration.retriers
|
|
141
|
+
end
|
|
163
142
|
end
|
|
164
143
|
|
|
165
144
|
end
|