faulty 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|