faulty 0.10.0 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![CI](https://github.com/ParentSquare/faulty/workflows/CI/badge.svg)](https://github.com/ParentSquare/faulty/actions?query=workflow%3ACI+branch%3Amaster)
|
5
5
|
[![Code Quality](https://app.codacy.com/project/badge/Grade/16bb1df1569a4ddba893a866673dac2a)](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
|
[![Code Coverage](https://codecov.io/gh/ParentSquare/faulty/branch/master/graph/badge.svg?token=1NDT4FW1YJ)](https://codecov.io/gh/ParentSquare/faulty)
|
7
|
-
[![
|
7
|
+
[![Online docs](https://img.shields.io/badge/docs-✓-green.svg)](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:
|