faulty 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/faulty.gemspec CHANGED
@@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
27
27
  # Other non-essential development dependencies go in the Gemfile.
28
28
  spec.add_development_dependency 'connection_pool', '~> 2.0'
29
29
  spec.add_development_dependency 'honeybadger', '>= 2.0'
30
- spec.add_development_dependency 'redis', '~> 3.0'
30
+ spec.add_development_dependency 'redis', '>= 3.0'
31
31
  spec.add_development_dependency 'rspec', '~> 3.8'
32
32
  # 0.81 is the last rubocop version with Ruby 2.3 support
33
33
  spec.add_development_dependency 'rubocop', '0.81.0'
data/lib/faulty.rb CHANGED
@@ -129,6 +129,10 @@ class Faulty
129
129
  # @return [Cache::Interface] A cache backend if you want
130
130
  # to use Faulty's cache support. Automatically wrapped in a
131
131
  # {Cache::AutoWire}. Default `Cache::AutoWire.new`.
132
+ # @!attribute [r] circuit_defaults
133
+ # @see Circuit::Options
134
+ # @return [Hash] A hash of default options to be used when creating
135
+ # new circuits. See {Circuit::Options} for a full list.
132
136
  # @!attribute [r] storage
133
137
  # @see Storage::AutoWire
134
138
  # @return [Storage::Interface, Array<Storage::Interface>] The storage
@@ -142,6 +146,7 @@ class Faulty
142
146
  # ignored.
