faulty 0.5.1 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95e52bcb5c7e0ea6975567ff5d5ce3e5d00e15f0f7a4dd4c059e1998060c242e
4
- data.tar.gz: 8ef55ce3375edcd1a14314baec3e3641b19a2a51da1a838e568a2dd1eff761a7
3
+ metadata.gz: d963d5c58861cc9e39c3222b5dc78a2a537c15fc10e0ed76d9d5b41a6704356b
4
+ data.tar.gz: e31811fc1b5be36975982e966106ea7f323b4e334b87cfaca85667f550245584
5
5
  SHA512:
6
- metadata.gz: 7bf4a244b3d448ec4f7075e3e8eacd6d5777bdf8f3e3e5b033857876651a9be5cacb8f3b78eaabd0881cb18cfc754b0dfb39d5aad0a2105517abc9345612ec4c
7
- data.tar.gz: 87f5419f6f75b4efbfeb373f1217a1a505e7d0423a19227114c8700afd1937af210d9ec81b38106f7ad6ea5e0a985d2a2b9b438db01f243d32ac2771ba34a45c
6
+ metadata.gz: 52aae6a3997fca7d38aebd124399747ee21a687114076e5d73c135d86ab147360e71a9f7a95ee4a5f548b6709089697e77fa472730c3fc18cb9c1d412bdac4ca
7
+ data.tar.gz: 56b02cad8fe96373c916746bfc7c6f83665a74e7a3771d7b761244373b1d4b79aeb44d94fbe9401f82f8671b4e21c625a079b4845a42565dfbfbdfd37a07d2bf
@@ -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
- 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.
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
- closed. Payload: `circuit`
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
@@ -33,7 +33,7 @@ class Faulty
33
33
 
34
34
  self.circuit ||= Circuit.new(
35
35
  Faulty::Storage::CircuitProxy.name,
36
- notifier: notifier,
36
+ notifier: Events::FilterNotifier.new(notifier, exclude: %i[circuit_success]),
37
37
  cache: Cache::Null.new
38
38
  )
39
39
  end
@@ -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
- entries = storage.entry(self, Faulty.current_time, true)
323
- status = Status.from_entries(entries, **status.to_h)
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, status: status)
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 EVENTS.include?(event)
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 EVENTS.include?(event)
21
+ return unless HONEYBADGER_EVENTS.include?(event)
14
22
 
15
- send(event, payload) if respond_to?(event, true)
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 EVENTS.include?(event)
19
+ return unless EVENT_SET.include?(event)
20
20
 
21
- send(event, payload) if respond_to?(event, true)
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, state: payload[:status].state)
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
- extra_str = extra.map { |k, v| "#{k}=#{v}" }.join(' ')
83
- logger.public_send(level, "#{msg}: #{action} #{extra_str}")
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'
@@ -39,7 +39,7 @@ class Faulty
39
39
  Thread.current[faulty_running_key] = true
40
40
  @faulty_circuit.run { yield }
41
41
  ensure
42
- Thread.current[faulty_running_key] = false
42
+ Thread.current[faulty_running_key] = nil
43
43
  end
44
44
  end
45
45
  end
@@ -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)
@@ -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 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}.
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: [::Redis::BaseConnectionError],
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
- # Reads/writes to redis are protected
53
- def io(&block)
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
- entries.each do |(time, success)|
70
- next unless time > Faulty.current_time - hash[:options].evaluation_window
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
@@ -33,7 +33,7 @@ class Faulty
33
33
 
34
34
  self.circuit ||= Circuit.new(
35
35
  Faulty::Storage::CircuitProxy.name,
36
- notifier: notifier,
36
+ notifier: Events::FilterNotifier.new(notifier, exclude: %i[circuit_success]),
37
37
  cache: Cache::Null.new
38
38
  )
39
39
  end
@@ -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
@@ -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'
@@ -3,6 +3,6 @@
3
3
  class Faulty
4
4
  # The current Faulty version
5
5
  def self.version
6
- Gem::Version.new('0.5.1')
6
+ Gem::Version.new('0.7.2')
7
7
  end
8
8
  end
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.5.1
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-05-28 00:00:00.000000000 Z
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