faulty 0.5.1 → 0.7.2
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 +7 -0
- data/CHANGELOG.md +26 -0
- data/Gemfile +3 -0
- data/README.md +84 -6
- data/bin/benchmark +52 -0
- data/faulty.gemspec +0 -1
- data/lib/faulty/cache/circuit_proxy.rb +1 -1
- data/lib/faulty/circuit.rb +5 -15
- data/lib/faulty/events/callback_listener.rb +1 -1
- data/lib/faulty/events/filter_notifier.rb +31 -0
- data/lib/faulty/events/honeybadger_listener.rb +10 -2
- data/lib/faulty/events/log_listener.rb +9 -5
- data/lib/faulty/events.rb +3 -0
- data/lib/faulty/patch/base.rb +1 -1
- data/lib/faulty/patch/mysql2.rb +81 -0
- data/lib/faulty/patch/redis.rb +43 -10
- data/lib/faulty/status.rb +9 -2
- data/lib/faulty/storage/circuit_proxy.rb +1 -1
- data/lib/faulty/storage/null.rb +83 -0
- data/lib/faulty/storage.rb +1 -0
- data/lib/faulty/version.rb +1 -1
- data/lib/faulty.rb +27 -0
- metadata +6 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d963d5c58861cc9e39c3222b5dc78a2a537c15fc10e0ed76d9d5b41a6704356b
|
4
|
+
data.tar.gz: e31811fc1b5be36975982e966106ea7f323b4e334b87cfaca85667f550245584
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 52aae6a3997fca7d38aebd124399747ee21a687114076e5d73c135d86ab147360e71a9f7a95ee4a5f548b6709089697e77fa472730c3fc18cb9c1d412bdac4ca
|
7
|
+
data.tar.gz: 56b02cad8fe96373c916746bfc7c6f83665a74e7a3771d7b761244373b1d4b79aeb44d94fbe9401f82f8671b4e21c625a079b4845a42565dfbfbdfd37a07d2bf
|
data/.github/workflows/ci.yml
CHANGED
@@ -25,12 +25,19 @@ jobs:
|
|
25
25
|
steps:
|
26
26
|
- uses: actions/checkout@v2
|
27
27
|
- uses: ruby/setup-ruby@v1
|
28
|
+
env:
|
29
|
+
REDIS_VERSION: ${{ matrix.redis }}
|
28
30
|
with:
|
29
31
|
ruby-version: ${{ matrix.ruby }}
|
30
32
|
bundler-cache: true
|
31
33
|
- run: bundle exec rubocop
|
32
34
|
if: matrix.ruby == '2.7'
|
35
|
+
- name: start MySQL
|
36
|
+
run: sudo /etc/init.d/mysql start
|
33
37
|
- run: bundle exec rspec --format doc
|
38
|
+
env:
|
39
|
+
MYSQL_USER: root
|
40
|
+
MYSQL_PASSWORD: root
|
34
41
|
- name: Run codacy-coverage-reporter
|
35
42
|
uses: codacy/codacy-coverage-reporter-action@master
|
36
43
|
with:
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,29 @@
|
|
1
|
+
## Release v0.7.2
|
2
|
+
|
3
|
+
* Add Faulty.disable! for disabling globally #38 justinhoward
|
4
|
+
* Suppress circuit_success for proxy circuits #39 justinhoward
|
5
|
+
|
6
|
+
## Release v0.7.1
|
7
|
+
|
8
|
+
* Fix success event crash in log listener #37 justinhoward
|
9
|
+
|
10
|
+
## Release v0.7.0
|
11
|
+
|
12
|
+
* Add initial benchmarks and performance improvements #36 justinhoward
|
13
|
+
|
14
|
+
### Breaking Changes
|
15
|
+
|
16
|
+
The `circuit_success` event no longer contains the status value. Computing this
|
17
|
+
value was causing performance problems.
|
18
|
+
|
19
|
+
## Release v0.6.0
|
20
|
+
|
21
|
+
* docs, use correct state in description for skipped event #27 senny
|
22
|
+
* Fix CI to set REDIS_VERSION correctly #31 justinhoward
|
23
|
+
* Fix a potential memory leak in patches #32 justinhoward
|
24
|
+
* Capture an error for BUSY redis backend when patched #30 justinhoward
|
25
|
+
* Add a patch for mysql2 #28 justinhoward
|
26
|
+
|
1
27
|
## Release v0.5.1
|
2
28
|
|
3
29
|
* Fix Storage::FaultTolerantProxy to return empty history on entries fail #26 justinhoward
|
data/Gemfile
CHANGED
@@ -13,7 +13,10 @@ not_jruby = %i[ruby mingw x64_mingw].freeze
|
|
13
13
|
gem 'activesupport', '>= 4.2'
|
14
14
|
gem 'bundler', '>= 1.17', '< 3'
|
15
15
|
gem 'byebug', platforms: not_jruby
|
16
|
+
gem 'honeybadger', '>= 2.0'
|
16
17
|
gem 'irb', '~> 1.0'
|
18
|
+
# Minimum of 0.5.0 for specific error classes
|
19
|
+
gem 'mysql2', '>= 0.5.0', platforms: not_jruby
|
17
20
|
gem 'redcarpet', '~> 3.5', platforms: not_jruby
|
18
21
|
gem 'rspec_junit_formatter', '~> 0.4'
|
19
22
|
gem 'simplecov', '>= 0.17.1'
|
data/README.md
CHANGED
@@ -83,10 +83,12 @@ Also see "Release It!: Design and Deploy Production-Ready Software" by
|
|
83
83
|
+ [Locking Circuits](#locking-circuits)
|
84
84
|
* [Patches](#patches)
|
85
85
|
+ [Patch::Redis](#patchredis)
|
86
|
+
+ [Patch::Mysql2](#patchmysql2)
|
86
87
|
* [Event Handling](#event-handling)
|
87
88
|
+ [CallbackListener](#callbacklistener)
|
88
89
|
+ [Other Built-in Listeners](#other-built-in-listeners)
|
89
90
|
+ [Custom Listeners](#custom-listeners)
|
91
|
+
* [Disabling Faulty Globally](#disabling-faulty-globally)
|
90
92
|
* [How it Works](#how-it-works)
|
91
93
|
+ [Caching](#caching)
|
92
94
|
+ [Fault Tolerance](#fault-tolerance)
|
@@ -948,15 +950,40 @@ Or require them in your `Gemfile`
|
|
948
950
|
gem 'faulty', require: %w[faulty faulty/patch/redis]
|
949
951
|
```
|
950
952
|
|
953
|
+
For core dependencies you'll most likely want to use the in-memory circuit
|
954
|
+
storage adapter and not the Redis storage adapter. That way if Redis fails, your
|
955
|
+
circuit storage doesn't also fail, causing cascading failures.
|
956
|
+
|
957
|
+
For example, you can use a separate Faulty instance to manage your Mysql2
|
958
|
+
circuit:
|
959
|
+
|
960
|
+
```ruby
|
961
|
+
# Setup your default config. This can use the Redis backend if you prefer
|
962
|
+
Faulty.init do |config|
|
963
|
+
# ...
|
964
|
+
end
|
965
|
+
|
966
|
+
Faulty.register(:mysql) do |config|
|
967
|
+
# Here we decide to set some circuit defaults more useful for
|
968
|
+
# frequent database calls
|
969
|
+
config.circuit_defaults = {
|
970
|
+
cool_down: 20.0,
|
971
|
+
evaluation_window: 40,
|
972
|
+
sample_threshold: 25
|
973
|
+
}
|
974
|
+
end
|
975
|
+
|
976
|
+
# Now we can use our "mysql" faulty instance when constructing a Mysql2 client
|
977
|
+
Mysql2::Client.new(host: '127.0.0.1', faulty: { instance: 'mysql2' })
|
978
|
+
```
|
979
|
+
|
951
980
|
### Patch::Redis
|
952
981
|
|
953
982
|
[`Faulty::Patch::Redis`](https://www.rubydoc.info/gems/faulty/Faulty/Patch/Redis)
|
954
983
|
protects a Redis client with an internal circuit. Pass a `:faulty` key along
|
955
984
|
with your connection options to enable the circuit breaker.
|
956
985
|
|
957
|
-
|
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.
|
986
|
+
The Redis patch supports the Redis gem versions 3 and 4.
|
960
987
|
|
961
988
|
```ruby
|
962
989
|
require 'faulty/patch/redis'
|
@@ -982,6 +1009,43 @@ redis = Redis.new(url: 'redis://localhost:6379')
|
|
982
1009
|
redis.connect # not protected by a circuit
|
983
1010
|
```
|
984
1011
|
|
1012
|
+
### Patch::Mysql2
|
1013
|
+
|
1014
|
+
[`Faulty::Patch::Mysql2`](https://www.rubydoc.info/gems/faulty/Faulty/Patch/Mysql2)
|
1015
|
+
protects a `Mysql2::Client` with an internal circuit. Pass a `:faulty` key along
|
1016
|
+
with your connection options to enable the circuit breaker.
|
1017
|
+
|
1018
|
+
Faulty supports the mysql2 gem versions 0.5 and greater.
|
1019
|
+
|
1020
|
+
Note: Although Faulty supports Ruby 2.3 in general, the Mysql2 patch is not
|
1021
|
+
fully supported on Ruby 2.3. It may work for you, but use it at your own risk.
|
1022
|
+
|
1023
|
+
```ruby
|
1024
|
+
require 'faulty/patch/mysql2'
|
1025
|
+
|
1026
|
+
mysql = Mysql2::Client.new(host: '127.0.0.1', faulty: {
|
1027
|
+
# The name for the Mysql2 circuit
|
1028
|
+
name: 'mysql2'
|
1029
|
+
|
1030
|
+
# The faulty instance to use
|
1031
|
+
# This can also be a registered faulty instance or a constant name. See API
|
1032
|
+
# docs for more details
|
1033
|
+
instance: Faulty.default
|
1034
|
+
|
1035
|
+
# By default, circuit errors will be subclasses of
|
1036
|
+
# Mysql2::Error::ConnectionError
|
1037
|
+
# To disable this behavior, set patch_errors to false and Faulty
|
1038
|
+
# will raise its default errors
|
1039
|
+
patch_errors: true
|
1040
|
+
})
|
1041
|
+
|
1042
|
+
mysql.query('SELECT * FROM users') # raises Faulty::CircuitError if connection fails
|
1043
|
+
|
1044
|
+
# If the faulty key is not given, no circuit is used
|
1045
|
+
mysql = Mysql2::Client.new(host: '127.0.0.1')
|
1046
|
+
mysql.query('SELECT * FROM users') # not protected by a circuit
|
1047
|
+
```
|
1048
|
+
|
985
1049
|
## Event Handling
|
986
1050
|
|
987
1051
|
Faulty uses an event-dispatching model to deliver notifications of internal
|
@@ -1000,9 +1064,8 @@ events. The full list of events is available from
|
|
1000
1064
|
- `circuit_reopened` - A circuit execution cause the circuit to reopen from
|
1001
1065
|
half-open. Payload: `circuit`, `error`.
|
1002
1066
|
- `circuit_skipped` - A circuit execution was skipped because the circuit is
|
1003
|
-
|
1004
|
-
- `circuit_success` - A circuit execution was successful. Payload: `circuit
|
1005
|
-
`status`
|
1067
|
+
open. Payload: `circuit`
|
1068
|
+
- `circuit_success` - A circuit execution was successful. Payload: `circuit`
|
1006
1069
|
- `storage_failure` - A storage backend raised an error. Payload `circuit` (can
|
1007
1070
|
be nil), `action`, `error`
|
1008
1071
|
|
@@ -1062,6 +1125,21 @@ Faulty.init do |config|
|
|
1062
1125
|
end
|
1063
1126
|
```
|
1064
1127
|
|
1128
|
+
## Disabling Faulty Globally
|
1129
|
+
|
1130
|
+
For testing or for some environments, you may wish to disable Faulty circuits
|
1131
|
+
at a global level.
|
1132
|
+
|
1133
|
+
```ruby
|
1134
|
+
Faulty.disable!
|
1135
|
+
```
|
1136
|
+
|
1137
|
+
This only affects the process where you run the `#disable!` method and it does
|
1138
|
+
not affect the stored state of circuits.
|
1139
|
+
|
1140
|
+
Faulty will **still use the cache** even when disabled. If you also want to
|
1141
|
+
disable the cache, configure Faulty to use a `Faulty::Cache::Null` cache.
|
1142
|
+
|
1065
1143
|
## How it Works
|
1066
1144
|
|
1067
1145
|
Faulty implements a version of circuit breakers inspired by "Release It!: Design
|
data/bin/benchmark
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'benchmark'
|
6
|
+
require 'faulty'
|
7
|
+
|
8
|
+
n = 100_000
|
9
|
+
|
10
|
+
puts "Starting circuit benchmarks with #{n} iterations each\n\n"
|
11
|
+
|
12
|
+
Benchmark.bm(25) do |b|
|
13
|
+
in_memory = Faulty.new(listeners: [])
|
14
|
+
b.report('memory storage') do
|
15
|
+
n.times { in_memory.circuit(:memory).run { true } }
|
16
|
+
end
|
17
|
+
|
18
|
+
b.report('memory storage failures') do
|
19
|
+
n.times do
|
20
|
+
begin
|
21
|
+
in_memory.circuit(:memory_fail, sample_threshold: n + 1).run { raise 'fail' }
|
22
|
+
rescue StandardError
|
23
|
+
# Expected to raise here
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
in_memory_large = Faulty.new(listeners: [], storage: Faulty::Storage::Memory.new(max_sample_size: 1000))
|
29
|
+
b.report('large memory storage') do
|
30
|
+
n.times { in_memory_large.circuit(:memory_large).run { true } }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
n = 1_000_000
|
35
|
+
|
36
|
+
puts "\n\nStarting extra benchmarks with #{n} iterations each\n\n"
|
37
|
+
|
38
|
+
Benchmark.bm(25) do |b|
|
39
|
+
in_memory = Faulty.new(listeners: [])
|
40
|
+
|
41
|
+
log_listener = Faulty::Events::LogListener.new(Logger.new(File::NULL))
|
42
|
+
log_circuit = in_memory.circuit(:log_listener)
|
43
|
+
log_status = log_circuit.status
|
44
|
+
b.report('log listener success') do
|
45
|
+
n.times { log_listener.handle(:circuit_success, circuit: log_circuit, status: log_status) }
|
46
|
+
end
|
47
|
+
|
48
|
+
log_error = StandardError.new('test error')
|
49
|
+
b.report('log listener failure') do
|
50
|
+
n.times { log_listener.handle(:circuit_failure, error: log_error, circuit: log_circuit, status: log_status) }
|
51
|
+
end
|
52
|
+
end
|
data/faulty.gemspec
CHANGED
@@ -26,7 +26,6 @@ Gem::Specification.new do |spec|
|
|
26
26
|
# Only essential development tools and dependencies go here.
|
27
27
|
# Other non-essential development dependencies go in the Gemfile.
|
28
28
|
spec.add_development_dependency 'connection_pool', '~> 2.0'
|
29
|
-
spec.add_development_dependency 'honeybadger', '>= 2.0'
|
30
29
|
spec.add_development_dependency 'redis', '>= 3.0'
|
31
30
|
spec.add_development_dependency 'rspec', '~> 3.8'
|
32
31
|
# 0.81 is the last rubocop version with Ruby 2.3 support
|
data/lib/faulty/circuit.rb
CHANGED
@@ -319,12 +319,10 @@ class Faulty
|
|
319
319
|
|
320
320
|
# @return [Boolean] True if the circuit transitioned to closed
|
321
321
|
def success!(status)
|
322
|
-
|
323
|
-
|
324
|
-
closed = false
|
325
|
-
closed = close! if should_close?(status)
|
322
|
+
storage.entry(self, Faulty.current_time, true)
|
323
|
+
closed = close! if status.half_open?
|
326
324
|
|
327
|
-
options.notifier.notify(:circuit_success, circuit: self
|
325
|
+
options.notifier.notify(:circuit_success, circuit: self)
|
328
326
|
closed
|
329
327
|
end
|
330
328
|
|
@@ -370,16 +368,6 @@ class Faulty
|
|
370
368
|
closed
|
371
369
|
end
|
372
370
|
|
373
|
-
# Test whether we should close after a successful run
|
374
|
-
#
|
375
|
-
# Currently this is always true if the circuit is half-open, which is the
|
376
|
-
# traditional behavior for a circuit-breaker
|
377
|
-
#
|
378
|
-
# @return [Boolean] True if we should close the circuit from half-open
|
379
|
-
def should_close?(status)
|
380
|
-
status.half_open?
|
381
|
-
end
|
382
|
-
|
383
371
|
# Read from the cache if it is configured
|
384
372
|
#
|
385
373
|
# @param key The key to read from the cache
|
@@ -445,6 +433,8 @@ class Faulty
|
|
445
433
|
#
|
446
434
|
# @return [Storage::Interface]
|
447
435
|
def storage
|
436
|
+
return Faulty::Storage::Null.new if Faulty.disabled?
|
437
|
+
|
448
438
|
options.storage
|
449
439
|
end
|
450
440
|
end
|
@@ -23,7 +23,7 @@ class Faulty
|
|
23
23
|
# @param (see ListenerInterface#handle)
|
24
24
|
# @return [void]
|
25
25
|
def handle(event, payload)
|
26
|
-
return unless
|
26
|
+
return unless EVENT_SET.include?(event)
|
27
27
|
return unless @handlers.key?(event)
|
28
28
|
|
29
29
|
@handlers[event].each do |handler|
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Faulty
|
4
|
+
module Events
|
5
|
+
# Wraps a Notifier and filters events by name
|
6
|
+
class FilterNotifier
|
7
|
+
# @param notifier [Notifier] The internal notifier to filter events for
|
8
|
+
# @param events [Array, nil] An array of events to allow. If nil, all
|
9
|
+
# {EVENTS} will be used
|
10
|
+
# @param exclude [Array, nil] An array of events to disallow. If nil,
|
11
|
+
# no events will be disallowed. Takes priority over `events`.
|
12
|
+
def initialize(notifier, events: nil, exclude: nil)
|
13
|
+
@notifier = notifier
|
14
|
+
@events = Set.new(events || EVENTS)
|
15
|
+
exclude&.each { |e| @events.delete(e) }
|
16
|
+
end
|
17
|
+
|
18
|
+
# Notify all listeners of an event
|
19
|
+
#
|
20
|
+
# If a listener raises an error while handling an event, that error will
|
21
|
+
# be captured and written to STDERR.
|
22
|
+
#
|
23
|
+
# @param (see Notifier)
|
24
|
+
def notify(event, payload)
|
25
|
+
return unless @events.include?(event)
|
26
|
+
|
27
|
+
@notifier.notify(event, payload)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -8,11 +8,19 @@ class Faulty
|
|
8
8
|
#
|
9
9
|
# The honeybadger gem must be available.
|
10
10
|
class HoneybadgerListener
|
11
|
+
HONEYBADGER_EVENTS = Set[
|
12
|
+
:circuit_failure,
|
13
|
+
:circuit_opened,
|
14
|
+
:circuit_reopened,
|
15
|
+
:cache_failure,
|
16
|
+
:storage_failure
|
17
|
+
].freeze
|
18
|
+
|
11
19
|
# (see ListenerInterface#handle)
|
12
20
|
def handle(event, payload)
|
13
|
-
return unless
|
21
|
+
return unless HONEYBADGER_EVENTS.include?(event)
|
14
22
|
|
15
|
-
send(event, payload)
|
23
|
+
send(event, payload)
|
16
24
|
end
|
17
25
|
|
18
26
|
private
|
@@ -16,9 +16,9 @@ class Faulty
|
|
16
16
|
|
17
17
|
# (see ListenerInterface#handle)
|
18
18
|
def handle(event, payload)
|
19
|
-
return unless
|
19
|
+
return unless EVENT_SET.include?(event)
|
20
20
|
|
21
|
-
send(event, payload)
|
21
|
+
send(event, payload)
|
22
22
|
end
|
23
23
|
|
24
24
|
private
|
@@ -36,7 +36,7 @@ class Faulty
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def circuit_success(payload)
|
39
|
-
log(:debug, 'Circuit succeeded', payload[:circuit].name
|
39
|
+
log(:debug, 'Circuit succeeded', payload[:circuit].name)
|
40
40
|
end
|
41
41
|
|
42
42
|
def circuit_failure(payload)
|
@@ -79,8 +79,12 @@ class Faulty
|
|
79
79
|
end
|
80
80
|
|
81
81
|
def log(level, msg, action, extra = {})
|
82
|
-
|
83
|
-
|
82
|
+
@logger.public_send(level) do
|
83
|
+
extra_str = extra.map { |k, v| "#{k}=#{v}" }.join(' ')
|
84
|
+
extra_str = " #{extra_str}" unless extra_str.empty?
|
85
|
+
|
86
|
+
"#{msg}: #{action}#{extra_str}"
|
87
|
+
end
|
84
88
|
end
|
85
89
|
end
|
86
90
|
end
|
data/lib/faulty/events.rb
CHANGED
@@ -17,6 +17,8 @@ class Faulty
|
|
17
17
|
circuit_success
|
18
18
|
storage_failure
|
19
19
|
].freeze
|
20
|
+
|
21
|
+
EVENT_SET = Set.new(EVENTS)
|
20
22
|
end
|
21
23
|
end
|
22
24
|
|
@@ -24,3 +26,4 @@ require 'faulty/events/callback_listener'
|
|
24
26
|
require 'faulty/events/honeybadger_listener'
|
25
27
|
require 'faulty/events/log_listener'
|
26
28
|
require 'faulty/events/notifier'
|
29
|
+
require 'faulty/events/filter_notifier'
|
data/lib/faulty/patch/base.rb
CHANGED
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'mysql2'
|
4
|
+
|
5
|
+
if Gem::Version.new(Mysql2::VERSION) < Gem::Version.new('0.5.0')
|
6
|
+
raise NotImplementedError, 'The faulty mysql2 patch requires mysql2 0.5.0 or later'
|
7
|
+
end
|
8
|
+
|
9
|
+
class Faulty
|
10
|
+
module Patch
|
11
|
+
# Patch Mysql2 to run connections and queries in a circuit
|
12
|
+
#
|
13
|
+
# This module is not required by default
|
14
|
+
#
|
15
|
+
# Pass a `:faulty` key into your MySQL connection options to enable
|
16
|
+
# circuit protection. See {Patch.circuit_from_hash} for the available
|
17
|
+
# options.
|
18
|
+
#
|
19
|
+
# COMMIT, ROLLBACK, and RELEASE SAVEPOINT queries are intentionally not
|
20
|
+
# protected by the circuit. This is to allow open transactions to be closed
|
21
|
+
# if possible.
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# require 'faulty/patch/mysql2'
|
25
|
+
#
|
26
|
+
# mysql = Mysql2::Client.new(host: '127.0.0.1', faulty: {})
|
27
|
+
# mysql.query('SELECT * FROM users') # raises Faulty::CircuitError if connection fails
|
28
|
+
#
|
29
|
+
# # If the faulty key is not given, no circuit is used
|
30
|
+
# mysql = Mysql2::Client.new(host: '127.0.0.1')
|
31
|
+
# mysql.query('SELECT * FROM users') # not protected by a circuit
|
32
|
+
#
|
33
|
+
# @see Patch.circuit_from_hash
|
34
|
+
module Mysql2
|
35
|
+
include Base
|
36
|
+
|
37
|
+
Patch.define_circuit_errors(self, ::Mysql2::Error::ConnectionError)
|
38
|
+
|
39
|
+
QUERY_WHITELIST = [
|
40
|
+
%r{\A(?:/\*.*?\*/)?\s*ROLLBACK}i,
|
41
|
+
%r{\A(?:/\*.*?\*/)?\s*COMMIT}i,
|
42
|
+
%r{\A(?:/\*.*?\*/)?\s*RELEASE\s+SAVEPOINT}i
|
43
|
+
].freeze
|
44
|
+
|
45
|
+
def initialize(opts = {})
|
46
|
+
@faulty_circuit = Patch.circuit_from_hash(
|
47
|
+
'mysql2',
|
48
|
+
opts[:faulty],
|
49
|
+
errors: [
|
50
|
+
::Mysql2::Error::ConnectionError,
|
51
|
+
::Mysql2::Error::TimeoutError
|
52
|
+
],
|
53
|
+
patched_error_module: Faulty::Patch::Mysql2
|
54
|
+
)
|
55
|
+
|
56
|
+
super
|
57
|
+
end
|
58
|
+
|
59
|
+
# Protect manual connection pings
|
60
|
+
def ping
|
61
|
+
faulty_run { super }
|
62
|
+
rescue Faulty::Patch::Mysql2::FaultyError
|
63
|
+
false
|
64
|
+
end
|
65
|
+
|
66
|
+
# Protect the initial connnection
|
67
|
+
def connect(*args)
|
68
|
+
faulty_run { super }
|
69
|
+
end
|
70
|
+
|
71
|
+
# Protect queries unless they are whitelisted
|
72
|
+
def query(*args)
|
73
|
+
return super if QUERY_WHITELIST.any? { |r| !r.match(args.first).nil? }
|
74
|
+
|
75
|
+
faulty_run { super }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
::Mysql2::Client.prepend(Faulty::Patch::Mysql2)
|
data/lib/faulty/patch/redis.rb
CHANGED
@@ -8,13 +8,9 @@ class Faulty
|
|
8
8
|
#
|
9
9
|
# This module is not required by default
|
10
10
|
#
|
11
|
-
# Pass a `:faulty` key into your
|
12
|
-
# circuit protection.
|
13
|
-
#
|
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}.
|
11
|
+
# Pass a `:faulty` key into your MySQL connection options to enable
|
12
|
+
# circuit protection. See {Patch.circuit_from_hash} for the available
|
13
|
+
# options.
|
18
14
|
#
|
19
15
|
# @example
|
20
16
|
# require 'faulty/patch/redis'
|
@@ -32,12 +28,18 @@ class Faulty
|
|
32
28
|
|
33
29
|
Patch.define_circuit_errors(self, ::Redis::BaseConnectionError)
|
34
30
|
|
31
|
+
class BusyError < ::Redis::CommandError
|
32
|
+
end
|
33
|
+
|
35
34
|
# Patches Redis to add the `:faulty` key
|
36
35
|
def initialize(options = {})
|
37
36
|
@faulty_circuit = Patch.circuit_from_hash(
|
38
37
|
'redis',
|
39
38
|
options[:faulty],
|
40
|
-
errors: [
|
39
|
+
errors: [
|
40
|
+
::Redis::BaseConnectionError,
|
41
|
+
BusyError
|
42
|
+
],
|
41
43
|
patched_error_module: Faulty::Patch::Redis
|
42
44
|
)
|
43
45
|
|
@@ -49,10 +51,41 @@ class Faulty
|
|
49
51
|
faulty_run { super }
|
50
52
|
end
|
51
53
|
|
52
|
-
#
|
53
|
-
def
|
54
|
+
# Protect command calls
|
55
|
+
def call(command)
|
56
|
+
faulty_run { super }
|
57
|
+
end
|
58
|
+
|
59
|
+
# Protect command_loop calls
|
60
|
+
def call_loop(command, timeout = 0)
|
61
|
+
faulty_run { super }
|
62
|
+
end
|
63
|
+
|
64
|
+
# Protect pipelined commands
|
65
|
+
def call_pipelined(commands)
|
54
66
|
faulty_run { super }
|
55
67
|
end
|
68
|
+
|
69
|
+
# Inject specific error classes if client is patched
|
70
|
+
#
|
71
|
+
# This method does not raise errors, it returns them
|
72
|
+
# as exception objects, so we simply modify that error if necessary and
|
73
|
+
# return it.
|
74
|
+
#
|
75
|
+
# The call* methods above will then raise that error, so we are able to
|
76
|
+
# capture it with faulty_run.
|
77
|
+
def io(&block)
|
78
|
+
return super unless @faulty_circuit
|
79
|
+
|
80
|
+
reply = super
|
81
|
+
if reply.is_a?(::Redis::CommandError)
|
82
|
+
if reply.message.start_with?('BUSY')
|
83
|
+
reply = BusyError.new(reply.message)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
reply
|
88
|
+
end
|
56
89
|
end
|
57
90
|
end
|
58
91
|
end
|
data/lib/faulty/status.rb
CHANGED
@@ -64,10 +64,17 @@ class Faulty
|
|
64
64
|
# sample_size
|
65
65
|
# @return [Status]
|
66
66
|
def self.from_entries(entries, **hash)
|
67
|
+
window_start = Faulty.current_time - hash[:options].evaluation_window
|
68
|
+
size = entries.size
|
69
|
+
i = 0
|
67
70
|
failures = 0
|
68
71
|
sample_size = 0
|
69
|
-
|
70
|
-
|
72
|
+
|
73
|
+
# This is a hot loop, and while is slightly faster than each
|
74
|
+
while i < size
|
75
|
+
time, success = entries[i]
|
76
|
+
i += 1
|
77
|
+
next unless time > window_start
|
71
78
|
|
72
79
|
sample_size += 1
|
73
80
|
failures += 1 unless success
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Faulty
|
4
|
+
module Storage
|
5
|
+
# A no-op backend for disabling circuits
|
6
|
+
class Null
|
7
|
+
# Define a single global instance
|
8
|
+
@instance = new
|
9
|
+
|
10
|
+
def self.new
|
11
|
+
@instance
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param (see Interface#entry)
|
15
|
+
# @return (see Interface#entry)
|
16
|
+
def entry(_circuit, _time, _success)
|
17
|
+
[]
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param (see Interface#open)
|
21
|
+
# @return (see Interface#open)
|
22
|
+
def open(_circuit, _opened_at)
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param (see Interface#reopen)
|
27
|
+
# @return (see Interface#reopen)
|
28
|
+
def reopen(_circuit, _opened_at, _previous_opened_at)
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param (see Interface#close)
|
33
|
+
# @return (see Interface#close)
|
34
|
+
def close(_circuit)
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param (see Interface#lock)
|
39
|
+
# @return (see Interface#lock)
|
40
|
+
def lock(_circuit, _state)
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param (see Interface#unlock)
|
44
|
+
# @return (see Interface#unlock)
|
45
|
+
def unlock(_circuit)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param (see Interface#reset)
|
49
|
+
# @return (see Interface#reset)
|
50
|
+
def reset(_circuit)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param (see Interface#status)
|
54
|
+
# @return (see Interface#status)
|
55
|
+
def status(circuit)
|
56
|
+
Faulty::Status.new(
|
57
|
+
options: circuit.options,
|
58
|
+
stub: true
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
# @param (see Interface#history)
|
63
|
+
# @return (see Interface#history)
|
64
|
+
def history(_circuit)
|
65
|
+
[]
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param (see Interface#list)
|
69
|
+
# @return (see Interface#list)
|
70
|
+
def list
|
71
|
+
[]
|
72
|
+
end
|
73
|
+
|
74
|
+
# This backend is fault tolerant
|
75
|
+
#
|
76
|
+
# @param (see Interface#fault_tolerant?)
|
77
|
+
# @return (see Interface#fault_tolerant?)
|
78
|
+
def fault_tolerant?
|
79
|
+
true
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/faulty/storage.rb
CHANGED
@@ -10,5 +10,6 @@ require 'faulty/storage/auto_wire'
|
|
10
10
|
require 'faulty/storage/circuit_proxy'
|
11
11
|
require 'faulty/storage/fallback_chain'
|
12
12
|
require 'faulty/storage/fault_tolerant_proxy'
|
13
|
+
require 'faulty/storage/null'
|
13
14
|
require 'faulty/storage/memory'
|
14
15
|
require 'faulty/storage/redis'
|
data/lib/faulty/version.rb
CHANGED
data/lib/faulty.rb
CHANGED
@@ -128,6 +128,33 @@ class Faulty
|
|
128
128
|
def current_time
|
129
129
|
Time.now.to_i
|
130
130
|
end
|
131
|
+
|
132
|
+
# Disable Faulty circuits
|
133
|
+
#
|
134
|
+
# This allows circuits to run as if they were always closed. Does
|
135
|
+
# not disable caching.
|
136
|
+
#
|
137
|
+
# Intended for use in tests, or to disable Faulty entirely for an
|
138
|
+
# environment.
|
139
|
+
#
|
140
|
+
# @return [void]
|
141
|
+
def disable!
|
142
|
+
@disabled = true
|
143
|
+
end
|
144
|
+
|
145
|
+
# Re-enable Faulty if disabled with {#disable!}
|
146
|
+
#
|
147
|
+
# @return [void]
|
148
|
+
def enable!
|
149
|
+
@disabled = false
|
150
|
+
end
|
151
|
+
|
152
|
+
# Check whether Faulty was disabled with {#disable!}
|
153
|
+
#
|
154
|
+
# @return [Boolean] True if disabled
|
155
|
+
def disabled?
|
156
|
+
@disabled == true
|
157
|
+
end
|
131
158
|
end
|
132
159
|
|
133
160
|
attr_reader :options
|
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.7.2
|
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-09-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -38,20 +38,6 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '2.0'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: honeybadger
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - ">="
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '2.0'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - ">="
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '2.0'
|
55
41
|
- !ruby/object:Gem::Dependency
|
56
42
|
name: redis
|
57
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -138,6 +124,7 @@ files:
|
|
138
124
|
- Gemfile
|
139
125
|
- LICENSE.txt
|
140
126
|
- README.md
|
127
|
+
- bin/benchmark
|
141
128
|
- bin/check-version
|
142
129
|
- bin/console
|
143
130
|
- bin/rspec
|
@@ -160,6 +147,7 @@ files:
|
|
160
147
|
- lib/faulty/error.rb
|
161
148
|
- lib/faulty/events.rb
|
162
149
|
- lib/faulty/events/callback_listener.rb
|
150
|
+
- lib/faulty/events/filter_notifier.rb
|
163
151
|
- lib/faulty/events/honeybadger_listener.rb
|
164
152
|
- lib/faulty/events/listener_interface.rb
|
165
153
|
- lib/faulty/events/log_listener.rb
|
@@ -167,6 +155,7 @@ files:
|
|
167
155
|
- lib/faulty/immutable_options.rb
|
168
156
|
- lib/faulty/patch.rb
|
169
157
|
- lib/faulty/patch/base.rb
|
158
|
+
- lib/faulty/patch/mysql2.rb
|
170
159
|
- lib/faulty/patch/redis.rb
|
171
160
|
- lib/faulty/result.rb
|
172
161
|
- lib/faulty/status.rb
|
@@ -177,6 +166,7 @@ files:
|
|
177
166
|
- lib/faulty/storage/fault_tolerant_proxy.rb
|
178
167
|
- lib/faulty/storage/interface.rb
|
179
168
|
- lib/faulty/storage/memory.rb
|
169
|
+
- lib/faulty/storage/null.rb
|
180
170
|
- lib/faulty/storage/redis.rb
|
181
171
|
- lib/faulty/version.rb
|
182
172
|
homepage: https://github.com/ParentSquare/faulty
|