faulty 0.3.0 → 0.4.0

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