faulty 0.4.0 → 0.5.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/CHANGELOG.md +13 -0
- data/README.md +67 -0
- data/lib/faulty.rb +14 -4
- data/lib/faulty/circuit.rb +21 -11
- data/lib/faulty/error.rb +11 -3
- 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/storage/interface.rb +2 -1
- data/lib/faulty/storage/memory.rb +1 -1
- data/lib/faulty/storage/redis.rb +3 -3
- data/lib/faulty/version.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f8ce491fe09e0c6224c3ecc6da2825eeff601f15f7b5bbfbff065f898860fc48
|
4
|
+
data.tar.gz: caf56c1bfc86511d0acb6fbd3f7507d521bcd7b8ac74b47a85ebcbd9a4fdef1b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 89eb97edae88432ea0e6e36a9c0b0f69d4d89075193ffd5a6eb0fe10e24c0983047289ccdb8e363619c4f83b1e80baec9decc34e4fdf029cfbef24189f244138
|
7
|
+
data.tar.gz: 4cabda8638d452bcac0a6375b284b4465d7eb34ed5e4b546a7247c09816eae4480620d69f9777144bd3d3aa8abcc58ca83243037e5d7aac0635e6d01d45ff289
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
## Release v0.5.0
|
2
|
+
|
3
|
+
* Allow creating a new Faulty instance in Faulty#register #24 justinhoward
|
4
|
+
* Add support for patches to core dependencies starting with redis #14 justinhoward
|
5
|
+
* Improve storage #entries performance by returning entries #23 justinhoward
|
6
|
+
|
7
|
+
### Breaking Changes
|
8
|
+
|
9
|
+
* Faulty #[] no longer differentiates between symbols and strings when accessing
|
10
|
+
Faulty instances
|
11
|
+
* Faulty::Storage::Interface must now return a history array instead of a
|
12
|
+
circuit status object. Custom storage backends must be updated.
|
13
|
+
|
1
14
|
## Release v0.4.0
|
2
15
|
|
3
16
|
* Switch from Travis CI to GitHub actions #11 justinhoward
|
data/README.md
CHANGED
@@ -81,6 +81,8 @@ Also see "Release It!: Design and Deploy Production-Ready Software" by
|
|
81
81
|
+ [Circuit Options](#circuit-options)
|
82
82
|
+ [Listing Circuits](#listing-circuits)
|
83
83
|
+ [Locking Circuits](#locking-circuits)
|
84
|
+
* [Patches](#patches)
|
85
|
+
+ [Patch::Redis](#patchredis)
|
84
86
|
* [Event Handling](#event-handling)
|
85
87
|
+ [CallbackListener](#callbacklistener)
|
86
88
|
+ [Other Built-in Listeners](#other-built-in-listeners)
|
@@ -221,6 +223,10 @@ users = Faulty.circuit(:api).try_run do
|
|
221
223
|
end.or_default([])
|
222
224
|
```
|
223
225
|
|
226
|
+
If you want to globally wrap your core dependencies, like your cache or
|
227
|
+
database, you may want to look at [Patches](#patches), which can automatically
|
228
|
+
wrap your connections in a Faulty circuit.
|
229
|
+
|
224
230
|
See [Running a Circuit](#running-a-circuit) for more in-depth examples. Also,
|
225
231
|
make sure you have proper [Event Handlers](#event-handling) setup so that you
|
226
232
|
can monitor your circuits for failures.
|
@@ -580,6 +586,14 @@ principal to any other registered Faulty instance:
|
|
580
586
|
Faulty[:api].circuit('api_circuit').run { 'ok' }
|
581
587
|
```
|
582
588
|
|
589
|
+
You can also create and register a Faulty instance in one step:
|
590
|
+
|
591
|
+
```ruby
|
592
|
+
Faulty.register(:api) do |config|
|
593
|
+
# This accepts the same options as Faulty.init
|
594
|
+
end
|
595
|
+
```
|
596
|
+
|
583
597
|
#### Standalone Instances
|
584
598
|
|
585
599
|
If you choose, you can use Faulty instances without registering them globally by
|
@@ -915,6 +929,59 @@ Locking or unlocking a circuit has no concurrency guarantees, so it's not
|
|
915
929
|
recommended to lock or unlock circuits from production code. Instead, locks are
|
916
930
|
intended as an emergency tool for troubleshooting and debugging.
|
917
931
|
|
932
|
+
## Patches
|
933
|
+
|
934
|
+
For certain core dependencies like a cache or a database connection, it is
|
935
|
+
inconvenient to wrap every call in its own circuit. Faulty provides some patches
|
936
|
+
to wrap these calls in a circuit automatically. To use a patch, it first needs
|
937
|
+
to be loaded. Since patches modify third-party code, they are not automatically
|
938
|
+
required with the Faulty gem, so they need to be required individually.
|
939
|
+
|
940
|
+
```ruby
|
941
|
+
require 'faulty'
|
942
|
+
require 'faulty/patch/redis'
|
943
|
+
```
|
944
|
+
|
945
|
+
Or require them in your `Gemfile`
|
946
|
+
|
947
|
+
```ruby
|
948
|
+
gem 'faulty', require: %w[faulty faulty/patch/redis]
|
949
|
+
```
|
950
|
+
|
951
|
+
### Patch::Redis
|
952
|
+
|
953
|
+
[`Faulty::Patch::Redis`](https://www.rubydoc.info/gems/faulty/Faulty/Patch/Redis)
|
954
|
+
protects a Redis client with an internal circuit. Pass a `:faulty` key along
|
955
|
+
with your connection options to enable the circuit breaker.
|
956
|
+
|
957
|
+
Keep in mind that when using this patch, you'll most likely want to use the
|
958
|
+
in-memory circuit storage adapter and not the Redis storage adapter. That way
|
959
|
+
if Redis fails, your circuit storage doesn't also fail.
|
960
|
+
|
961
|
+
```ruby
|
962
|
+
require 'faulty/patch/redis'
|
963
|
+
|
964
|
+
redis = Redis.new(url: 'redis://localhost:6379', faulty: {
|
965
|
+
# The name for the redis circuit
|
966
|
+
name: 'redis'
|
967
|
+
|
968
|
+
# The faulty instance to use
|
969
|
+
# This can also be a registered faulty instance or a constant name. See API
|
970
|
+
# docs for more details
|
971
|
+
instance: Faulty.default
|
972
|
+
|
973
|
+
# By default, circuit errors will be subclasses of Redis::BaseError
|
974
|
+
# To disable this behavior, set patch_errors to false and Faulty
|
975
|
+
# will raise its default errors
|
976
|
+
patch_errors: true
|
977
|
+
})
|
978
|
+
redis.connect # raises Faulty::CircuitError if connection fails
|
979
|
+
|
980
|
+
# If the faulty key is not given, no circuit is used
|
981
|
+
redis = Redis.new(url: 'redis://localhost:6379')
|
982
|
+
redis.connect # not protected by a circuit
|
983
|
+
```
|
984
|
+
|
918
985
|
## Event Handling
|
919
986
|
|
920
987
|
Faulty uses an event-dispatching model to deliver notifications of internal
|
data/lib/faulty.rb
CHANGED
@@ -9,6 +9,7 @@ require 'faulty/cache'
|
|
9
9
|
require 'faulty/circuit'
|
10
10
|
require 'faulty/error'
|
11
11
|
require 'faulty/events'
|
12
|
+
require 'faulty/patch'
|
12
13
|
require 'faulty/result'
|
13
14
|
require 'faulty/status'
|
14
15
|
require 'faulty/storage'
|
@@ -66,7 +67,7 @@ class Faulty
|
|
66
67
|
def [](name)
|
67
68
|
raise UninitializedError unless @instances
|
68
69
|
|
69
|
-
@instances[name]
|
70
|
+
@instances[name.to_s]
|
70
71
|
end
|
71
72
|
|
72
73
|
# Register an instance to the global Faulty state
|
@@ -75,13 +76,22 @@ class Faulty
|
|
75
76
|
# return value if you need to know whether the instance already existed.
|
76
77
|
#
|
77
78
|
# @param name [Symbol] The name of the instance to register
|
78
|
-
# @param instance [Faulty] The instance to register
|
79
|
+
# @param instance [Faulty] The instance to register. If nil, a new instance
|
80
|
+
# will be created from the given options or block.
|
81
|
+
# @param config [Hash] Attributes for {Faulty::Options}
|
82
|
+
# @yield [Faulty::Options] For setting options in a block
|
79
83
|
# @return [Faulty, nil] The previously-registered instance of that name if
|
80
84
|
# it already existed, otherwise nil.
|
81
|
-
def register(name, instance)
|
85
|
+
def register(name, instance = nil, **config, &block)
|
82
86
|
raise UninitializedError unless @instances
|
83
87
|
|
84
|
-
|
88
|
+
if instance
|
89
|
+
raise ArgumentError, 'Do not give config options if an instance is given' if !config.empty? || block
|
90
|
+
else
|
91
|
+
instance = new(**config, &block)
|
92
|
+
end
|
93
|
+
|
94
|
+
@instances.put_if_absent(name.to_s, instance)
|
85
95
|
end
|
86
96
|
|
87
97
|
# Get the options for the default instance
|
data/lib/faulty/circuit.rb
CHANGED
@@ -50,6 +50,9 @@ class Faulty
|
|
50
50
|
# @!attribute [r] cool_down
|
51
51
|
# @return [Integer] The number of seconds the circuit will
|
52
52
|
# stay open after it is tripped. Default 300.
|
53
|
+
# @!attribute [r] error_module
|
54
|
+
# @return [Module] Used by patches to set the namespace module for
|
55
|
+
# the faulty errors that will be raised. Default `Faulty`
|
53
56
|
# @!attribute [r] evaluation_window
|
54
57
|
# @return [Integer] The number of seconds of history that
|
55
58
|
# will be evaluated to determine the failure rate for a circuit.
|
@@ -88,6 +91,7 @@ class Faulty
|
|
88
91
|
:rate_threshold,
|
89
92
|
:sample_threshold,
|
90
93
|
:errors,
|
94
|
+
:error_module,
|
91
95
|
:exclude,
|
92
96
|
:cache,
|
93
97
|
:notifier,
|
@@ -103,6 +107,7 @@ class Faulty
|
|
103
107
|
cache_refreshes_after: 900,
|
104
108
|
cool_down: 300,
|
105
109
|
errors: [StandardError],
|
110
|
+
error_module: Faulty,
|
106
111
|
exclude: [],
|
107
112
|
evaluation_window: 60,
|
108
113
|
rate_threshold: 0.5,
|
@@ -115,6 +120,7 @@ class Faulty
|
|
115
120
|
cache
|
116
121
|
cool_down
|
117
122
|
errors
|
123
|
+
error_module
|
118
124
|
exclude
|
119
125
|
evaluation_window
|
120
126
|
rate_threshold
|
@@ -213,9 +219,11 @@ class Faulty
|
|
213
219
|
cached_value = cache_read(cache)
|
214
220
|
# return cached unless cached.nil?
|
215
221
|
return cached_value if !cached_value.nil? && !cache_should_refresh?(cache)
|
216
|
-
return run_skipped(cached_value) unless status.can_run?
|
217
222
|
|
218
|
-
|
223
|
+
current_status = status
|
224
|
+
return run_skipped(cached_value) unless current_status.can_run?
|
225
|
+
|
226
|
+
run_exec(current_status, cached_value, cache, &block)
|
219
227
|
end
|
220
228
|
|
221
229
|
# Force the circuit to stay open until unlocked
|
@@ -282,7 +290,7 @@ class Faulty
|
|
282
290
|
# @return The result from cache if available
|
283
291
|
def run_skipped(cached_value)
|
284
292
|
skipped!
|
285
|
-
raise OpenCircuitError.new(nil, self) if cached_value.nil?
|
293
|
+
raise options.error_module::OpenCircuitError.new(nil, self) if cached_value.nil?
|
286
294
|
|
287
295
|
cached_value
|
288
296
|
end
|
@@ -292,26 +300,27 @@ class Faulty
|
|
292
300
|
# @param cached_value The cached value if one is available
|
293
301
|
# @param cache_key [String, nil] The cache key if one is given
|
294
302
|
# @return The run result
|
295
|
-
def run_exec(cached_value, cache_key)
|
303
|
+
def run_exec(status, cached_value, cache_key)
|
296
304
|
result = yield
|
297
|
-
success!
|
305
|
+
success!(status)
|
298
306
|
cache_write(cache_key, result)
|
299
307
|
result
|
300
308
|
rescue *options.errors => e
|
301
309
|
raise if options.exclude.any? { |ex| e.is_a?(ex) }
|
302
310
|
|
303
311
|
if cached_value.nil?
|
304
|
-
raise CircuitTrippedError.new(nil, self) if failure!(e)
|
312
|
+
raise options.error_module::CircuitTrippedError.new(nil, self) if failure!(status, e)
|
305
313
|
|
306
|
-
raise CircuitFailureError.new(nil, self)
|
314
|
+
raise options.error_module::CircuitFailureError.new(nil, self)
|
307
315
|
else
|
308
316
|
cached_value
|
309
317
|
end
|
310
318
|
end
|
311
319
|
|
312
320
|
# @return [Boolean] True if the circuit transitioned to closed
|
313
|
-
def success!
|
314
|
-
|
321
|
+
def success!(status)
|
322
|
+
entries = storage.entry(self, Faulty.current_time, true)
|
323
|
+
status = Status.from_entries(entries, **status.to_h)
|
315
324
|
closed = false
|
316
325
|
closed = close! if should_close?(status)
|
317
326
|
|
@@ -320,8 +329,9 @@ class Faulty
|
|
320
329
|
end
|
321
330
|
|
322
331
|
# @return [Boolean] True if the circuit transitioned to open
|
323
|
-
def failure!(error)
|
324
|
-
|
332
|
+
def failure!(status, error)
|
333
|
+
entries = storage.entry(self, Faulty.current_time, false)
|
334
|
+
status = Status.from_entries(entries, **status.to_h)
|
325
335
|
options.notifier.notify(:circuit_failure, circuit: self, status: status, error: error)
|
326
336
|
|
327
337
|
opened = if status.half_open?
|
data/lib/faulty/error.rb
CHANGED
@@ -28,11 +28,13 @@ class Faulty
|
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
-
#
|
32
|
-
#
|
33
|
-
|
31
|
+
# Included in faulty circuit errors to provide common features for
|
32
|
+
# native and patched errors
|
33
|
+
module CircuitErrorBase
|
34
34
|
attr_reader :circuit
|
35
35
|
|
36
|
+
# @param message [String]
|
37
|
+
# @param circuit [Circuit] The circuit that raised the error
|
36
38
|
def initialize(message, circuit)
|
37
39
|
message ||= %(circuit error for "#{circuit.name}")
|
38
40
|
@circuit = circuit
|
@@ -41,6 +43,12 @@ class Faulty
|
|
41
43
|
end
|
42
44
|
end
|
43
45
|
|
46
|
+
# The base error for all errors raised during circuit runs
|
47
|
+
#
|
48
|
+
class CircuitError < FaultyError
|
49
|
+
include CircuitErrorBase
|
50
|
+
end
|
51
|
+
|
44
52
|
# Raised when running a circuit that is already open
|
45
53
|
class OpenCircuitError < CircuitError; end
|
46
54
|
|
data/lib/faulty/patch.rb
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'faulty/patch/base'
|
4
|
+
|
5
|
+
class Faulty
|
6
|
+
# Automatic wrappers for common core dependencies like database connections
|
7
|
+
# or caches
|
8
|
+
module Patch
|
9
|
+
class << self
|
10
|
+
# Create a circuit from a configuration hash
|
11
|
+
#
|
12
|
+
# This is intended to be used in contexts where the user passes in
|
13
|
+
# something like a connection hash to a third-party library. For example
|
14
|
+
# the Redis patch hooks into the normal Redis connection options to add
|
15
|
+
# a `:faulty` key, which is a hash of faulty circuit options. This is
|
16
|
+
# slightly different from the normal Faulty circuit options because
|
17
|
+
# we also accept an `:instance` key which is a faulty instance.
|
18
|
+
#
|
19
|
+
# @example
|
20
|
+
# # We pass in a faulty instance along with some circuit options
|
21
|
+
# Patch.circuit_from_hash(
|
22
|
+
# :mysql,
|
23
|
+
# { host: 'localhost', faulty: {
|
24
|
+
# name: 'my_mysql', # A custom circuit name can be included
|
25
|
+
# instance: Faulty.new,
|
26
|
+
# sample_threshold: 5
|
27
|
+
# }
|
28
|
+
# }
|
29
|
+
# )
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# # instance can be a registered faulty instance referenced by a string
|
33
|
+
# or symbol
|
34
|
+
# Faulty.register(:db_faulty, Faulty.new)
|
35
|
+
# Patch.circuit_from_hash(
|
36
|
+
# :mysql,
|
37
|
+
# { host: 'localhost', faulty: { instance: :db_faulty } }
|
38
|
+
# )
|
39
|
+
# @example
|
40
|
+
# # If instance is a hash with the key :constant, the value can be
|
41
|
+
# # a global constant name containing a Faulty instance
|
42
|
+
# DB_FAULTY = Faulty.new
|
43
|
+
# Patch.circuit_from_hash(
|
44
|
+
# :mysql,
|
45
|
+
# { host: 'localhost', faulty: { instance: { constant: 'DB_FAULTY' } } }
|
46
|
+
# )
|
47
|
+
#
|
48
|
+
# @example
|
49
|
+
# # Certain patches may want to enforce certain options like :errors
|
50
|
+
# # This can be done via hash or the usual block syntax
|
51
|
+
# Patch.circuit_from_hash(:mysql,
|
52
|
+
# { host: 'localhost', faulty: {} }
|
53
|
+
# errors: [Mysql2::Error]
|
54
|
+
# )
|
55
|
+
#
|
56
|
+
# Patch.circuit_from_hash(:mysql,
|
57
|
+
# { host: 'localhost', faulty: {} }
|
58
|
+
# ) do |conf|
|
59
|
+
# conf.errors = [Mysql2::Error]
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# @param default_name [String] The default name for the circuit
|
63
|
+
# @param hash [Hash] A hash of user-provided options. Supports any circuit
|
64
|
+
# option and these additional options
|
65
|
+
# @option hash [String] :name The circuit name. Defaults to `default_name`
|
66
|
+
# @option hash [Boolean] :patch_errors By default, circuit errors will be
|
67
|
+
# subclasses of `options[:patched_error_module]`. The user can disable
|
68
|
+
# this by setting this option to false.
|
69
|
+
# @option hash [Faulty, String, Symbol, Hash{ constant: String }] :instance
|
70
|
+
# A reference to a faulty instance. See examples.
|
71
|
+
# @param options [Hash] Additional override options. Supports any circuit
|
72
|
+
# option and these additional ones.
|
73
|
+
# @option options [Module] :patched_error_module The namespace module
|
74
|
+
# for patched errors
|
75
|
+
# @yield [Circuit::Options] For setting override options in a block
|
76
|
+
# @return [Circuit, nil] The circuit if one was created
|
77
|
+
def circuit_from_hash(default_name, hash, **options, &block)
|
78
|
+
return unless hash
|
79
|
+
|
80
|
+
hash = symbolize_keys(hash)
|
81
|
+
name = hash.delete(:name) || default_name
|
82
|
+
patch_errors = hash.delete(:patch_errors) != false
|
83
|
+
error_module = options.delete(:patched_error_module)
|
84
|
+
hash[:error_module] ||= error_module if error_module && patch_errors
|
85
|
+
faulty = resolve_instance(hash.delete(:instance))
|
86
|
+
faulty.circuit(name, **hash, **options, &block)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Create a full set of {CircuitError}s with a given base error class
|
90
|
+
#
|
91
|
+
# For patches that need their errors to be subclasses of a common base.
|
92
|
+
#
|
93
|
+
# @param namespace [Module] The module to define the error classes in
|
94
|
+
# @param base [Class] The base class for the error classes
|
95
|
+
# @return [void]
|
96
|
+
def define_circuit_errors(namespace, base)
|
97
|
+
circuit_error = Class.new(base) { include CircuitErrorBase }
|
98
|
+
namespace.const_set('CircuitError', circuit_error)
|
99
|
+
namespace.const_set('OpenCircuitError', Class.new(circuit_error))
|
100
|
+
namespace.const_set('CircuitFailureError', Class.new(circuit_error))
|
101
|
+
namespace.const_set('CircuitTrippedError', Class.new(circuit_error))
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
# Resolves a constant from a constant name or returns a default
|
107
|
+
#
|
108
|
+
# - If value is a string or symbol, gets a registered Faulty instance with that name
|
109
|
+
# - If value is a Hash with a key `:constant`, resolves the value to a global constant
|
110
|
+
# - If value is nil, gets Faulty.default
|
111
|
+
# - Otherwise, return value directly
|
112
|
+
#
|
113
|
+
# @param value [String, Symbol, Faulty, nil] The object or constant name to resolve
|
114
|
+
# @return [Object] The resolved Faulty instance
|
115
|
+
def resolve_instance(value)
|
116
|
+
case value
|
117
|
+
when String, Symbol
|
118
|
+
result = Faulty[value]
|
119
|
+
raise NameError, "No Faulty instance for #{value}" unless result
|
120
|
+
|
121
|
+
result
|
122
|
+
when Hash
|
123
|
+
const_name = value[:constant]
|
124
|
+
raise ArgumentError 'Missing hash key :constant for Faulty instance' unless const_name
|
125
|
+
|
126
|
+
Kernel.const_get(const_name)
|
127
|
+
when nil
|
128
|
+
Faulty.default
|
129
|
+
else
|
130
|
+
value
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Some config files may not suport symbol keys, so we convert the hash
|
135
|
+
# to use symbols so that users can pass in strings
|
136
|
+
#
|
137
|
+
# We cannot use transform_keys since we support Ruby < 2.5
|
138
|
+
#
|
139
|
+
# @param hash [Hash] A hash to convert
|
140
|
+
# @return [Hash] The hash with keys as symbols
|
141
|
+
def symbolize_keys(hash)
|
142
|
+
result = {}
|
143
|
+
hash.each do |key, val|
|
144
|
+
result[key.to_sym] = if val.is_a?(Hash)
|
145
|
+
symbolize_keys(val)
|
146
|
+
else
|
147
|
+
val
|
148
|
+
end
|
149
|
+
end
|
150
|
+
result
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -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)
|
@@ -14,7 +14,8 @@ class Faulty
|
|
14
14
|
# @param circuit [Circuit] The circuit that ran
|
15
15
|
# @param time [Integer] The unix timestamp for the run
|
16
16
|
# @param success [Boolean] True if the run succeeded
|
17
|
-
# @return [
|
17
|
+
# @return [Array<Array>] An array of the new history tuples after adding
|
18
|
+
# the new entry, see {#history}
|
18
19
|
def entry(circuit, time, success)
|
19
20
|
raise NotImplementedError
|
20
21
|
end
|
data/lib/faulty/storage/redis.rb
CHANGED
@@ -95,15 +95,15 @@ class Faulty
|
|
95
95
|
# @return (see Interface#entry)
|
96
96
|
def entry(circuit, time, success)
|
97
97
|
key = entries_key(circuit)
|
98
|
-
pipe do |r|
|
98
|
+
result = pipe do |r|
|
99
99
|
r.sadd(list_key, circuit.name)
|
100
100
|
r.expire(list_key, options.circuit_ttl + options.list_granularity) if options.circuit_ttl
|
101
101
|
r.lpush(key, "#{time}#{ENTRY_SEPARATOR}#{success ? 1 : 0}")
|
102
102
|
r.ltrim(key, 0, options.max_sample_size - 1)
|
103
103
|
r.expire(key, options.sample_ttl) if options.sample_ttl
|
104
|
+
r.lrange(key, 0, -1)
|
104
105
|
end
|
105
|
-
|
106
|
-
status(circuit)
|
106
|
+
map_entries(result.last)
|
107
107
|
end
|
108
108
|
|
109
109
|
# Mark a circuit as open
|
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.5.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: 2021-
|
11
|
+
date: 2021-05-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -165,6 +165,9 @@ files:
|
|
165
165
|
- lib/faulty/events/log_listener.rb
|
166
166
|
- lib/faulty/events/notifier.rb
|
167
167
|
- lib/faulty/immutable_options.rb
|
168
|
+
- lib/faulty/patch.rb
|
169
|
+
- lib/faulty/patch/base.rb
|
170
|
+
- lib/faulty/patch/redis.rb
|
168
171
|
- lib/faulty/result.rb
|
169
172
|
- lib/faulty/status.rb
|
170
173
|
- lib/faulty/storage.rb
|
@@ -195,8 +198,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
195
198
|
- !ruby/object:Gem::Version
|
196
199
|
version: '0'
|
197
200
|
requirements: []
|
198
|
-
|
199
|
-
rubygems_version: 2.7.6
|
201
|
+
rubygems_version: 3.1.2
|
200
202
|
signing_key:
|
201
203
|
specification_version: 4
|
202
204
|
summary: Fault-tolerance tools for ruby based on circuit-breakers
|