faulty 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/.travis.yml +2 -2
- data/CHANGELOG.md +9 -0
- data/README.md +157 -9
- data/bin/check-version +5 -1
- data/lib/faulty.rb +11 -17
- data/lib/faulty/cache.rb +2 -0
- data/lib/faulty/cache/auto_wire.rb +65 -0
- data/lib/faulty/cache/circuit_proxy.rb +61 -0
- data/lib/faulty/cache/default.rb +9 -20
- data/lib/faulty/cache/fault_tolerant_proxy.rb +13 -2
- data/lib/faulty/cache/rails.rb +8 -9
- data/lib/faulty/circuit.rb +9 -4
- data/lib/faulty/error.rb +14 -0
- data/lib/faulty/storage.rb +3 -0
- data/lib/faulty/storage/auto_wire.rb +122 -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 +49 -54
- data/lib/faulty/storage/memory.rb +6 -2
- data/lib/faulty/storage/redis.rb +66 -4
- data/lib/faulty/version.rb +1 -1
- metadata +7 -2
data/lib/faulty/cache/default.rb
CHANGED
@@ -11,6 +11,8 @@ class Faulty
|
|
11
11
|
# - If ActiveSupport is available, it will use an `ActiveSupport::Cache::MemoryStore`
|
12
12
|
# - Otherwise it will use a {Faulty::Cache::Null}
|
13
13
|
class Default
|
14
|
+
extend Forwardable
|
15
|
+
|
14
16
|
def initialize
|
15
17
|
@cache = if defined?(::Rails)
|
16
18
|
Cache::Rails.new(::Rails.cache)
|
@@ -21,28 +23,15 @@ class Faulty
|
|
21
23
|
end
|
22
24
|
end
|
23
25
|
|
24
|
-
#
|
26
|
+
# @!method read(key)
|
27
|
+
# (see Faulty::Cache::Interface#read)
|
25
28
|
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
def read(key)
|
29
|
-
@cache.read(key)
|
30
|
-
end
|
31
|
-
|
32
|
-
# Write to the internal cache
|
29
|
+
# @!method write(key, value, expires_in: expires_in)
|
30
|
+
# (see Faulty::Cache::Interface#write)
|
33
31
|
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
|
37
|
-
@cache.write(key, value, expires_in: expires_in)
|
38
|
-
end
|
39
|
-
|
40
|
-
# This cache is fault tolerant if the internal one is
|
41
|
-
#
|
42
|
-
# @return [Boolean]
|
43
|
-
def fault_tolerant?
|
44
|
-
@cache.fault_tolerant?
|
45
|
-
end
|
32
|
+
# @!method fault_tolerant
|
33
|
+
# (see Faulty::Cache::Interface#fault_tolerant?)
|
34
|
+
def_delegators :@cache, :read, :write, :fault_tolerant?
|
46
35
|
end
|
47
36
|
end
|
48
37
|
end
|
@@ -8,7 +8,8 @@ class Faulty
|
|
8
8
|
# this class.
|
9
9
|
#
|
10
10
|
# If the cache backend raises a `StandardError`, it will be captured and
|
11
|
-
# sent to the notifier.
|
11
|
+
# sent to the notifier. Reads errors will return `nil`, and writes will be
|
12
|
+
# a no-op.
|
12
13
|
class FaultTolerantProxy
|
13
14
|
attr_reader :options
|
14
15
|
|
@@ -36,6 +37,16 @@ class Faulty
|
|
36
37
|
@options = Options.new(options, &block)
|
37
38
|
end
|
38
39
|
|
40
|
+
# Wrap a cache in a FaultTolerantProxy unless it's already fault tolerant
|
41
|
+
#
|
42
|
+
# @param cache [Cache::Interface] The cache to maybe wrap
|
43
|
+
# @return [Cache::Interface] The original cache or a {FaultTolerantProxy}
|
44
|
+
def self.wrap(cache, **options, &block)
|
45
|
+
return cache if cache.fault_tolerant?
|
46
|
+
|
47
|
+
new(cache, **options, &block)
|
48
|
+
end
|
49
|
+
|
39
50
|
# Read from the cache safely
|
40
51
|
#
|
41
52
|
# If the backend raises a `StandardError`, this will return `nil`.
|
@@ -58,7 +69,7 @@ class Faulty
|
|
58
69
|
# @return [void]
|
59
70
|
def write(key, value, expires_in: nil)
|
60
71
|
@cache.write(key, value, expires_in: expires_in)
|
61
|
-
rescue StandardError
|
72
|
+
rescue StandardError => e
|
62
73
|
options.notifier.notify(:cache_failure, key: key, action: :write, error: e)
|
63
74
|
nil
|
64
75
|
end
|
data/lib/faulty/cache/rails.rb
CHANGED
@@ -5,6 +5,8 @@ class Faulty
|
|
5
5
|
# A wrapper for a Rails or ActiveSupport cache
|
6
6
|
#
|
7
7
|
class Rails
|
8
|
+
extend Forwardable
|
9
|
+
|
8
10
|
# @param cache The Rails cache to wrap
|
9
11
|
# @param fault_tolerant [Boolean] Whether the Rails cache is
|
10
12
|
# fault_tolerant. See {#fault_tolerant?} for more details
|
@@ -13,15 +15,12 @@ class Faulty
|
|
13
15
|
@fault_tolerant = fault_tolerant
|
14
16
|
end
|
15
17
|
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
def write(key, value, expires_in: nil)
|
23
|
-
@cache.write(key, value, expires_in: expires_in)
|
24
|
-
end
|
18
|
+
# @!method read(key)
|
19
|
+
# (see Faulty::Cache::Interface#read)
|
20
|
+
#
|
21
|
+
# @!method write(key, value, expires_in: expires_in)
|
22
|
+
# (see Faulty::Cache::Interface#write)
|
23
|
+
def_delegators :@cache, :read, :write
|
25
24
|
|
26
25
|
# Although ActiveSupport cache implementations are fault-tolerant,
|
27
26
|
# Rails.cache is not guranteed to be fault tolerant. For this reason,
|
data/lib/faulty/circuit.rb
CHANGED
@@ -66,14 +66,19 @@ class Faulty
|
|
66
66
|
# @return [Error, Array<Error>] An array of errors that are considered circuit
|
67
67
|
# failures. Default `[StandardError]`.
|
68
68
|
# @!attribute [r] exclude
|
69
|
-
# @return [Error, Array<Error>] An array of errors that will be
|
70
|
-
#
|
69
|
+
# @return [Error, Array<Error>] An array of errors that will not be
|
70
|
+
# captured by Faulty. These errors will not be considered circuit
|
71
|
+
# failures. Default `[]`.
|
71
72
|
# @!attribute [r] cache
|
72
|
-
# @return [Cache::Interface] The cache backend. Default
|
73
|
+
# @return [Cache::Interface] The cache backend. Default
|
74
|
+
# `Cache::Null.new`. Unlike {Faulty#initialize}, this is not wrapped in
|
75
|
+
# {Cache::AutoWire} by default.
|
73
76
|
# @!attribute [r] notifier
|
74
77
|
# @return [Events::Notifier] A Faulty notifier. Default `Events::Notifier.new`
|
75
78
|
# @!attribute [r] storage
|
76
|
-
# @return [Storage::Interface] The storage backend. Default
|
79
|
+
# @return [Storage::Interface] The storage backend. Default
|
80
|
+
# `Storage::Memory.new`. Unlike {Faulty#initialize}, this is not wrapped
|
81
|
+
# in {Storage::AutoWire} by default.
|
77
82
|
Options = Struct.new(
|
78
83
|
:cache_expires_in,
|
79
84
|
:cache_refreshes_after,
|
data/lib/faulty/error.rb
CHANGED
@@ -59,8 +59,22 @@ class Faulty
|
|
59
59
|
# Raised if calling get or error on a result without checking it
|
60
60
|
class UncheckedResultError < FaultyError; end
|
61
61
|
|
62
|
+
# An error that wraps multiple other errors
|
63
|
+
class FaultyMultiError < FaultyError
|
64
|
+
def initialize(message, errors)
|
65
|
+
message = "#{message}: #{errors.map(&:message).join(', ')}"
|
66
|
+
super(message)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
62
70
|
# Raised if getting the wrong result type.
|
63
71
|
#
|
64
72
|
# For example, calling get on an error result will raise this
|
65
73
|
class WrongResultError < FaultyError; end
|
74
|
+
|
75
|
+
# Raised if a FallbackChain partially fails
|
76
|
+
class PartialFailureError < FaultyMultiError; end
|
77
|
+
|
78
|
+
# Raised if all FallbackChain backends fail
|
79
|
+
class AllFailedError < FaultyMultiError; end
|
66
80
|
end
|
data/lib/faulty/storage.rb
CHANGED
@@ -6,6 +6,9 @@ class Faulty
|
|
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,122 @@
|
|
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
|
+
extend Forwardable
|
10
|
+
|
11
|
+
# Options for {AutoWire}
|
12
|
+
Options = Struct.new(
|
13
|
+
:notifier
|
14
|
+
) do
|
15
|
+
include ImmutableOptions
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def required
|
20
|
+
%i[notifier]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Wrap storage backends with sensible defaults
|
25
|
+
#
|
26
|
+
# If the cache is `nil`, create a new {Memory} storage.
|
27
|
+
#
|
28
|
+
# If a single storage backend is given and is fault tolerant, leave it
|
29
|
+
# unmodified.
|
30
|
+
#
|
31
|
+
# If a single storage backend is given and is not fault tolerant, wrap it
|
32
|
+
# in a {CircuitProxy} and a {FaultTolerantProxy}.
|
33
|
+
#
|
34
|
+
# If an array of storage backends is given, wrap each non-fault-tolerant
|
35
|
+
# entry in a {CircuitProxy} and create a {FallbackChain}. If none of the
|
36
|
+
# backends in the array are fault tolerant, also wrap the {FallbackChain}
|
37
|
+
# in a {FaultTolerantProxy}.
|
38
|
+
#
|
39
|
+
# @todo Consider using a {FallbackChain} for non-fault-tolerant storages
|
40
|
+
# by default. This would fallback to a {Memory} storage. It would
|
41
|
+
# require a more conservative implementation of {Memory} that could
|
42
|
+
# limit the number of circuits stored. For now, users need to manually
|
43
|
+
# configure fallbacks.
|
44
|
+
#
|
45
|
+
# @param storage [Interface, Array<Interface>] A storage backed or array
|
46
|
+
# of storage backends to setup.
|
47
|
+
# @param options [Hash] Attributes for {Options}
|
48
|
+
# @yield [Options] For setting options in a block
|
49
|
+
def initialize(storage, **options, &block)
|
50
|
+
@options = Options.new(options, &block)
|
51
|
+
@storage = if storage.nil?
|
52
|
+
Memory.new
|
53
|
+
elsif storage.is_a?(Array)
|
54
|
+
wrap_array(storage)
|
55
|
+
elsif !storage.fault_tolerant?
|
56
|
+
wrap_one(storage)
|
57
|
+
else
|
58
|
+
storage
|
59
|
+
end
|
60
|
+
|
61
|
+
freeze
|
62
|
+
end
|
63
|
+
|
64
|
+
# @!method entry(circuit, time, success)
|
65
|
+
# (see Faulty::Storage::Interface#entry)
|
66
|
+
#
|
67
|
+
# @!method open(circuit, opened_at)
|
68
|
+
# (see Faulty::Storage::Interface#open)
|
69
|
+
#
|
70
|
+
# @!method reopen(circuit, opened_at, previous_opened_at)
|
71
|
+
# (see Faulty::Storage::Interface#reopen)
|
72
|
+
#
|
73
|
+
# @!method close(circuit)
|
74
|
+
# (see Faulty::Storage::Interface#close)
|
75
|
+
#
|
76
|
+
# @!method lock(circuit, state)
|
77
|
+
# (see Faulty::Storage::Interface#lock)
|
78
|
+
#
|
79
|
+
# @!method unlock(circuit)
|
80
|
+
# (see Faulty::Storage::Interface#unlock)
|
81
|
+
#
|
82
|
+
# @!method reset(circuit)
|
83
|
+
# (see Faulty::Storage::Interface#reset)
|
84
|
+
#
|
85
|
+
# @!method status(circuit)
|
86
|
+
# (see Faulty::Storage::Interface#status)
|
87
|
+
#
|
88
|
+
# @!method history(circuit)
|
89
|
+
# (see Faulty::Storage::Interface#history)
|
90
|
+
#
|
91
|
+
# @!method list
|
92
|
+
# (see Faulty::Storage::Interface#list)
|
93
|
+
#
|
94
|
+
def_delegators :@storage,
|
95
|
+
:entry, :open, :reopen, :close, :lock,
|
96
|
+
:unlock, :reset, :status, :history, :list
|
97
|
+
|
98
|
+
def fault_tolerant?
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
# Wrap an array of storage backends in a fault-tolerant FallbackChain
|
105
|
+
#
|
106
|
+
# @return [Storage::Interface] A fault-tolerant fallback chain
|
107
|
+
def wrap_array(array)
|
108
|
+
FaultTolerantProxy.wrap(FallbackChain.new(
|
109
|
+
array.map { |s| s.fault_tolerant? ? s : CircuitProxy.new(s, notifier: @options.notifier) },
|
110
|
+
notifier: @options.notifier
|
111
|
+
), notifier: @options.notifier)
|
112
|
+
end
|
113
|
+
|
114
|
+
def wrap_one(storage)
|
115
|
+
FaultTolerantProxy.new(
|
116
|
+
CircuitProxy.new(storage, notifier: @options.notifier),
|
117
|
+
notifier: @options.notifier
|
118
|
+
)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
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
|