143
147
  Options = Struct.new(
144
148
  :cache,
149
+ :circuit_defaults,
145
150
  :storage,
146
151
  :listeners,
147
152
  :notifier
@@ -152,16 +157,17 @@ class Faulty
152
157
 
153
158
  def finalize
154
159
  self.notifier ||= Events::Notifier.new(listeners || [])
155
- self.storage = Storage::AutoWire.new(storage, notifier: notifier)
156
- self.cache = Cache::AutoWire.new(cache, notifier: notifier)
160
+ self.storage = Storage::AutoWire.wrap(storage, notifier: notifier)
161
+ self.cache = Cache::AutoWire.wrap(cache, notifier: notifier)
157
162
  end
158
163
 
159
164
  def required
160
- %i[cache storage notifier]
165
+ %i[cache circuit_defaults storage notifier]
161
166
  end
162
167
 
163
168
  def defaults
164
169
  {
170
+ circuit_defaults: {},
165
171
  listeners: [Events::LogListener.new]
166
172
  }
167
173
  end
@@ -200,8 +206,8 @@ class Faulty
200
206
  # @return [Circuit] The new circuit or the existing circuit if it already exists
201
207
  def circuit(name, **options, &block)
202
208
  name = name.to_s
203
- options = options.merge(circuit_options)
204
209
  @circuits.compute_if_absent(name) do
210
+ options = circuit_options.merge(options)
205
211
  Circuit.new(name, **options, &block)
206
212
  end
207
213
  end
@@ -219,6 +225,8 @@ class Faulty
219
225
  #
220
226
  # @return [Hash] The circuit options
221
227
  def circuit_options
222
- @options.to_h.select { |k, _v| %i[cache storage notifier].include?(k) }
228
+ @options.to_h
229
+ .select { |k, _v| %i[cache storage notifier].include?(k) }
230
+ .merge(options.circuit_defaults)
223
231
  end
224
232
  end
@@ -6,10 +6,17 @@ class Faulty
6
6
  #
7
7
  # Used by {Faulty#initialize} to setup sensible cache defaults
8
8
  class AutoWire
9
- extend Forwardable
10
-
11
9
  # Options for {AutoWire}
10
+ #
11
+ # @!attribute [r] circuit
12
+ # @return [Circuit] A circuit for {CircuitProxy} if one is created.
13
+ # When modifying this, be careful to use only a reliable circuit
14
+ # storage backend so that you don't introduce cascading failures.
15
+ # @!attribute [r] notifier
16
+ # @return [Events::Notifier] A Faulty notifier. If given, listeners are
17
+ # ignored.
12
18
  Options = Struct.new(
19
+ :circuit,
13
20
  :notifier
14
21
  ) do
15
22
  include ImmutableOptions
@@ -21,44 +28,30 @@ class Faulty
21
28
  end
22
29
  end
23
30
 
24
- # Wrap a cache backend with sensible defaults
25
- #
26
- # If the cache is `nil`, create a new {Default}.
27
- #
28
- # If the backend is not fault tolerant, wrap it in {CircuitProxy} and
29
- # {FaultTolerantProxy}.
30
- #
31
- # @param cache [Interface] A cache backend
32
- # @param options [Hash] Attributes for {Options}
33
- # @yield [Options] For setting options in a block
34
- def initialize(cache, **options, &block)
35
- @options = Options.new(options, &block)
36
- @cache = if cache.nil?
37
- Cache::Default.new
38
- elsif cache.fault_tolerant?
39
- cache
40
- else
41
- Cache::FaultTolerantProxy.new(
42
- Cache::CircuitProxy.new(cache, notifier: @options.notifier),
43
- notifier: @options.notifier
44
- )
31
+ class << self
32
+ # Wrap a cache backend with sensible defaults
33
+ #
34
+ # If the cache is `nil`, create a new {Default}.
35
+ #
36
+ # If the backend is not fault tolerant, wrap it in {CircuitProxy} and
37
+ # {FaultTolerantProxy}.
38
+ #
39
+ # @param cache [Interface] A cache backend
40
+ # @param options [Hash] Attributes for {Options}
41
+ # @yield [Options] For setting options in a block
42
+ def wrap(cache, **options, &block)
43
+ options = Options.new(options, &block)
44
+ if cache.nil?
45
+ Cache::Default.new
46
+ elsif cache.fault_tolerant?
47
+ cache
48
+ else
49
+ Cache::FaultTolerantProxy.new(
50
+ Cache::CircuitProxy.new(cache, circuit: options.circuit, notifier: options.notifier),
51
+ notifier: options.notifier
52
+ )
53
+ end
45
54
  end
46
-
47
- freeze
48
- end
49
-
50
- # @!method read(key)
51
- # (see Faulty::Cache::Interface#read)
52
- #
53
- # @!method write(key, value, expires_in: expires_in)
54
- # (see Faulty::Cache::Interface#write)
55
- def_delegators :@cache, :read, :write
56
-
57
- # Auto-wired caches are always fault tolerant
58
- #
59
- # @return [true]
60
- def fault_tolerant?
61
- true
62
55
  end
63
56
  end
64
57
  end
@@ -72,11 +72,10 @@ class Faulty
72
72
  end
73
73
 
74
74
  def storage_failure(payload)
75
- log(
76
- :error, 'Storage failure', payload[:action],
77
- circuit: payload[:circuit]&.name,
78
- error: payload[:error].message
79
- )
75
+ extra = {}
76
+ extra[:circuit] = payload[:circuit].name if payload.key?(:circuit)
77
+ extra[:error] = payload[:error].message
78
+ log(:error, 'Storage failure', payload[:action], extra)
80
79
  end
81
80
 
82
81
  def log(level, msg, action, extra = {})
data/lib/faulty/status.rb CHANGED
@@ -144,9 +144,10 @@ class Faulty
144
144
 
145
145
  def finalize
146
146
  raise ArgumentError, "state must be a symbol in #{self.class}::STATES" unless STATES.include?(state)
147
- unless lock.nil? || LOCKS.include?(state)
147
+ unless lock.nil? || LOCKS.include?(lock)
148
148
  raise ArgumentError, "lock must be a symbol in #{self.class}::LOCKS or nil"
149
149
  end
150
+ raise ArgumentError, 'opened_at is required if state is open' if state == :open && opened_at.nil?
150
151
  end
151
152
 
152
153
  def required
@@ -6,10 +6,17 @@ class Faulty
6
6
  #
7
7
  # Used by {Faulty#initialize} to setup sensible storage defaults
8
8
  class AutoWire
9
- extend Forwardable
10
-
11
9
  # Options for {AutoWire}
10
+ #
11
+ # @!attribute [r] circuit
12
+ # @return [Circuit] A circuit for {CircuitProxy} if one is created.
13
+ # When modifying this, be careful to use only a reliable circuit
14
+ # storage backend so that you don't introduce cascading failures.
15
+ # @!attribute [r] notifier
16
+ # @return [Events::Notifier] A Faulty notifier. If given, listeners are
17
+ # ignored.
12
18
  Options = Struct.new(
19
+ :circuit,
13
20
  :notifier
14
21
  ) do
15
22
  include ImmutableOptions
@@ -21,101 +28,79 @@ class Faulty
21
28
  end
22
29
  end
23
30
 
24
- # Wrap storage backends with sensible defaults
25
- #
26
- # If the cache is `nil`, create a new {Memory} storage.
27
- #
28
- # If a single storage backend is given and is fault tolerant, leave it
29
- # unmodified.
30
- #
31
- # If a single storage backend is given and is not fault tolerant, wrap it
32
- # in a {CircuitProxy} and a {FaultTolerantProxy}.
33
- #
34
- # If an array of storage backends is given, wrap each non-fault-tolerant
35
- # entry in a {CircuitProxy} and create a {FallbackChain}. If none of the
36
- # backends in the array are fault tolerant, also wrap the {FallbackChain}
37
- # in a {FaultTolerantProxy}.
38
- #
39
- # @todo Consider using a {FallbackChain} for non-fault-tolerant storages
40
- # by default. This would fallback to a {Memory} storage. It would
41
- # require a more conservative implementation of {Memory} that could
42
- # limit the number of circuits stored. For now, users need to manually
43
- # configure fallbacks.
44
- #
45
- # @param storage [Interface, Array<Interface>] A storage backed or array
46
- # of storage backends to setup.
47
- # @param options [Hash] Attributes for {Options}
48
- # @yield [Options] For setting options in a block
49
- def initialize(storage, **options, &block)
50
- @options = Options.new(options, &block)
51
- @storage = if storage.nil?
52
- Memory.new
53
- elsif storage.is_a?(Array)
54
- wrap_array(storage)
55
- elsif !storage.fault_tolerant?
56
- wrap_one(storage)
57
- else
58
- storage
31
+ class << self
32
+ # Wrap storage backends with sensible defaults
33
+ #
34
+ # If the cache is `nil`, create a new {Memory} storage.
35
+ #
36
+ # If a single storage backend is given and is fault tolerant, leave it
37
+ # unmodified.
38
+ #
39
+ # If a single storage backend is given and is not fault tolerant, wrap it
40
+ # in a {CircuitProxy} and a {FaultTolerantProxy}.
41
+ #
42
+ # If an array of storage backends is given, wrap each non-fault-tolerant
43
+ # entry in a {CircuitProxy} and create a {FallbackChain}. If none of the
44
+ # backends in the array are fault tolerant, also wrap the {FallbackChain}
45
+ # in a {FaultTolerantProxy}.
46
+ #
47
+ # @todo Consider using a {FallbackChain} for non-fault-tolerant storages
48
+ # by default. This would fallback to a {Memory} storage. It would
49
+ # require a more conservative implementation of {Memory} that could
50
+ # limit the number of circuits stored. For now, users need to manually
51
+ # configure fallbacks.
52
+ #
53
+ # @param storage [Interface, Array<Interface>] A storage backed or array
54
+ # of storage backends to setup.
55
+ # @param options [Hash] Attributes for {Options}
56
+ # @yield [Options] For setting options in a block
57
+ def wrap(storage, **options, &block)
58
+ options = Options.new(options, &block)
59
+ if storage.nil?
60
+ Memory.new
61
+ elsif storage.is_a?(Array)
62
+ wrap_array(storage, options)
63
+ elsif !storage.fault_tolerant?
64
+ wrap_one(storage, options)
65
+ else
66
+ storage
67
+ end
59
68
  end
60
69
 
61
- freeze
62
- end
63
-
64
- # @!method entry(circuit, time, success)
65
- # (see Faulty::Storage::Interface#entry)
66
- #
67
- # @!method open(circuit, opened_at)
68
- # (see Faulty::Storage::Interface#open)
69
- #
70
- # @!method reopen(circuit, opened_at, previous_opened_at)
71
- # (see Faulty::Storage::Interface#reopen)
72
- #
73
- # @!method close(circuit)
74
- # (see Faulty::Storage::Interface#close)
75
- #
76
- # @!method lock(circuit, state)
77
- # (see Faulty::Storage::Interface#lock)
78
- #
79
- # @!method unlock(circuit)
80
- # (see Faulty::Storage::Interface#unlock)
81
- #
82
- # @!method reset(circuit)
83
- # (see Faulty::Storage::Interface#reset)
84
- #
85
- # @!method status(circuit)
86
- # (see Faulty::Storage::Interface#status)
87
- #
88
- # @!method history(circuit)
89
- # (see Faulty::Storage::Interface#history)
90
- #
91
- # @!method list
92
- # (see Faulty::Storage::Interface#list)
93
- #
94
- def_delegators :@storage,
95
- :entry, :open, :reopen, :close, :lock,
96
- :unlock, :reset, :status, :history, :list
97
-
98
- def fault_tolerant?
99
- true
100
- end
70
+ private
101
71
 
102
- private
72
+ # Wrap an array of storage backends in a fault-tolerant FallbackChain
73
+ #
74
+ # @param [Array<Storage::Interface>] The array to wrap
75
+ # @param options [Options]
76
+ # @return [Storage::Interface] A fault-tolerant fallback chain
77
+ def wrap_array(array, options)
78
+ FaultTolerantProxy.wrap(FallbackChain.new(
79
+ array.map { |s| s.fault_tolerant? ? s : circuit_proxy(s, options) },
80
+ notifier: options.notifier
81
+ ), notifier: options.notifier)
82
+ end
103
83
 
104
- # Wrap an array of storage backends in a fault-tolerant FallbackChain
105
- #
106
- # @return [Storage::Interface] A fault-tolerant fallback chain
107
- def wrap_array(array)
108
- FaultTolerantProxy.wrap(FallbackChain.new(
109
- array.map { |s| s.fault_tolerant? ? s : CircuitProxy.new(s, notifier: @options.notifier) },
110
- notifier: @options.notifier
111
- ), notifier: @options.notifier)
112
- end
84
+ # Wrap one storage backend in fault-tolerant backends
85
+ #
86
+ # @param [Storage::Interface] The storage to wrap
87
+ # @param options [Options]
88
+ # @return [Storage::Interface] A fault-tolerant storage backend
89
+ def wrap_one(storage, options)
90
+ FaultTolerantProxy.new(
91
+ circuit_proxy(storage, options),
92
+ notifier: options.notifier
93
+ )
94
+ end
113
95
 
114
- def wrap_one(storage)
115
- FaultTolerantProxy.new(
116
- CircuitProxy.new(storage, notifier: @options.notifier),
117
- notifier: @options.notifier
118
- )
96
+ # Wrap storage in a CircuitProxy
97
+ #
98
+ # @param [Storage::Interface] The storage to wrap
99
+ # @param options [Options]
100
+ # @return [CircuitProxy]
101
+ def circuit_proxy(storage, options)
102
+ CircuitProxy.new(storage, circuit: options.circuit, notifier: options.notifier)
103
+ end
119
104
  end
120
105
  end
121
106
  end
@@ -360,7 +360,7 @@ class Faulty
360
360
  end
361
361
 
362
362
  def check_redis_options!
363
- ropts = redis { |r| r.client.options }
363
+ ropts = redis { |r| r.instance_variable_get(:@client).options }
364
364
 
365
365
  bad_timeouts = {}
366
366
  %i[connect_timeout read_timeout write_timeout].each do |time_opt|
@@ -384,7 +384,7 @@ class Faulty
384
384
  end
385
385
 
386
386
  def check_pool_options!
387
- if options.client.is_a?(ConnectionPool)
387
+ if options.client.class.name == 'ConnectionPool'
388
388
  timeout = options.client.instance_variable_get(:@timeout)
389
389
  warn(<<~MSG) if timeout > 2
390
390
  Faulty recommends setting ConnectionPool timeouts <= 2 to prevent
@@ -3,6 +3,6 @@
3
3
  class Faulty
4
4
  # The current Faulty version
5
5
  def self.version
6
- Gem::Version.new('0.3.0')
6
+ Gem::Version.new('0.4.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.3.0
4
+ version: 0.4.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: 2020-10-24 00:00:00.000000000 Z
11
+ date: 2021-02-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -56,14 +56,14 @@ dependencies:
56
56
  name: redis
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
61
  version: '3.0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - "~>"
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '3.0'
69
69
  - !ruby/object:Gem::Dependency
@@ -129,10 +129,10 @@ executables: []
129
129
  extensions: []
130
130
  extra_rdoc_files: []
131
131
  files:
132
+ - ".github/workflows/ci.yml"
132
133
  - ".gitignore"
133
134
  - ".rspec"
134
135
  - ".rubocop.yml"
135
- - ".travis.yml"
136
136
  - ".yardopts"
137
137
  - CHANGELOG.md
138
138
  - Gemfile
@@ -195,7 +195,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
195
195
  - !ruby/object:Gem::Version
196
196
  version: '0'
197
197
  requirements: []
198
- rubygems_version: 3.0.8
198
+ rubyforge_project:
199
+ rubygems_version: 2.7.6
199
200
  signing_key:
200
201
  specification_version: 4
201
202
  summary: Fault-tolerance tools for ruby based on circuit-breakers