faulty 0.5.1 → 0.6.0

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: 1fe02d7203e9d26a8f859d55efa9ffbe099ab6c2cf025fe7ca831c6ccdfd684f
4
+ data.tar.gz: bed9fe698b66b367dab9066ffec98567c60fd8dd57f9ea3ed7ea3a2a3a4a3b48
5
5
  SHA512:
6
- metadata.gz: 7bf4a244b3d448ec4f7075e3e8eacd6d5777bdf8f3e3e5b033857876651a9be5cacb8f3b78eaabd0881cb18cfc754b0dfb39d5aad0a2105517abc9345612ec4c
7
- data.tar.gz: 87f5419f6f75b4efbfeb373f1217a1a505e7d0423a19227114c8700afd1937af210d9ec81b38106f7ad6ea5e0a985d2a2b9b438db01f243d32ac2771ba34a45c
6
+ metadata.gz: c69668b5b99fc2dad979031bd3a61e61d608c8f1c8ba095924709f3cea1b653a4116edcc45a72fb4d3b72e367a3e8b56d7c3cd3d56ed03aadb0ae48be6f3d7bc
7
+ data.tar.gz: 97d9ff9d9f6bb368ca395b9338bc52b83ef545f67a1f412729b52aef997f6187d57f28fbfda989584e7d26e5e300127f075699f60b888746eaa0c48a2e3b3656
@@ -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,11 @@
1
+ ## Release v0.6.0
2
+
3
+ * docs, use correct state in description for skipped event #27 senny
4
+ * Fix CI to set REDIS_VERSION correctly #31 justinhoward
5
+ * Fix a potential memory leak in patches #32 justinhoward
6
+ * Capture an error for BUSY redis backend when patched #30 justinhoward
7
+ * Add a patch for mysql2 #28 justinhoward
8
+
1
9
  ## Release v0.5.1
2
10
 
3
11
  * 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,6 +83,7 @@ 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)
@@ -948,15 +949,40 @@ Or require them in your `Gemfile`
948
949
  gem 'faulty', require: %w[faulty faulty/patch/redis]
949
950
  ```
950
951
 
952
+ For core dependencies you'll most likely want to use the in-memory circuit
953
+ storage adapter and not the Redis storage adapter. That way if Redis fails, your
954
+ circuit storage doesn't also fail, causing cascading failures.
955
+
956
+ For example, you can use a separate Faulty instance to manage your Mysql2
957
+ circuit:
958
+
959
+ ```ruby
960
+ # Setup your default config. This can use the Redis backend if you prefer
961
+ Faulty.init do |config|
962
+ # ...
963
+ end
964
+
965
+ Faulty.register(:mysql) do |config|
966
+ # Here we decide to set some circuit defaults more useful for
967
+ # frequent database calls
968
+ config.circuit_defaults = {
969
+ cool_down: 20.0,
970
+ evaluation_window: 40,
971
+ sample_threshold: 25
972
+ }
973
+ end
974
+
975
+ # Now we can use our "mysql" faulty instance when constructing a Mysql2 client
976
+ Mysql2::Client.new(host: '127.0.0.1', faulty: { instance: 'mysql2' })
977
+ ```
978
+
951
979
  ### Patch::Redis
952
980
 
953
981
  [`Faulty::Patch::Redis`](https://www.rubydoc.info/gems/faulty/Faulty/Patch/Redis)
954
982
  protects a Redis client with an internal circuit. Pass a `:faulty` key along
955
983
  with your connection options to enable the circuit breaker.
956
984
 
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.
985
+ The Redis patch supports the Redis gem versions 3 and 4.
960
986
 
961
987
  ```ruby
962
988
  require 'faulty/patch/redis'
@@ -982,6 +1008,43 @@ redis = Redis.new(url: 'redis://localhost:6379')
982
1008
  redis.connect # not protected by a circuit
983
1009
  ```
984
1010
 
1011
+ ### Patch::Mysql2
1012
+
1013
+ [`Faulty::Patch::Mysql2`](https://www.rubydoc.info/gems/faulty/Faulty/Patch/Mysql2)
1014
+ protects a `Mysql2::Client` with an internal circuit. Pass a `:faulty` key along
1015
+ with your connection options to enable the circuit breaker.
1016
+
1017
+ Faulty supports the mysql2 gem versions 0.5 and greater.
1018
+
1019
+ Note: Although Faulty supports Ruby 2.3 in general, the Mysql2 patch is not
1020
+ fully supported on Ruby 2.3. It may work for you, but use it at your own risk.
1021
+
1022
+ ```ruby
1023
+ require 'faulty/patch/mysql2'
1024
+
1025
+ mysql = Mysql2::Client.new(host: '127.0.0.1', faulty: {
1026
+ # The name for the Mysql2 circuit
1027
+ name: 'mysql2'
1028
+
1029
+ # The faulty instance to use
1030
+ # This can also be a registered faulty instance or a constant name. See API
1031
+ # docs for more details
1032
+ instance: Faulty.default
1033
+
1034
+ # By default, circuit errors will be subclasses of
1035
+ # Mysql2::Error::ConnectionError
1036
+ # To disable this behavior, set patch_errors to false and Faulty
1037
+ # will raise its default errors
1038
+ patch_errors: true
1039
+ })
1040
+
1041
+ mysql.query('SELECT * FROM users') # raises Faulty::CircuitError if connection fails
1042
+
1043
+ # If the faulty key is not given, no circuit is used
1044
+ mysql = Mysql2::Client.new(host: '127.0.0.1')
1045
+ mysql.query('SELECT * FROM users') # not protected by a circuit
1046
+ ```
1047
+
985
1048
  ## Event Handling
986
1049
 
987
1050
  Faulty uses an event-dispatching model to deliver notifications of internal
@@ -1000,7 +1063,7 @@ events. The full list of events is available from
1000
1063
  - `circuit_reopened` - A circuit execution cause the circuit to reopen from
1001
1064
  half-open. Payload: `circuit`, `error`.
1002
1065
  - `circuit_skipped` - A circuit execution was skipped because the circuit is
1003
- closed. Payload: `circuit`
1066
+ open. Payload: `circuit`
1004
1067
  - `circuit_success` - A circuit execution was successful. Payload: `circuit`,
1005
1068
  `status`
1006
1069
  - `storage_failure` - A storage backend raised an error. Payload `circuit` (can
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
@@ -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
@@ -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.6.0')
7
7
  end
8
8
  end
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.6.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: 2021-05-28 00:00:00.000000000 Z
11
+ date: 2021-06-10 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
@@ -167,6 +153,7 @@ files:
167
153
  - lib/faulty/immutable_options.rb
168
154
  - lib/faulty/patch.rb
169
155
  - lib/faulty/patch/base.rb
156
+ - lib/faulty/patch/mysql2.rb
170
157
  - lib/faulty/patch/redis.rb
171
158
  - lib/faulty/result.rb
172
159
  - lib/faulty/status.rb