faulty 0.3.0 → 0.4.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/.github/workflows/ci.yml +49 -0
- data/CHANGELOG.md +16 -0
- data/Gemfile +8 -3
- data/README.md +741 -403
- data/faulty.gemspec +1 -1
- data/lib/faulty.rb +13 -5
- data/lib/faulty/cache/auto_wire.rb +32 -39
- data/lib/faulty/events/log_listener.rb +4 -5
- data/lib/faulty/status.rb +2 -1
- data/lib/faulty/storage/auto_wire.rb +77 -92
- data/lib/faulty/storage/redis.rb +2 -2
- data/lib/faulty/version.rb +1 -1
- metadata +7 -6
- data/.travis.yml +0 -46
data/faulty.gemspec
CHANGED
@@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
|
|
27
27
|
# Other non-essential development dependencies go in the Gemfile.
|
28
28
|
spec.add_development_dependency 'connection_pool', '~> 2.0'
|
29
29
|
spec.add_development_dependency 'honeybadger', '>= 2.0'
|
30
|
-
spec.add_development_dependency 'redis', '
|
30
|
+
spec.add_development_dependency 'redis', '>= 3.0'
|
31
31
|
spec.add_development_dependency 'rspec', '~> 3.8'
|
32
32
|
# 0.81 is the last rubocop version with Ruby 2.3 support
|
33
33
|
spec.add_development_dependency 'rubocop', '0.81.0'
|
data/lib/faulty.rb
CHANGED
@@ -129,6 +129,10 @@ class Faulty
|
|
129
129
|
# @return [Cache::Interface] A cache backend if you want
|
130
130
|
# to use Faulty's cache support. Automatically wrapped in a
|
131
131
|
# {Cache::AutoWire}. Default `Cache::AutoWire.new`.
|
132
|
+
# @!attribute [r] circuit_defaults
|
133
|
+
# @see Circuit::Options
|
134
|
+
# @return [Hash] A hash of default options to be used when creating
|
135
|
+
# new circuits. See {Circuit::Options} for a full list.
|
132
136
|
# @!attribute [r] storage
|
133
137
|
# @see Storage::AutoWire
|
134
138
|
# @return [Storage::Interface, Array<Storage::Interface>] The storage
|
@@ -142,6 +146,7 @@ class Faulty
|
|
142
146
|
# ignored.
|
143
147
|
Options = Struct.new(
|
144
148
|
:cache,
|
149
|
+
:circuit_defaults,
|
145
150
|
:storage,
|
146
151
|
:listeners,
|
147
152
|
:notifier
|
@@ -152,16 +157,17 @@ class Faulty
|
|
152
157
|
|
153
158
|
def finalize
|
154
159
|
self.notifier ||= Events::Notifier.new(listeners || [])
|
155
|
-
self.storage = Storage::AutoWire.
|
156
|
-
self.cache = Cache::AutoWire.
|
160
|
+
self.storage = Storage::AutoWire.wrap(storage, notifier: notifier)
|
161
|
+
self.cache = Cache::AutoWire.wrap(cache, notifier: notifier)
|
157
162
|
end
|
158
163
|
|
159
164
|
def required
|
160
|
-
%i[cache storage notifier]
|
165
|
+
%i[cache circuit_defaults storage notifier]
|
161
166
|
end
|
162
167
|
|
163
168
|
def defaults
|
164
169
|
{
|
170
|
+
circuit_defaults: {},
|
165
171
|
listeners: [Events::LogListener.new]
|
166
172
|
}
|
167
173
|
end
|
@@ -200,8 +206,8 @@ class Faulty
|
|
200
206
|
# @return [Circuit] The new circuit or the existing circuit if it already exists
|
201
207
|
def circuit(name, **options, &block)
|
202
208
|
name = name.to_s
|
203
|
-
options = options.merge(circuit_options)
|
204
209
|
@circuits.compute_if_absent(name) do
|
210
|
+
options = circuit_options.merge(options)
|
205
211
|
Circuit.new(name, **options, &block)
|
206
212
|
end
|
207
213
|
end
|
@@ -219,6 +225,8 @@ class Faulty
|
|
219
225
|
#
|
220
226
|
# @return [Hash] The circuit options
|
221
227
|
def circuit_options
|
222
|
-
@options.to_h
|
228
|
+
@options.to_h
|
229
|
+
.select { |k, _v| %i[cache storage notifier].include?(k) }
|
230
|
+
.merge(options.circuit_defaults)
|
223
231
|
end
|
224
232
|
end
|
@@ -6,10 +6,17 @@ class Faulty
|
|
6
6
|
#
|
7
7
|
# Used by {Faulty#initialize} to setup sensible cache defaults
|
8
8
|
class AutoWire
|
9
|
-
extend Forwardable
|
10
|
-
|
11
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.
|
12
18
|
Options = Struct.new(
|
19
|
+
:circuit,
|
13
20
|
:notifier
|
14
21
|
) do
|
15
22
|
include ImmutableOptions
|
@@ -21,44 +28,30 @@ class Faulty
|
|
21
28
|
end
|
22
29
|
end
|
23
30
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
cache
|
40
|
-
|
41
|
-
|
42
|
-
Cache::
|
43
|
-
|
44
|
-
|
31
|
+
class << self
|
32
|
+
# Wrap a cache backend with sensible defaults
|
33
|
+
#
|
34
|
+
# If the cache is `nil`, create a new {Default}.
|
35
|
+
#
|
36
|
+
# If the backend is not fault tolerant, wrap it in {CircuitProxy} and
|
37
|
+
# {FaultTolerantProxy}.
|
38
|
+
#
|
39
|
+
# @param cache [Interface] A cache backend
|
40
|
+
# @param options [Hash] Attributes for {Options}
|
41
|
+
# @yield [Options] For setting options in a block
|
42
|
+
def wrap(cache, **options, &block)
|
43
|
+
options = Options.new(options, &block)
|
44
|
+
if cache.nil?
|
45
|
+
Cache::Default.new
|
46
|
+
elsif cache.fault_tolerant?
|
47
|
+
cache
|
48
|
+
else
|
49
|
+
Cache::FaultTolerantProxy.new(
|
50
|
+
Cache::CircuitProxy.new(cache, circuit: options.circuit, notifier: options.notifier),
|
51
|
+
notifier: options.notifier
|
52
|
+
)
|
53
|
+
end
|
45
54
|
end
|
46
|
-
|
47
|
-
freeze
|
48
|
-
end
|
49
|
-
|
50
|
-
# @!method read(key)
|
51
|
-
# (see Faulty::Cache::Interface#read)
|
52
|
-
#
|
53
|
-
# @!method write(key, value, expires_in: expires_in)
|
54
|
-
# (see Faulty::Cache::Interface#write)
|
55
|
-
def_delegators :@cache, :read, :write
|
56
|
-
|
57
|
-
# Auto-wired caches are always fault tolerant
|
58
|
-
#
|
59
|
-
# @return [true]
|
60
|
-
def fault_tolerant?
|
61
|
-
true
|
62
55
|
end
|
63
56
|
end
|
64
57
|
end
|
@@ -72,11 +72,10 @@ class Faulty
|
|
72
72
|
end
|
73
73
|
|
74
74
|
def storage_failure(payload)
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
)
|
75
|
+
extra = {}
|
76
|
+
extra[:circuit] = payload[:circuit].name if payload.key?(:circuit)
|
77
|
+
extra[:error] = payload[:error].message
|
78
|
+
log(:error, 'Storage failure', payload[:action], extra)
|
80
79
|
end
|
81
80
|
|
82
81
|
def log(level, msg, action, extra = {})
|
data/lib/faulty/status.rb
CHANGED
@@ -144,9 +144,10 @@ class 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
|
@@ -6,10 +6,17 @@ class Faulty
|
|
6
6
|
#
|
7
7
|
# Used by {Faulty#initialize} to setup sensible storage defaults
|
8
8
|
class AutoWire
|
9
|
-
extend Forwardable
|
10
|
-
|
11
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.
|
12
18
|
Options = Struct.new(
|
19
|
+
:circuit,
|
13
20
|
:notifier
|
14
21
|
) do
|
15
22
|
include ImmutableOptions
|
@@ -21,101 +28,79 @@ class Faulty
|
|
21
28
|
end
|
22
29
|
end
|
23
30
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
59
68
|
end
|
60
69
|
|
61
|
-
|
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
|
70
|
+
private
|
101
71
|
|
102
|
-
|
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
|
103
83
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
113
95
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
119
104
|
end
|
120
105
|
end
|
121
106
|
end
|
data/lib/faulty/storage/redis.rb
CHANGED
@@ -360,7 +360,7 @@ class Faulty
|
|
360
360
|
end
|
361
361
|
|
362
362
|
def check_redis_options!
|
363
|
-
ropts = redis { |r| r.client.options }
|
363
|
+
ropts = redis { |r| r.instance_variable_get(:@client).options }
|
364
364
|
|
365
365
|
bad_timeouts = {}
|
366
366
|
%i[connect_timeout read_timeout write_timeout].each do |time_opt|
|
@@ -384,7 +384,7 @@ class Faulty
|
|
384
384
|
end
|
385
385
|
|
386
386
|
def check_pool_options!
|
387
|
-
if options.client.
|
387
|
+
if options.client.class.name == 'ConnectionPool'
|
388
388
|
timeout = options.client.instance_variable_get(:@timeout)
|
389
389
|
warn(<<~MSG) if timeout > 2
|
390
390
|
Faulty recommends setting ConnectionPool timeouts <= 2 to prevent
|
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.4.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:
|
11
|
+
date: 2021-02-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -56,14 +56,14 @@ dependencies:
|
|
56
56
|
name: redis
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '3.0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '3.0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
@@ -129,10 +129,10 @@ executables: []
|
|
129
129
|
extensions: []
|
130
130
|
extra_rdoc_files: []
|
131
131
|
files:
|
132
|
+
- ".github/workflows/ci.yml"
|
132
133
|
- ".gitignore"
|
133
134
|
- ".rspec"
|
134
135
|
- ".rubocop.yml"
|
135
|
-
- ".travis.yml"
|
136
136
|
- ".yardopts"
|
137
137
|
- CHANGELOG.md
|
138
138
|
- Gemfile
|
@@ -195,7 +195,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
195
195
|
- !ruby/object:Gem::Version
|
196
196
|
version: '0'
|
197
197
|
requirements: []
|
198
|
-
|
198
|
+
rubyforge_project:
|
199
|
+
rubygems_version: 2.7.6
|
199
200
|
signing_key:
|
200
201
|
specification_version: 4
|
201
202
|
summary: Fault-tolerance tools for ruby based on circuit-breakers
|