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 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