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
@@ -10,6 +10,8 @@ class Faulty
|
|
10
10
|
# If the storage backend raises a `StandardError`, it will be captured and
|
11
11
|
# sent to the notifier.
|
12
12
|
class FaultTolerantProxy
|
13
|
+
extend Forwardable
|
14
|
+
|
13
15
|
attr_reader :options
|
14
16
|
|
15
17
|
# Options for {FaultTolerantProxy}
|
@@ -36,6 +38,53 @@ class Faulty
|
|
36
38
|
@options = Options.new(options, &block)
|
37
39
|
end
|
38
40
|
|
41
|
+
# Wrap a storage backend in a FaultTolerantProxy unless it's already
|
42
|
+
# fault tolerant
|
43
|
+
#
|
44
|
+
# @param storage [Storage::Interface] The storage to maybe wrap
|
45
|
+
# @return [Storage::Interface] The original storage or a {FaultTolerantProxy}
|
46
|
+
def self.wrap(storage, **options, &block)
|
47
|
+
return storage if storage.fault_tolerant?
|
48
|
+
|
49
|
+
new(storage, **options, &block)
|
50
|
+
end
|
51
|
+
|
52
|
+
# @!method lock(circuit, state)
|
53
|
+
# Lock is not called in normal operation, so it doesn't capture errors
|
54
|
+
#
|
55
|
+
# @see Interface#lock
|
56
|
+
# @param (see Interface#lock)
|
57
|
+
# @return (see Interface#lock)
|
58
|
+
#
|
59
|
+
# @!method unlock(circuit)
|
60
|
+
# Unlock is not called in normal operation, so it doesn't capture errors
|
61
|
+
#
|
62
|
+
# @see Interface#unlock
|
63
|
+
# @param (see Interface#unlock)
|
64
|
+
# @return (see Interface#unlock)
|
65
|
+
#
|
66
|
+
# @!method reset(circuit)
|
67
|
+
# Reset is not called in normal operation, so it doesn't capture errors
|
68
|
+
#
|
69
|
+
# @see Interface#reset
|
70
|
+
# @param (see Interface#reset)
|
71
|
+
# @return (see Interface#reset)
|
72
|
+
#
|
73
|
+
# @!method history(circuit)
|
74
|
+
# History is not called in normal operation, so it doesn't capture errors
|
75
|
+
#
|
76
|
+
# @see Interface#history
|
77
|
+
# @param (see Interface#history)
|
78
|
+
# @return (see Interface#history)
|
79
|
+
#
|
80
|
+
# @!method list
|
81
|
+
# List is not called in normal operation, so it doesn't capture errors
|
82
|
+
#
|
83
|
+
# @see Interface#list
|
84
|
+
# @param (see Interface#list)
|
85
|
+
# @return (see Interface#list)
|
86
|
+
def_delegators :@storage, :lock, :unlock, :reset, :history, :list
|
87
|
+
|
39
88
|
# Add a history entry safely
|
40
89
|
#
|
41
90
|
# @see Interface#entry
|
@@ -84,36 +133,6 @@ class Faulty
|
|
84
133
|
false
|
85
134
|
end
|
86
135
|
|
87
|
-
# Since lock is not called in normal operation, it does not capture
|
88
|
-
# errors
|
89
|
-
#
|
90
|
-
# @see Interface#lock
|
91
|
-
# @param (see Interface#lock)
|
92
|
-
# @return (see Interface#lock)
|
93
|
-
def lock(circuit, state)
|
94
|
-
@storage.lock(circuit, state)
|
95
|
-
end
|
96
|
-
|
97
|
-
# Since unlock is not called in normal operation, it does not capture
|
98
|
-
# errors
|
99
|
-
#
|
100
|
-
# @see Interface#unlock
|
101
|
-
# @param (see Interface#unlock)
|
102
|
-
# @return (see Interface#unlock)
|
103
|
-
def unlock(circuit)
|
104
|
-
@storage.unlock(circuit)
|
105
|
-
end
|
106
|
-
|
107
|
-
# Since reset is not called in normal operation, it does not capture
|
108
|
-
# errors
|
109
|
-
#
|
110
|
-
# @see Interface#reset
|
111
|
-
# @param (see Interface#reset)
|
112
|
-
# @return (see Interface#reset)
|
113
|
-
def reset(circuit)
|
114
|
-
@storage.reset(circuit)
|
115
|
-
end
|
116
|
-
|
117
136
|
# Safely get the status of a circuit
|
118
137
|
#
|
119
138
|
# If the backend is unavailable, this returns a stub status that
|
@@ -129,30 +148,6 @@ class Faulty
|
|
129
148
|
stub_status(circuit)
|
130
149
|
end
|
131
150
|
|
132
|
-
# Since history is not called in normal operation, it does not capture
|
133
|
-
# errors
|
134
|
-
#
|
135
|
-
# @see Interface#history
|
136
|
-
# @param (see Interface#history)
|
137
|
-
# @return (see Interface#history)
|
138
|
-
def history(circuit)
|
139
|
-
@storage.history(circuit)
|
140
|
-
end
|
141
|
-
|
142
|
-
# Safely get the list of circuit names
|
143
|
-
#
|
144
|
-
# If the backend is unavailable, this returns an empty array
|
145
|
-
#
|
146
|
-
# @see Interface#list
|
147
|
-
# @param (see Interface#list)
|
148
|
-
# @return (see Interface#list)
|
149
|
-
def list
|
150
|
-
@storage.list
|
151
|
-
rescue StandardError => e
|
152
|
-
options.notifier.notify(:storage_failure, action: :list, error: e)
|
153
|
-
[]
|
154
|
-
end
|
155
|
-
|
156
151
|
# This cache makes any storage fault tolerant, so this is always `true`
|
157
152
|
#
|
158
153
|
# @return [true]
|
@@ -4,8 +4,9 @@ class Faulty
|
|
4
4
|
module Storage
|
5
5
|
# The default in-memory storage for circuits
|
6
6
|
#
|
7
|
-
# This implementation is
|
8
|
-
#
|
7
|
+
# This implementation is thread-safe and circuit state is shared across
|
8
|
+
# threads. Since state is stored in-memory, this state is not shared across
|
9
|
+
# processes, or persisted across application restarts.
|
9
10
|
#
|
10
11
|
# Circuit state and runs are stored in memory. Although runs have a maximum
|
11
12
|
# size within a circuit, there is no limit on the number of circuits that
|
@@ -18,6 +19,9 @@ class Faulty
|
|
18
19
|
#
|
19
20
|
# This can be used as a reference implementation for storage backends that
|
20
21
|
# store a list of circuit run entries.
|
22
|
+
#
|
23
|
+
# @todo Add a more sophsticated implmentation that can limit the number of
|
24
|
+
# circuits stored.
|
21
25
|
class Memory
|
22
26
|
attr_reader :options
|
23
27
|
|
data/lib/faulty/storage/redis.rb
CHANGED
@@ -2,6 +2,13 @@
|
|
2
2
|
|
3
3
|
class Faulty
|
4
4
|
module Storage
|
5
|
+
# A storage backend for storing circuit state in Redis.
|
6
|
+
#
|
7
|
+
# When using this or any networked backend, be sure to evaluate the risk,
|
8
|
+
# and set conservative timeouts so that the circuit storage does not cause
|
9
|
+
# cascading failures in your application when evaluating circuits. Always
|
10
|
+
# wrap this backend with a {FaultTolerantProxy} to limit the effect of
|
11
|
+
# these types of events.
|
5
12
|
class Redis # rubocop:disable Metrics/ClassLength
|
6
13
|
# Separates the time/status for history entry strings
|
7
14
|
ENTRY_SEPARATOR = ':'
|
@@ -27,12 +34,17 @@ class Faulty
|
|
27
34
|
# circuit run history entry. Default `100`.
|
28
35
|
# @!attribute [r] circuit_ttl
|
29
36
|
# @return [Integer] The maximum number of seconds to keep a circuit.
|
30
|
-
# A value of `nil` disables circuit expiration.
|
37
|
+
# A value of `nil` disables circuit expiration. This does not apply to
|
38
|
+
# locks, which have an indefinite storage time.
|
31
39
|
# Default `604_800` (1 week).
|
32
40
|
# @!attribute [r] list_granularity
|
33
41
|
# @return [Integer] The number of seconds after which a new set is
|
34
42
|
# created to store circuit names. The old set is kept until
|
35
43
|
# circuit_ttl expires. Default `3600` (1 hour).
|
44
|
+
# @!attribute [r] disable_warnings
|
45
|
+
# @return [Boolean] By default, this class warns if the client options
|
46
|
+
# are outside the recommended values. Set to true to disable these
|
47
|
+
# warnings.
|
36
48
|
Options = Struct.new(
|
37
49
|
:client,
|
38
50
|
:key_prefix,
|
@@ -40,7 +52,8 @@ class Faulty
|
|
40
52
|
:max_sample_size,
|
41
53
|
:sample_ttl,
|
42
54
|
:circuit_ttl,
|
43
|
-
:list_granularity
|
55
|
+
:list_granularity,
|
56
|
+
:disable_warnings
|
44
57
|
) do
|
45
58
|
include ImmutableOptions
|
46
59
|
|
@@ -53,7 +66,8 @@ class Faulty
|
|
53
66
|
max_sample_size: 100,
|
54
67
|
sample_ttl: 1800,
|
55
68
|
circuit_ttl: 604_800,
|
56
|
-
list_granularity: 3600
|
69
|
+
list_granularity: 3600,
|
70
|
+
disable_warnings: false
|
57
71
|
}
|
58
72
|
end
|
59
73
|
|
@@ -62,7 +76,7 @@ class Faulty
|
|
62
76
|
end
|
63
77
|
|
64
78
|
def finalize
|
65
|
-
self.client = ::Redis.new unless client
|
79
|
+
self.client = ::Redis.new(timeout: 1) unless client
|
66
80
|
end
|
67
81
|
end
|
68
82
|
|
@@ -70,6 +84,8 @@ class Faulty
|
|
70
84
|
# @yield [Options] For setting options in a block
|
71
85
|
def initialize(**options, &block)
|
72
86
|
@options = Options.new(options, &block)
|
87
|
+
|
88
|
+
check_client_options!
|
73
89
|
end
|
74
90
|
|
75
91
|
# Add an entry to storage
|
@@ -196,6 +212,9 @@ class Faulty
|
|
196
212
|
map_entries(entries).reverse
|
197
213
|
end
|
198
214
|
|
215
|
+
# List all unexpired circuits
|
216
|
+
#
|
217
|
+
# @return (see Interface#list)
|
199
218
|
def list
|
200
219
|
redis { |r| r.sunion(*all_list_keys) }
|
201
220
|
end
|
@@ -330,6 +349,49 @@ class Faulty
|
|
330
349
|
[time.to_i, state == '1']
|
331
350
|
end
|
332
351
|
end
|
352
|
+
|
353
|
+
def check_client_options!
|
354
|
+
return if options.disable_warnings
|
355
|
+
|
356
|
+
check_redis_options!
|
357
|
+
check_pool_options!
|
358
|
+
rescue StandardError => e
|
359
|
+
warn "Faulty error while checking client options: #{e.message}"
|
360
|
+
end
|
361
|
+
|
362
|
+
def check_redis_options!
|
363
|
+
ropts = redis { |r| r.client.options }
|
364
|
+
|
365
|
+
bad_timeouts = {}
|
366
|
+
%i[connect_timeout read_timeout write_timeout].each do |time_opt|
|
367
|
+
bad_timeouts[time_opt] = ropts[time_opt] if ropts[time_opt] > 2
|
368
|
+
end
|
369
|
+
|
370
|
+
unless bad_timeouts.empty?
|
371
|
+
warn <<~MSG
|
372
|
+
Faulty recommends setting Redis timeouts <= 2 to prevent cascading
|
373
|
+
failures when evaluating circuits. Your options are:
|
374
|
+
#{bad_timeouts}
|
375
|
+
MSG
|
376
|
+
end
|
377
|
+
|
378
|
+
if ropts[:reconnect_attempts] > 1
|
379
|
+
warn <<~MSG
|
380
|
+
Faulty recommends setting Redis reconnect_attempts to <= 1 to
|
381
|
+
prevent cascading failures. Your setting is #{ropts[:reconnect_attempts]}
|
382
|
+
MSG
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
def check_pool_options!
|
387
|
+
if options.client.is_a?(ConnectionPool)
|
388
|
+
timeout = options.client.instance_variable_get(:@timeout)
|
389
|
+
warn(<<~MSG) if timeout > 2
|
390
|
+
Faulty recommends setting ConnectionPool timeouts <= 2 to prevent
|
391
|
+
cascading failures when evaluating circuits. Your setting is #{timeout}
|
392
|
+
MSG
|
393
|
+
end
|
394
|
+
end
|
333
395
|
end
|
334
396
|
end
|
335
397
|
end
|
data/lib/faulty/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: faulty
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Howard
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-10-
|
11
|
+
date: 2020-10-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -148,6 +148,8 @@ files:
|
|
148
148
|
- faulty.gemspec
|
149
149
|
- lib/faulty.rb
|
150
150
|
- lib/faulty/cache.rb
|
151
|
+
- lib/faulty/cache/auto_wire.rb
|
152
|
+
- lib/faulty/cache/circuit_proxy.rb
|
151
153
|
- lib/faulty/cache/default.rb
|
152
154
|
- lib/faulty/cache/fault_tolerant_proxy.rb
|
153
155
|
- lib/faulty/cache/interface.rb
|
@@ -166,6 +168,9 @@ files:
|
|
166
168
|
- lib/faulty/result.rb
|
167
169
|
- lib/faulty/status.rb
|
168
170
|
- lib/faulty/storage.rb
|
171
|
+
- lib/faulty/storage/auto_wire.rb
|
172
|
+
- lib/faulty/storage/circuit_proxy.rb
|
173
|
+
- lib/faulty/storage/fallback_chain.rb
|
169
174
|
- lib/faulty/storage/fault_tolerant_proxy.rb
|
170
175
|
- lib/faulty/storage/interface.rb
|
171
176
|
- lib/faulty/storage/memory.rb
|