faulty 0.1.4 → 0.5.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/.github/workflows/ci.yml +49 -0
- data/.rubocop.yml +9 -0
- data/CHANGELOG.md +55 -0
- data/Gemfile +8 -3
- data/README.md +883 -310
- data/bin/check-version +5 -1
- data/faulty.gemspec +1 -1
- data/lib/faulty.rb +167 -43
- data/lib/faulty/cache.rb +3 -1
- data/lib/faulty/cache/auto_wire.rb +58 -0
- data/lib/faulty/cache/circuit_proxy.rb +61 -0
- data/lib/faulty/cache/default.rb +10 -21
- data/lib/faulty/cache/fault_tolerant_proxy.rb +15 -4
- data/lib/faulty/cache/interface.rb +1 -1
- data/lib/faulty/cache/mock.rb +1 -1
- data/lib/faulty/cache/null.rb +1 -1
- data/lib/faulty/cache/rails.rb +9 -10
- data/lib/faulty/circuit.rb +31 -16
- data/lib/faulty/error.rb +29 -7
- data/lib/faulty/events.rb +1 -1
- data/lib/faulty/events/callback_listener.rb +1 -1
- data/lib/faulty/events/honeybadger_listener.rb +1 -1
- data/lib/faulty/events/listener_interface.rb +1 -1
- data/lib/faulty/events/log_listener.rb +5 -6
- data/lib/faulty/events/notifier.rb +1 -1
- data/lib/faulty/immutable_options.rb +1 -1
- data/lib/faulty/patch.rb +154 -0
- data/lib/faulty/patch/base.rb +46 -0
- data/lib/faulty/patch/redis.rb +60 -0
- data/lib/faulty/result.rb +2 -2
- data/lib/faulty/status.rb +3 -2
- data/lib/faulty/storage.rb +4 -1
- data/lib/faulty/storage/auto_wire.rb +107 -0
- data/lib/faulty/storage/circuit_proxy.rb +64 -0
- data/lib/faulty/storage/fallback_chain.rb +207 -0
- data/lib/faulty/storage/fault_tolerant_proxy.rb +52 -57
- data/lib/faulty/storage/interface.rb +3 -2
- data/lib/faulty/storage/memory.rb +8 -4
- data/lib/faulty/storage/redis.rb +75 -13
- data/lib/faulty/version.rb +2 -2
- metadata +14 -7
- data/.travis.yml +0 -46
- data/lib/faulty/scope.rb +0 -117
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Faulty
|
4
|
+
module Patch
|
5
|
+
# Can be included in patch modules to provide common functionality
|
6
|
+
#
|
7
|
+
# The patch needs to set `@faulty_circuit`
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# module ThingPatch
|
11
|
+
# include Faulty::Patch::Base
|
12
|
+
#
|
13
|
+
# def initialize(options = {})
|
14
|
+
# @faulty_circuit = Faulty::Patch.circuit_from_hash('thing', options[:faulty])
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# def do_something
|
18
|
+
# faulty_run { super }
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# Thing.prepend(ThingPatch)
|
23
|
+
module Base
|
24
|
+
# Run a block wrapped by `@faulty_circuit`
|
25
|
+
#
|
26
|
+
# If `@faulty_circuit` is not set, the block will be run with no
|
27
|
+
# circuit.
|
28
|
+
#
|
29
|
+
# Nested calls to this method will only cause the circuit to be triggered
|
30
|
+
# once.
|
31
|
+
#
|
32
|
+
# @yield A block to run inside the circuit
|
33
|
+
# @return The block return value
|
34
|
+
def faulty_run
|
35
|
+
faulty_running_key = "faulty_running_#{object_id}"
|
36
|
+
return yield unless @faulty_circuit
|
37
|
+
return yield if Thread.current[faulty_running_key]
|
38
|
+
|
39
|
+
Thread.current[faulty_running_key] = true
|
40
|
+
@faulty_circuit.run { yield }
|
41
|
+
ensure
|
42
|
+
Thread.current[faulty_running_key] = false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'redis'
|
4
|
+
|
5
|
+
class Faulty
|
6
|
+
module Patch
|
7
|
+
# Patch Redis to run all network IO in a circuit
|
8
|
+
#
|
9
|
+
# This module is not required by default
|
10
|
+
#
|
11
|
+
# Pass a `:faulty` key into your redis connection options to enable
|
12
|
+
# circuit protection. This hash is a hash of circuit options for the
|
13
|
+
# internal circuit. The hash may also have a `:instance` key, which is the
|
14
|
+
# faulty instance to create the circuit from. `Faulty.default` will be
|
15
|
+
# used if no instance is given. The `:instance` key can also reference a
|
16
|
+
# registered Faulty instance or a global constantso that it can be set
|
17
|
+
# from config files. See {Patch.circuit_from_hash}.
|
18
|
+
#
|
19
|
+
# @example
|
20
|
+
# require 'faulty/patch/redis'
|
21
|
+
#
|
22
|
+
# redis = Redis.new(url: 'redis://localhost:6379', faulty: {})
|
23
|
+
# redis.connect # raises Faulty::CircuitError if connection fails
|
24
|
+
#
|
25
|
+
# # If the faulty key is not given, no circuit is used
|
26
|
+
# redis = Redis.new(url: 'redis://localhost:6379')
|
27
|
+
# redis.connect # not protected by a circuit
|
28
|
+
#
|
29
|
+
# @see Patch.circuit_from_hash
|
30
|
+
module Redis
|
31
|
+
include Base
|
32
|
+
|
33
|
+
Patch.define_circuit_errors(self, ::Redis::BaseConnectionError)
|
34
|
+
|
35
|
+
# Patches Redis to add the `:faulty` key
|
36
|
+
def initialize(options = {})
|
37
|
+
@faulty_circuit = Patch.circuit_from_hash(
|
38
|
+
'redis',
|
39
|
+
options[:faulty],
|
40
|
+
errors: [::Redis::BaseConnectionError],
|
41
|
+
patched_error_module: Faulty::Patch::Redis
|
42
|
+
)
|
43
|
+
|
44
|
+
super
|
45
|
+
end
|
46
|
+
|
47
|
+
# The initial connection is protected by a circuit
|
48
|
+
def connect
|
49
|
+
faulty_run { super }
|
50
|
+
end
|
51
|
+
|
52
|
+
# Reads/writes to redis are protected
|
53
|
+
def io(&block)
|
54
|
+
faulty_run { super }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
::Redis::Client.prepend(Faulty::Patch::Redis)
|
data/lib/faulty/result.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
class Faulty
|
4
4
|
# An approximation of the `Result` type from some strongly-typed languages.
|
5
5
|
#
|
6
6
|
# F#: https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/results
|
@@ -53,7 +53,7 @@ module Faulty
|
|
53
53
|
#
|
54
54
|
# @param ok An ok value
|
55
55
|
# @param error [Error] An error instance
|
56
|
-
def initialize(ok: NOTHING, error: NOTHING)
|
56
|
+
def initialize(ok: NOTHING, error: NOTHING)
|
57
57
|
if ok.equal?(NOTHING) && error.equal?(NOTHING)
|
58
58
|
raise ArgumentError, 'Result must have an ok or error value'
|
59
59
|
end
|
data/lib/faulty/status.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
class Faulty
|
4
4
|
# The status of a circuit
|
5
5
|
#
|
6
6
|
# Includes information like the state and locks. Also calculates
|
@@ -144,9 +144,10 @@ module Faulty
|
|
144
144
|
|
145
145
|
def finalize
|
146
146
|
raise ArgumentError, "state must be a symbol in #{self.class}::STATES" unless STATES.include?(state)
|
147
|
-
unless lock.nil? || LOCKS.include?(
|
147
|
+
unless lock.nil? || LOCKS.include?(lock)
|
148
148
|
raise ArgumentError, "lock must be a symbol in #{self.class}::LOCKS or nil"
|
149
149
|
end
|
150
|
+
raise ArgumentError, 'opened_at is required if state is open' if state == :open && opened_at.nil?
|
150
151
|
end
|
151
152
|
|
152
153
|
def required
|
data/lib/faulty/storage.rb
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
class Faulty
|
4
4
|
# The namespace for Faulty storage
|
5
5
|
module Storage
|
6
6
|
end
|
7
7
|
end
|
8
8
|
|
9
|
+
require 'faulty/storage/auto_wire'
|
10
|
+
require 'faulty/storage/circuit_proxy'
|
11
|
+
require 'faulty/storage/fallback_chain'
|
9
12
|
require 'faulty/storage/fault_tolerant_proxy'
|
10
13
|
require 'faulty/storage/memory'
|
11
14
|
require 'faulty/storage/redis'
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Faulty
|
4
|
+
module Storage
|
5
|
+
# Automatically configure a storage backend
|
6
|
+
#
|
7
|
+
# Used by {Faulty#initialize} to setup sensible storage defaults
|
8
|
+
class AutoWire
|
9
|
+
# Options for {AutoWire}
|
10
|
+
#
|
11
|
+
# @!attribute [r] circuit
|
12
|
+
# @return [Circuit] A circuit for {CircuitProxy} if one is created.
|
13
|
+
# When modifying this, be careful to use only a reliable circuit
|
14
|
+
# storage backend so that you don't introduce cascading failures.
|
15
|
+
# @!attribute [r] notifier
|
16
|
+
# @return [Events::Notifier] A Faulty notifier. If given, listeners are
|
17
|
+
# ignored.
|
18
|
+
Options = Struct.new(
|
19
|
+
:circuit,
|
20
|
+
:notifier
|
21
|
+
) do
|
22
|
+
include ImmutableOptions
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def required
|
27
|
+
%i[notifier]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class << self
|
32
|
+
# Wrap storage backends with sensible defaults
|
33
|
+
#
|
34
|
+
# If the cache is `nil`, create a new {Memory} storage.
|
35
|
+
#
|
36
|
+
# If a single storage backend is given and is fault tolerant, leave it
|
37
|
+
# unmodified.
|
38
|
+
#
|
39
|
+
# If a single storage backend is given and is not fault tolerant, wrap it
|
40
|
+
# in a {CircuitProxy} and a {FaultTolerantProxy}.
|
41
|
+
#
|
42
|
+
# If an array of storage backends is given, wrap each non-fault-tolerant
|
43
|
+
# entry in a {CircuitProxy} and create a {FallbackChain}. If none of the
|
44
|
+
# backends in the array are fault tolerant, also wrap the {FallbackChain}
|
45
|
+
# in a {FaultTolerantProxy}.
|
46
|
+
#
|
47
|
+
# @todo Consider using a {FallbackChain} for non-fault-tolerant storages
|
48
|
+
# by default. This would fallback to a {Memory} storage. It would
|
49
|
+
# require a more conservative implementation of {Memory} that could
|
50
|
+
# limit the number of circuits stored. For now, users need to manually
|
51
|
+
# configure fallbacks.
|
52
|
+
#
|
53
|
+
# @param storage [Interface, Array<Interface>] A storage backed or array
|
54
|
+
# of storage backends to setup.
|
55
|
+
# @param options [Hash] Attributes for {Options}
|
56
|
+
# @yield [Options] For setting options in a block
|
57
|
+
def wrap(storage, **options, &block)
|
58
|
+
options = Options.new(options, &block)
|
59
|
+
if storage.nil?
|
60
|
+
Memory.new
|
61
|
+
elsif storage.is_a?(Array)
|
62
|
+
wrap_array(storage, options)
|
63
|
+
elsif !storage.fault_tolerant?
|
64
|
+
wrap_one(storage, options)
|
65
|
+
else
|
66
|
+
storage
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# Wrap an array of storage backends in a fault-tolerant FallbackChain
|
73
|
+
#
|
74
|
+
# @param [Array<Storage::Interface>] The array to wrap
|
75
|
+
# @param options [Options]
|
76
|
+
# @return [Storage::Interface] A fault-tolerant fallback chain
|
77
|
+
def wrap_array(array, options)
|
78
|
+
FaultTolerantProxy.wrap(FallbackChain.new(
|
79
|
+
array.map { |s| s.fault_tolerant? ? s : circuit_proxy(s, options) },
|
80
|
+
notifier: options.notifier
|
81
|
+
), notifier: options.notifier)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Wrap one storage backend in fault-tolerant backends
|
85
|
+
#
|
86
|
+
# @param [Storage::Interface] The storage to wrap
|
87
|
+
# @param options [Options]
|
88
|
+
# @return [Storage::Interface] A fault-tolerant storage backend
|
89
|
+
def wrap_one(storage, options)
|
90
|
+
FaultTolerantProxy.new(
|
91
|
+
circuit_proxy(storage, options),
|
92
|
+
notifier: options.notifier
|
93
|
+
)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Wrap storage in a CircuitProxy
|
97
|
+
#
|
98
|
+
# @param [Storage::Interface] The storage to wrap
|
99
|
+
# @param options [Options]
|
100
|
+
# @return [CircuitProxy]
|
101
|
+
def circuit_proxy(storage, options)
|
102
|
+
CircuitProxy.new(storage, circuit: options.circuit, notifier: options.notifier)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Faulty
|
4
|
+
module Storage
|
5
|
+
# A circuit wrapper for storage backends
|
6
|
+
#
|
7
|
+
# This class uses an internal {Circuit} to prevent the storage backend from
|
8
|
+
# causing application issues. If the backend fails continuously, this
|
9
|
+
# circuit will trip to prevent cascading failures. This internal circuit
|
10
|
+
# uses an independent in-memory backend by default.
|
11
|
+
class CircuitProxy
|
12
|
+
attr_reader :options
|
13
|
+
|
14
|
+
# Options for {CircuitProxy}
|
15
|
+
#
|
16
|
+
# @!attribute [r] circuit
|
17
|
+
# @return [Circuit] A replacement for the internal circuit. When
|
18
|
+
# modifying this, be careful to use only a reliable storage backend
|
19
|
+
# so that you don't introduce cascading failures.
|
20
|
+
# @!attribute [r] notifier
|
21
|
+
# @return [Events::Notifier] A Faulty notifier to use for circuit
|
22
|
+
# notifications. If `circuit` is given, this is ignored.
|
23
|
+
Options = Struct.new(
|
24
|
+
:circuit,
|
25
|
+
:notifier
|
26
|
+
) do
|
27
|
+
include ImmutableOptions
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def finalize
|
32
|
+
raise ArgumentError, 'The circuit or notifier option must be given' unless notifier || circuit
|
33
|
+
|
34
|
+
self.circuit ||= Circuit.new(
|
35
|
+
Faulty::Storage::CircuitProxy.name,
|
36
|
+
notifier: notifier,
|
37
|
+
cache: Cache::Null.new
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param storage [Storage::Interface] The storage backend to wrap
|
43
|
+
# @param options [Hash] Attributes for {Options}
|
44
|
+
# @yield [Options] For setting options in a block
|
45
|
+
def initialize(storage, **options, &block)
|
46
|
+
@storage = storage
|
47
|
+
@options = Options.new(options, &block)
|
48
|
+
end
|
49
|
+
|
50
|
+
%i[entry open reopen close lock unlock reset status history list].each do |method|
|
51
|
+
define_method(method) do |*args|
|
52
|
+
options.circuit.run { @storage.public_send(method, *args) }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# This cache makes any storage fault tolerant, so this is always `true`
|
57
|
+
#
|
58
|
+
# @return [true]
|
59
|
+
def fault_tolerant?
|
60
|
+
@storage.fault_tolerant?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Faulty
|
4
|
+
module Storage
|
5
|
+
# An prioritized list of storage backends
|
6
|
+
#
|
7
|
+
# If any backend fails, the next will be tried until one succeeds. This
|
8
|
+
# should typically be used when using a fault-prone backend such as
|
9
|
+
# {Storage::Redis}.
|
10
|
+
#
|
11
|
+
# This is used by {Faulty#initialize} if the `storage` option is set to an
|
12
|
+
# array.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# # This storage will try Redis first, then fallback to memory storage
|
16
|
+
# # if Redis is unavailable.
|
17
|
+
# storage = Faulty::Storage::FallbackChain.new([
|
18
|
+
# Faulty::Storage::Redis.new,
|
19
|
+
# Faulty::Storage::Memory.new
|
20
|
+
# ])
|
21
|
+
class FallbackChain
|
22
|
+
attr_reader :options
|
23
|
+
|
24
|
+
# Options for {FallbackChain}
|
25
|
+
#
|
26
|
+
# @!attribute [r] notifier
|
27
|
+
# @return [Events::Notifier] A Faulty notifier
|
28
|
+
Options = Struct.new(
|
29
|
+
:notifier
|
30
|
+
) do
|
31
|
+
include ImmutableOptions
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def required
|
36
|
+
%i[notifier]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Create a new {FallbackChain} to automatically fallback to reliable storage
|
41
|
+
#
|
42
|
+
# @param storages [Array<Storage::Interface>] An array of storage backends.
|
43
|
+
# The primary storage should be specified first. If that one fails,
|
44
|
+
# additional entries will be tried in sequence until one succeeds.
|
45
|
+
# @param options [Hash] Attributes for {Options}
|
46
|
+
# @yield [Options] For setting options in a block
|
47
|
+
def initialize(storages, **options, &block)
|
48
|
+
@storages = storages
|
49
|
+
@options = Options.new(options, &block)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Create a circuit entry in the first available storage backend
|
53
|
+
#
|
54
|
+
# @param (see Interface#entry)
|
55
|
+
# @return (see Interface#entry)
|
56
|
+
def entry(circuit, time, success)
|
57
|
+
send_chain(:entry, circuit, time, success) do |e|
|
58
|
+
options.notifier.notify(:storage_failure, circuit: circuit, action: :entry, error: e)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Open a circuit in the first available storage backend
|
63
|
+
#
|
64
|
+
# @param (see Interface#open)
|
65
|
+
# @return (see Interface#open)
|
66
|
+
def open(circuit, opened_at)
|
67
|
+
send_chain(:open, circuit, opened_at) do |e|
|
68
|
+
options.notifier.notify(:storage_failure, circuit: circuit, action: :open, error: e)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Reopen a circuit in the first available storage backend
|
73
|
+
#
|
74
|
+
# @param (see Interface#reopen)
|
75
|
+
# @return (see Interface#reopen)
|
76
|
+
def reopen(circuit, opened_at, previous_opened_at)
|
77
|
+
send_chain(:reopen, circuit, opened_at, previous_opened_at) do |e|
|
78
|
+
options.notifier.notify(:storage_failure, circuit: circuit, action: :reopen, error: e)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Close a circuit in the first available storage backend
|
83
|
+
#
|
84
|
+
# @param (see Interface#close)
|
85
|
+
# @return (see Interface#close)
|
86
|
+
def close(circuit)
|
87
|
+
send_chain(:close, circuit) do |e|
|
88
|
+
options.notifier.notify(:storage_failure, circuit: circuit, action: :close, error: e)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Lock a circuit in all storage backends
|
93
|
+
#
|
94
|
+
# @param (see Interface#lock)
|
95
|
+
# @return (see Interface#lock)
|
96
|
+
def lock(circuit, state)
|
97
|
+
send_all(:lock, circuit, state)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Unlock a circuit in all storage backends
|
101
|
+
#
|
102
|
+
# @param (see Interface#unlock)
|
103
|
+
# @return (see Interface#unlock)
|
104
|
+
def unlock(circuit)
|
105
|
+
send_all(:unlock, circuit)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Reset a circuit in all storage backends
|
109
|
+
#
|
110
|
+
# @param (see Interface#reset)
|
111
|
+
# @return (see Interface#reset)
|
112
|
+
def reset(circuit)
|
113
|
+
send_all(:reset, circuit)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Get the status of a circuit from the first available storage backend
|
117
|
+
#
|
118
|
+
# @param (see Interface#status)
|
119
|
+
# @return (see Interface#status)
|
120
|
+
def status(circuit)
|
121
|
+
send_chain(:status, circuit) do |e|
|
122
|
+
options.notifier.notify(:storage_failure, circuit: circuit, action: :status, error: e)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Get the history of a circuit from the first available storage backend
|
127
|
+
#
|
128
|
+
# @param (see Interface#history)
|
129
|
+
# @return (see Interface#history)
|
130
|
+
def history(circuit)
|
131
|
+
send_chain(:history, circuit) do |e|
|
132
|
+
options.notifier.notify(:storage_failure, circuit: circuit, action: :history, error: e)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Get the list of circuits from the first available storage backend
|
137
|
+
#
|
138
|
+
# @param (see Interface#list)
|
139
|
+
# @return (see Interface#list)
|
140
|
+
def list
|
141
|
+
send_chain(:list) do |e|
|
142
|
+
options.notifier.notify(:storage_failure, action: :list, error: e)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# This is fault tolerant if any of the available backends are fault tolerant
|
147
|
+
#
|
148
|
+
# @param (see Interface#fault_tolerant?)
|
149
|
+
# @return (see Interface#fault_tolerant?)
|
150
|
+
def fault_tolerant?
|
151
|
+
@storages.any?(&:fault_tolerant?)
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
# Call a method on the backend and return the first successful result
|
157
|
+
#
|
158
|
+
# Short-circuits, so that if a call succeeds, no additional backends are
|
159
|
+
# called.
|
160
|
+
#
|
161
|
+
# @param method [Symbol] The method to call
|
162
|
+
# @param args [Array] The arguments to send
|
163
|
+
# @raise [AllFailedError] AllFailedError if all backends fail
|
164
|
+
# @return The return value from the first successful call
|
165
|
+
def send_chain(method, *args)
|
166
|
+
errors = []
|
167
|
+
@storages.each do |s|
|
168
|
+
begin
|
169
|
+
return s.public_send(method, *args)
|
170
|
+
rescue StandardError => e
|
171
|
+
errors << e
|
172
|
+
yield e
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
raise AllFailedError.new("#{self.class}##{method} failed for all storage backends", errors)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Call a method on every backend
|
180
|
+
#
|
181
|
+
# @param method [Symbol] The method to call
|
182
|
+
# @param args [Array] The arguments to send
|
183
|
+
# @raise [AllFailedError] AllFailedError if all backends fail
|
184
|
+
# @raise [PartialFailureError] PartialFailureError if some but not all
|
185
|
+
# backends fail
|
186
|
+
# @return [nil]
|
187
|
+
def send_all(method, *args)
|
188
|
+
errors = []
|
189
|
+
@storages.each do |s|
|
190
|
+
begin
|
191
|
+
s.public_send(method, *args)
|
192
|
+
rescue StandardError => e
|
193
|
+
errors << e
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
if errors.empty?
|
198
|
+
nil
|
199
|
+
elsif errors.size < @storages.size
|
200
|
+
raise PartialFailureError.new("#{self.class}##{method} failed for some storage backends", errors)
|
201
|
+
else
|
202
|
+
raise AllFailedError.new("#{self.class}##{method} failed for all storage backends", errors)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|