faulty 0.10.0 → 0.11.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/CHANGELOG.md +10 -1
- data/README.md +9 -2
- data/lib/faulty/circuit.rb +1 -1
- data/lib/faulty/patch/redis/middleware.rb +54 -0
- data/lib/faulty/patch/redis/patch.rb +76 -0
- data/lib/faulty/patch/redis.rb +13 -66
- data/lib/faulty/storage/redis.rb +12 -6
- data/lib/faulty/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8604d16866bb43e4b58ebd4d9efd9e3ae2a85555bd5e2d92c1f1ec8bfa24fb69
|
4
|
+
data.tar.gz: a766e4e122b41e4d58cab15fc0c249a254e940e523b54e8d906d00bcefafb5e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d2bf7c915310ab7eac608fc738f64df191555c7acfe1b6a28eb5a8e0519e42f2cf0512ba5bd7d13d62379783cdbefef69c9fa952d08ff78bf95d973afbd7f15e
|
7
|
+
data.tar.gz: 56963c2e80e34f2f56ac5a4efa405032f9eb72bd176ee0e7683a2f7dd0a4d26f06535773b9056c84c50d2d5349b2c2eb6d9dcc97fa98f3ef130245e3d65adb2b
|
data/CHANGELOG.md
CHANGED
@@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
9
9
|
[Unreleased]
|
10
10
|
-------------------
|
11
11
|
|
12
|
+
[0.11.0] - 2023-04-26
|
13
|
+
---------------------
|
14
|
+
|
15
|
+
### Added
|
16
|
+
|
17
|
+
* Add storage support for redis gem v5 #63 justinhoward
|
18
|
+
* Add Redis 5 support for patch #67 justinhoward
|
19
|
+
|
12
20
|
[0.10.0] - 2023-04-05
|
13
21
|
---------------------
|
14
22
|
|
@@ -300,7 +308,8 @@ of AutoWire.
|
|
300
308
|
|
301
309
|
Initial public release
|
302
310
|
|
303
|
-
[Unreleased]: https://github.com/ParentSquare/faulty/compare/v0.
|
311
|
+
[Unreleased]: https://github.com/ParentSquare/faulty/compare/v0.11.0...HEAD
|
312
|
+
[0.11.0]: https://github.com/ParentSquare/faulty/compare/v0.10.0...v0.11.0
|
304
313
|
[0.10.0]: https://github.com/ParentSquare/faulty/compare/v0.9.0...v0.10.0
|
305
314
|
[0.9.0]: https://github.com/ParentSquare/faulty/compare/v0.8.7...v0.9.0
|
306
315
|
[0.8.7]: https://github.com/ParentSquare/faulty/compare/v0.8.6...v0.8.7
|
data/README.md
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
[](https://github.com/ParentSquare/faulty/actions?query=workflow%3ACI+branch%3Amaster)
|
5
5
|
[](https://www.codacy.com/gh/ParentSquare/faulty/dashboard?utm_source=github.com&utm_medium=referral&utm_content=ParentSquare/faulty&utm_campaign=Badge_Grade)
|
6
6
|
[](https://codecov.io/gh/ParentSquare/faulty)
|
7
|
-
[](https://www.rubydoc.info/github/ParentSquare/faulty)
|
8
8
|
|
9
9
|
Fault-tolerance tools for ruby based on [circuit-breakers][martin fowler].
|
10
10
|
|
@@ -985,11 +985,12 @@ Mysql2::Client.new(host: '127.0.0.1', faulty: { instance: 'mysql2' })
|
|
985
985
|
protects a Redis client with an internal circuit. Pass a `:faulty` key along
|
986
986
|
with your connection options to enable the circuit breaker.
|
987
987
|
|
988
|
-
The Redis patch supports the Redis gem versions 3 and
|
988
|
+
The Redis patch supports the Redis gem versions 3 and up
|
989
989
|
|
990
990
|
```ruby
|
991
991
|
require 'faulty/patch/redis'
|
992
992
|
|
993
|
+
# For Redis <= 4, pass faulty into the top-level connection options
|
993
994
|
redis = Redis.new(url: 'redis://localhost:6379', faulty: {
|
994
995
|
# The name for the redis circuit
|
995
996
|
name: 'redis'
|
@@ -1004,6 +1005,12 @@ redis = Redis.new(url: 'redis://localhost:6379', faulty: {
|
|
1004
1005
|
# will raise its default errors
|
1005
1006
|
patch_errors: true
|
1006
1007
|
})
|
1008
|
+
|
1009
|
+
# Or for Redis 5+, pass faulty into the custom connection options
|
1010
|
+
redis = Redis.new(url: 'redis://localhost:6379', custom: { faulty: {
|
1011
|
+
# ...
|
1012
|
+
}})
|
1013
|
+
|
1007
1014
|
redis.connect # raises Faulty::CircuitError if connection fails
|
1008
1015
|
|
1009
1016
|
# If the faulty key is not given, no circuit is used
|
data/lib/faulty/circuit.rb
CHANGED
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Faulty
|
4
|
+
module Patch
|
5
|
+
module Redis
|
6
|
+
Patch.define_circuit_errors(self, ::RedisClient::ConnectionError)
|
7
|
+
|
8
|
+
class BusyError < ::RedisClient::CommandError
|
9
|
+
end
|
10
|
+
|
11
|
+
module Middleware
|
12
|
+
include Base
|
13
|
+
|
14
|
+
def initialize(client)
|
15
|
+
@faulty_circuit = Patch.circuit_from_hash(
|
16
|
+
'redis',
|
17
|
+
client.config.custom[:faulty],
|
18
|
+
errors: [
|
19
|
+
::RedisClient::ConnectionError,
|
20
|
+
BusyError
|
21
|
+
],
|
22
|
+
patched_error_mapper: Faulty::Patch::Redis
|
23
|
+
)
|
24
|
+
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
def connect(redis_config)
|
29
|
+
faulty_run { super }
|
30
|
+
end
|
31
|
+
|
32
|
+
def call(commands, redis_config)
|
33
|
+
faulty_run { wrap_command { super } }
|
34
|
+
end
|
35
|
+
|
36
|
+
def call_pipelined(commands, redis_config)
|
37
|
+
faulty_run { wrap_command { super } }
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def wrap_command
|
43
|
+
yield
|
44
|
+
rescue ::RedisClient::CommandError => e
|
45
|
+
raise BusyError, e.message if e.message.start_with?('BUSY')
|
46
|
+
|
47
|
+
raise
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
::RedisClient.register(Middleware)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'redis'
|
4
|
+
|
5
|
+
class Faulty
|
6
|
+
module Patch
|
7
|
+
module Redis
|
8
|
+
include Base
|
9
|
+
|
10
|
+
Patch.define_circuit_errors(self, ::Redis::BaseConnectionError)
|
11
|
+
|
12
|
+
class BusyError < ::Redis::CommandError
|
13
|
+
end
|
14
|
+
|
15
|
+
# Patches Redis to add the `:faulty` key
|
16
|
+
def initialize(options = {})
|
17
|
+
@faulty_circuit = Patch.circuit_from_hash(
|
18
|
+
'redis',
|
19
|
+
options[:faulty],
|
20
|
+
errors: [
|
21
|
+
::Redis::BaseConnectionError,
|
22
|
+
BusyError
|
23
|
+
],
|
24
|
+
patched_error_mapper: Faulty::Patch::Redis
|
25
|
+
)
|
26
|
+
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
# The initial connection is protected by a circuit
|
31
|
+
def connect
|
32
|
+
faulty_run { super }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Protect command calls
|
36
|
+
def call(command)
|
37
|
+
faulty_run { super }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Protect command_loop calls
|
41
|
+
def call_loop(command, timeout = 0)
|
42
|
+
faulty_run { super }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Protect pipelined commands
|
46
|
+
def call_pipelined(commands)
|
47
|
+
faulty_run { super }
|
48
|
+
end
|
49
|
+
|
50
|
+
# Inject specific error classes if client is patched
|
51
|
+
#
|
52
|
+
# This method does not raise errors, it returns them
|
53
|
+
# as exception objects, so we simply modify that error if necessary and
|
54
|
+
# return it.
|
55
|
+
#
|
56
|
+
# The call* methods above will then raise that error, so we are able to
|
57
|
+
# capture it with faulty_run.
|
58
|
+
def io(&block)
|
59
|
+
return super unless @faulty_circuit
|
60
|
+
|
61
|
+
reply = super
|
62
|
+
if reply.is_a?(::Redis::CommandError) && reply.message.start_with?('BUSY')
|
63
|
+
reply = BusyError.new(reply.message)
|
64
|
+
end
|
65
|
+
|
66
|
+
reply
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class Redis
|
73
|
+
class Client
|
74
|
+
prepend(Faulty::Patch::Redis)
|
75
|
+
end
|
76
|
+
end
|
data/lib/faulty/patch/redis.rb
CHANGED
@@ -8,9 +8,12 @@ class Faulty
|
|
8
8
|
#
|
9
9
|
# This module is not required by default
|
10
10
|
#
|
11
|
-
#
|
11
|
+
# Redis <= 4
|
12
|
+
# ---------------------
|
13
|
+
# Pass a `:faulty` key into your Redis connection options to enable
|
12
14
|
# circuit protection. See {Patch.circuit_from_hash} for the available
|
13
|
-
# options.
|
15
|
+
# options. On Redis 5+, the faulty key should be passed in the `:custom` hash
|
16
|
+
# instead of the top-level options. See example.
|
14
17
|
#
|
15
18
|
# By default, all circuit errors raised by this patch inherit from
|
16
19
|
# `::Redis::BaseConnectionError`
|
@@ -18,7 +21,11 @@ class Faulty
|
|
18
21
|
# @example
|
19
22
|
# require 'faulty/patch/redis'
|
20
23
|
#
|
24
|
+
# # Redis <= 4
|
21
25
|
# redis = Redis.new(url: 'redis://localhost:6379', faulty: {})
|
26
|
+
# # Or for Redis 5+
|
27
|
+
# redis = Redis.new(url: 'redis://localhost:6379', custom: { faulty: {} })
|
28
|
+
#
|
22
29
|
# redis.connect # raises Faulty::CircuitError if connection fails
|
23
30
|
#
|
24
31
|
# # If the faulty key is not given, no circuit is used
|
@@ -27,72 +34,12 @@ class Faulty
|
|
27
34
|
#
|
28
35
|
# @see Patch.circuit_from_hash
|
29
36
|
module Redis
|
30
|
-
include Base
|
31
|
-
|
32
|
-
Patch.define_circuit_errors(self, ::Redis::BaseConnectionError)
|
33
|
-
|
34
|
-
class BusyError < ::Redis::CommandError
|
35
|
-
end
|
36
|
-
|
37
|
-
# Patches Redis to add the `:faulty` key
|
38
|
-
def initialize(options = {})
|
39
|
-
@faulty_circuit = Patch.circuit_from_hash(
|
40
|
-
'redis',
|
41
|
-
options[:faulty],
|
42
|
-
errors: [
|
43
|
-
::Redis::BaseConnectionError,
|
44
|
-
BusyError
|
45
|
-
],
|
46
|
-
patched_error_mapper: Faulty::Patch::Redis
|
47
|
-
)
|
48
|
-
|
49
|
-
super
|
50
|
-
end
|
51
|
-
|
52
|
-
# The initial connection is protected by a circuit
|
53
|
-
def connect
|
54
|
-
faulty_run { super }
|
55
|
-
end
|
56
|
-
|
57
|
-
# Protect command calls
|
58
|
-
def call(command)
|
59
|
-
faulty_run { super }
|
60
|
-
end
|
61
|
-
|
62
|
-
# Protect command_loop calls
|
63
|
-
def call_loop(command, timeout = 0)
|
64
|
-
faulty_run { super }
|
65
|
-
end
|
66
|
-
|
67
|
-
# Protect pipelined commands
|
68
|
-
def call_pipelined(commands)
|
69
|
-
faulty_run { super }
|
70
|
-
end
|
71
|
-
|
72
|
-
# Inject specific error classes if client is patched
|
73
|
-
#
|
74
|
-
# This method does not raise errors, it returns them
|
75
|
-
# as exception objects, so we simply modify that error if necessary and
|
76
|
-
# return it.
|
77
|
-
#
|
78
|
-
# The call* methods above will then raise that error, so we are able to
|
79
|
-
# capture it with faulty_run.
|
80
|
-
def io(&block)
|
81
|
-
return super unless @faulty_circuit
|
82
|
-
|
83
|
-
reply = super
|
84
|
-
if reply.is_a?(::Redis::CommandError) && reply.message.start_with?('BUSY')
|
85
|
-
reply = BusyError.new(reply.message)
|
86
|
-
end
|
87
|
-
|
88
|
-
reply
|
89
|
-
end
|
90
37
|
end
|
91
38
|
end
|
92
39
|
end
|
93
40
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
41
|
+
if Redis::VERSION.to_f < 5
|
42
|
+
require 'faulty/patch/redis/patch'
|
43
|
+
else
|
44
|
+
require 'faulty/patch/redis/middleware'
|
98
45
|
end
|
data/lib/faulty/storage/redis.rb
CHANGED
@@ -25,7 +25,7 @@ class Faulty
|
|
25
25
|
# circuit state. Default `faulty`.
|
26
26
|
# @!attribute [r] key_separator
|
27
27
|
# @return [String] A string used to separate the parts of the Redis keys
|
28
|
-
# used to store circuit state.
|
28
|
+
# used to store circuit state. Default `:`.
|
29
29
|
# @!attribute [r] max_sample_size
|
30
30
|
# @return [Integer] The number of cache run entries to keep in memory
|
31
31
|
# for each circuit. Default `100`.
|
@@ -122,7 +122,7 @@ class Faulty
|
|
122
122
|
def entry(circuit, time, success, status)
|
123
123
|
key = entries_key(circuit.name)
|
124
124
|
result = pipe do |r|
|
125
|
-
r.sadd
|
125
|
+
r.call([:sadd, list_key, circuit.name])
|
126
126
|
r.expire(list_key, options.circuit_ttl + options.list_granularity) if options.circuit_ttl
|
127
127
|
r.lpush(key, "#{time}#{ENTRY_SEPARATOR}#{success ? 1 : 0}")
|
128
128
|
r.ltrim(key, 0, options.max_sample_size - 1)
|
@@ -425,11 +425,16 @@ class Faulty
|
|
425
425
|
end
|
426
426
|
|
427
427
|
def check_redis_options!
|
428
|
-
|
428
|
+
gte5 = ::Redis::VERSION.to_f >= 5
|
429
|
+
method = gte5 ? :config : :options
|
430
|
+
ropts = redis do |r|
|
431
|
+
r.instance_variable_get(:@client).public_send(method)
|
432
|
+
end
|
429
433
|
|
430
434
|
bad_timeouts = {}
|
431
435
|
%i[connect_timeout read_timeout write_timeout].each do |time_opt|
|
432
|
-
|
436
|
+
value = gte5 ? ropts.public_send(time_opt) : ropts[time_opt]
|
437
|
+
bad_timeouts[time_opt] = value if value > 2
|
433
438
|
end
|
434
439
|
|
435
440
|
unless bad_timeouts.empty?
|
@@ -440,10 +445,11 @@ class Faulty
|
|
440
445
|
MSG
|
441
446
|
end
|
442
447
|
|
443
|
-
|
448
|
+
gt1_retry = gte5 ? ropts.retry_connecting?(1, nil) : ropts[:reconnect_attempts] > 1
|
449
|
+
if gt1_retry
|
444
450
|
warn <<~MSG
|
445
451
|
Faulty recommends setting Redis reconnect_attempts to <= 1 to
|
446
|
-
prevent cascading failures. Your setting is
|
452
|
+
prevent cascading failures. Your setting is larger.
|
447
453
|
MSG
|
448
454
|
end
|
449
455
|
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.11.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: 2023-04-
|
11
|
+
date: 2023-04-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -132,6 +132,8 @@ files:
|
|
132
132
|
- lib/faulty/patch/elasticsearch.rb
|
133
133
|
- lib/faulty/patch/mysql2.rb
|
134
134
|
- lib/faulty/patch/redis.rb
|
135
|
+
- lib/faulty/patch/redis/middleware.rb
|
136
|
+
- lib/faulty/patch/redis/patch.rb
|
135
137
|
- lib/faulty/result.rb
|
136
138
|
- lib/faulty/status.rb
|
137
139
|
- lib/faulty/storage.rb
|
@@ -150,7 +152,7 @@ licenses:
|
|
150
152
|
metadata:
|
151
153
|
rubygems_mfa_required: 'true'
|
152
154
|
changelog_uri: https://github.com/ParentSquare/faulty/blob/master/CHANGELOG.md
|
153
|
-
documentation_uri: https://www.rubydoc.info/gems/faulty/0.
|
155
|
+
documentation_uri: https://www.rubydoc.info/gems/faulty/0.11.0
|
154
156
|
post_install_message:
|
155
157
|
rdoc_options: []
|
156
158
|
require_paths:
|