faulty 0.8.4 → 0.8.5

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: 674bada595aee5985be78855425adc16fdb25faa2741cc548758e13ea002bb5a
4
- data.tar.gz: 9144ec1b6051acbba1d4cc8cccd792826bd3aee831d4fa04c13b41a860150244
3
+ metadata.gz: 54ef7d6bc1b23815e02563c80e4c17fef2735812a8c424882e8283996506edcc
4
+ data.tar.gz: c4c2d074b118f3077a3cddb862fddc5c3ace18c62a206e4523cfb830b7b6e35b
5
5
  SHA512:
6
- metadata.gz: 75a8f4cf1608f65f326c66c3682826542e469891cbad97ba31b3854e75899f1e694dc5925e072582c5ac35ce29c9838535f0aca0e17d24c05a2bc5b9796cecae
7
- data.tar.gz: faa2361bc38bcb52917491eee6c499ef16114f100d844385e6eb799f531097e45780d2fc0db31447330cc1a3ef982fdd4bbfa45bb5a53fdec1ef776b7ae1f010
6
+ metadata.gz: 75fa840ff865e39e381894bdce59ab13f67ac001cbae57696360692c495b8d0ca2f643261a7a4ea083fdb1964d8e14ad0237815f90e9b436d17eff04a0abe0f5
7
+ data.tar.gz: dfcaede2d29e34bd466a21ad5037ddaeebfaa947bb44f09ef9c64b34ac0f207dd2a8e4d038067b47ce3ac76ccbba744721dc1fb4337854e7e9af0df739798169
@@ -37,6 +37,9 @@ jobs:
37
37
  bundler-cache: true
38
38
  - run: bundle exec rubocop
39
39
  if: matrix.ruby == '2.7'
40
+ - run: bin/yardoc --fail-on-warning
41
+ if: matrix.ruby == '2.7'
42
+ - run: bin/check-version
40
43
  - name: start MySQL
41
44
  run: sudo /etc/init.d/mysql start
42
45
  - run: bundle exec rspec --format doc
@@ -48,7 +51,6 @@ jobs:
48
51
  with:
49
52
  project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
50
53
  coverage-reports: coverage/lcov/faulty.lcov
51
- - run: bin/check-version
52
54
 
53
55
  release:
54
56
  needs: test
data/.yardopts CHANGED
@@ -1,3 +1,3 @@
1
1
  --markup markdown
2
2
  --markup-provider redcarpet
3
- - guides/*
3
+ - LICENSE.txt CHANGELOG.md
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## Release v0.8.5
2
+
3
+ * Fix yard warnings #49 justinhoward
4
+ * Fix crash in Redis storage backend if opened_at was missing #46 justinhoward
5
+ * Add granular errors for Elasticsearch patch #48 justinhoward
6
+ * Return status conditionally for Storage::Interface#entry #45 justinhoward
7
+
1
8
  ## Release v0.8.4
2
9
 
3
10
  * Add Elasticsearch client patch #44 justinhoward
@@ -49,9 +49,13 @@ class Faulty
49
49
  # @!attribute [r] cool_down
50
50
  # @return [Integer] The number of seconds the circuit will
51
51
  # stay open after it is tripped. Default 300.
52
- # @!attribute [r] error_module
53
- # @return [Module] Used by patches to set the namespace module for
54
- # the faulty errors that will be raised. Default `Faulty`
52
+ # @!attribute [r] error_mapper
53
+ # @return [Module, #call] Used by patches to set the namespace module for
54
+ # the faulty errors that will be raised. Should be a module or a callable.
55
+ # If given a module, the circuit assumes the module has error classes
56
+ # in that module. If given an object that responds to `#call` (a proc
57
+ # or lambda), the return value of the callable will be used. The callable
58
+ # is called with (`error_name`, `cause_error`, `circuit`). Default `Faulty`
55
59
  # @!attribute [r] evaluation_window
56
60
  # @return [Integer] The number of seconds of history that
57
61
  # will be evaluated to determine the failure rate for a circuit.
@@ -93,6 +97,7 @@ class Faulty
93
97
  :rate_threshold,
94
98
  :sample_threshold,
95
99
  :errors,
100
+ :error_mapper,
96
101
  :error_module,
97
102
  :exclude,
98
103
  :cache,
@@ -120,7 +125,7 @@ class Faulty
120
125
  cache_refreshes_after: 900,
121
126
  cool_down: 300,
122
127
  errors: [StandardError],
123
- error_module: Faulty,
128
+ error_mapper: Faulty,
124
129
  exclude: [],
125
130
  evaluation_window: 60,
126
131
  rate_threshold: 0.5,
@@ -133,7 +138,7 @@ class Faulty
133
138
  cache
134
139
  cool_down
135
140
  errors
136
- error_module
141
+ error_mapper
137
142
  exclude
138
143
  evaluation_window
139
144
  rate_threshold
@@ -153,6 +158,17 @@ class Faulty
153
158
  unless cache_refreshes_after.nil?
154
159
  self.cache_refresh_jitter = 0.2 * cache_refreshes_after
155
160
  end
161
+
162
+ deprecated_error_module
163
+ end
164
+
165
+ private
166
+
167
+ def deprecated_error_module
168
+ return unless error_module
169
+
170
+ Deprecation.method(self.class, :error_module, note: 'See :error_mapper', sunset: '0.9.0')
171
+ self.error_mapper = error_module
156
172
  end
157
173
  end
158
174
 
@@ -379,7 +395,7 @@ class Faulty
379
395
  # @return The result from cache if available
380
396
  def run_skipped(cached_value)
381
397
  skipped!
382
- raise options.error_module::OpenCircuitError.new(nil, self) if cached_value.nil?
398
+ raise map_error(:OpenCircuitError) if cached_value.nil?
383
399
 
384
400
  cached_value
385
401
  end
@@ -400,9 +416,9 @@ class Faulty
400
416
  opened = failure!(status, e)
401
417
  if cached_value.nil?
402
418
  if opened
403
- raise options.error_module::CircuitTrippedError.new(e.message, self)
419
+ raise map_error(:CircuitTrippedError, e)
404
420
  else
405
- raise options.error_module::CircuitFailureError.new(e.message, self)
421
+ raise map_error(:CircuitFailureError, e)
406
422
  end
407
423
  else
408
424
  cached_value
@@ -411,7 +427,11 @@ class Faulty
411
427
 
412
428
  # @return [Boolean] True if the circuit transitioned to closed
413
429
  def success!(status)
414
- storage.entry(self, Faulty.current_time, true)
430
+ if deprecated_entry?
431
+ storage.entry(self, Faulty.current_time, true, nil)
432
+ else
433
+ storage.entry(self, Faulty.current_time, true)
434
+ end
415
435
  closed = close! if status.half_open?
416
436
 
417
437
  options.notifier.notify(:circuit_success, circuit: self)
@@ -420,8 +440,11 @@ class Faulty
420
440
 
421
441
  # @return [Boolean] True if the circuit transitioned to open
422
442
  def failure!(status, error)
423
- entries = storage.entry(self, Faulty.current_time, false)
424
- status = Status.from_entries(entries, **status.to_h)
443
+ status = if deprecated_entry?
444
+ storage.entry(self, Faulty.current_time, false, status)
445
+ else
446
+ deprecated_entry(status)
447
+ end
425
448
  options.notifier.notify(:circuit_failure, circuit: self, status: status, error: error)
426
449
 
427
450
  opened = if status.half_open?
@@ -435,6 +458,23 @@ class Faulty
435
458
  opened
436
459
  end
437
460
 
461
+ def deprecated_entry?
462
+ return @deprecated_entry unless @deprecated_entry.nil?
463
+
464
+ @deprecated_entry = storage.method(:entry).arity == 4
465
+ end
466
+
467
+ def deprecated_entry(status)
468
+ Faulty::Deprecation.deprecate(
469
+ 'Returning entries array from entry',
470
+ note: 'see Storate::Interface#entry',
471
+ sunset: '0.9'
472
+ )
473
+
474
+ entries = storage.entry(self, Faulty.current_time, false)
475
+ Status.from_entries(entries, **status.to_h)
476
+ end
477
+
438
478
  def skipped!
439
479
  options.notifier.notify(:circuit_skipped, circuit: self)
440
480
  end
@@ -531,5 +571,13 @@ class Faulty
531
571
 
532
572
  @given_options.storage
533
573
  end
574
+
575
+ def map_error(error_name, cause = nil)
576
+ if options.error_mapper.respond_to?(:call)
577
+ options.error_mapper.call(error_name, cause, self)
578
+ else
579
+ options.error_mapper.const_get(error_name).new(cause&.message, self)
580
+ end
581
+ end
534
582
  end
535
583
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Faulty
4
+ # Support deprecating Faulty features
5
+ module Deprecation
6
+ class << self
7
+ # Call to raise errors instead of logging warnings for Faulty deprecations
8
+ def raise_errors!(enabled = true)
9
+ @raise_errors = (enabled == true)
10
+ end
11
+
12
+ def silenced
13
+ @silence = true
14
+ yield
15
+ ensure
16
+ @silence = false
17
+ end
18
+
19
+ # @private
20
+ def method(klass, name, note: nil, sunset: nil)
21
+ deprecate("#{klass}##{name}", note: note, sunset: sunset)
22
+ end
23
+
24
+ # @private
25
+ def deprecate(subject, note: nil, sunset: nil)
26
+ return if @silence
27
+
28
+ message = "#{subject} is deprecated"
29
+ message += " and will be removed in #{sunset}" if sunset
30
+ message += " (#{note})" if note
31
+ raise DeprecationError, message if @raise_errors
32
+
33
+ Kernel.warn("DEPRECATION: #{message}")
34
+ end
35
+ end
36
+ end
37
+ end
data/lib/faulty/error.rb CHANGED
@@ -28,6 +28,9 @@ class Faulty
28
28
  end
29
29
  end
30
30
 
31
+ class DeprecationError < FaultyError
32
+ end
33
+
31
34
  # Included in faulty circuit errors to provide common features for
32
35
  # native and patched errors
33
36
  module CircuitErrorBase
@@ -36,7 +36,32 @@ class Faulty
36
36
  module Elasticsearch
37
37
  include Base
38
38
 
39
- Patch.define_circuit_errors(self, ::Elasticsearch::Transport::Transport::Error)
39
+ module Error; end
40
+ module SnifferTimeoutError; end
41
+ module ServerError; end
42
+
43
+ # We will freeze this after adding the dynamic error classes
44
+ MAPPED_ERRORS = { # rubocop:disable Style/MutableConstant
45
+ ::Elasticsearch::Transport::Transport::Error => Error,
46
+ ::Elasticsearch::Transport::Transport::SnifferTimeoutError => SnifferTimeoutError,
47
+ ::Elasticsearch::Transport::Transport::ServerError => ServerError
48
+ }
49
+
50
+ module Errors
51
+ ::Elasticsearch::Transport::Transport::ERRORS.each do |_code, klass|
52
+ MAPPED_ERRORS[klass] = const_set(klass.name.split('::').last, Module.new)
53
+ end
54
+ end
55
+
56
+ MAPPED_ERRORS.freeze
57
+ MAPPED_ERRORS.each do |klass, mod|
58
+ Patch.define_circuit_errors(mod, klass)
59
+ end
60
+
61
+ ERROR_MAPPER = lambda do |error_name, cause, circuit|
62
+ MAPPED_ERRORS.fetch(cause&.class, Error).const_get(error_name).new(cause&.message, circuit)
63
+ end
64
+ private_constant :ERROR_MAPPER, :MAPPED_ERRORS
40
65
 
41
66
  def initialize(arguments = {}, &block)
42
67
  super
@@ -48,7 +73,8 @@ class Faulty
48
73
  'elasticsearch',
49
74
  arguments[:faulty],
50
75
  errors: errors,
51
- patched_error_module: Faulty::Patch::Elasticsearch
76
+ exclude: ::Elasticsearch::Transport::Transport::Errors::NotFound,
77
+ patched_error_mapper: ERROR_MAPPER
52
78
  )
53
79
  end
54
80
 
@@ -60,4 +86,10 @@ class Faulty
60
86
  end
61
87
  end
62
88
 
63
- ::Elasticsearch::Transport::Client.prepend(Faulty::Patch::Elasticsearch)
89
+ module Elasticsearch
90
+ module Transport
91
+ class Client
92
+ prepend(Faulty::Patch::Elasticsearch)
93
+ end
94
+ end
95
+ end
@@ -53,7 +53,7 @@ class Faulty
53
53
  ::Mysql2::Error::ConnectionError,
54
54
  ::Mysql2::Error::TimeoutError
55
55
  ],
56
- patched_error_module: Faulty::Patch::Mysql2
56
+ patched_error_mapper: Faulty::Patch::Mysql2
57
57
  )
58
58
 
59
59
  super
@@ -81,4 +81,8 @@ class Faulty
81
81
  end
82
82
  end
83
83
 
84
- ::Mysql2::Client.prepend(Faulty::Patch::Mysql2)
84
+ module Mysql2
85
+ class Client
86
+ prepend(Faulty::Patch::Mysql2)
87
+ end
88
+ end
@@ -43,7 +43,7 @@ class Faulty
43
43
  ::Redis::BaseConnectionError,
44
44
  BusyError
45
45
  ],
46
- patched_error_module: Faulty::Patch::Redis
46
+ patched_error_mapper: Faulty::Patch::Redis
47
47
  )
48
48
 
49
49
  super
@@ -93,4 +93,8 @@ class Faulty
93
93
  end
94
94
  end
95
95
 
96
- ::Redis::Client.prepend(Faulty::Patch::Redis)
96
+ class Redis
97
+ class Client
98
+ prepend(Faulty::Patch::Redis)
99
+ end
100
+ end
data/lib/faulty/patch.rb CHANGED
@@ -64,14 +64,15 @@ class Faulty
64
64
  # option and these additional options
65
65
  # @option hash [String] :name The circuit name. Defaults to `default_name`
66
66
  # @option hash [Boolean] :patch_errors By default, circuit errors will be
67
- # subclasses of `options[:patched_error_module]`. The user can disable
67
+ # subclasses of `options[:patched_error_mapper]`. The user can disable
68
68
  # this by setting this option to false.
69
69
  # @option hash [Faulty, String, Symbol, Hash{ constant: String }] :instance
70
70
  # A reference to a faulty instance. See examples.
71
71
  # @param options [Hash] Additional override options. Supports any circuit
72
72
  # option and these additional ones.
73
- # @option options [Module] :patched_error_module The namespace module
74
- # for patched errors
73
+ # @option options [Module] :patched_error_mapper The namespace module
74
+ # for patched errors or a mapping proc. See {Faulty::Circuit::Options}
75
+ # `:error_mapper`
75
76
  # @yield [Circuit::Options] For setting override options in a block
76
77
  # @return [Circuit, nil] The circuit if one was created
77
78
  def circuit_from_hash(default_name, hash, **options, &block)
@@ -80,8 +81,8 @@ class Faulty
80
81
  hash = symbolize_keys(hash)
81
82
  name = hash.delete(:name) || default_name
82
83
  patch_errors = hash.delete(:patch_errors) != false
83
- error_module = options.delete(:patched_error_module)
84
- hash[:error_module] ||= error_module if error_module && patch_errors
84
+ error_mapper = options.delete(:patched_error_mapper)
85
+ hash[:error_mapper] ||= error_mapper if error_mapper && patch_errors
85
86
  faulty = resolve_instance(hash.delete(:instance))
86
87
  faulty.circuit(name, **hash, **options, &block)
87
88
  end
@@ -69,7 +69,7 @@ class Faulty
69
69
 
70
70
  # Wrap an array of storage backends in a fault-tolerant FallbackChain
71
71
  #
72
- # @param [Array<Storage::Interface>] The array to wrap
72
+ # @param array [Array<Storage::Interface>] The array to wrap
73
73
  # @param options [Options]
74
74
  # @return [Storage::Interface] A fault-tolerant fallback chain
75
75
  def wrap_array(array, options)
@@ -81,7 +81,7 @@ class Faulty
81
81
 
82
82
  # Wrap one storage backend in fault-tolerant backends
83
83
  #
84
- # @param [Storage::Interface] The storage to wrap
84
+ # @param storage [Storage::Interface] The storage to wrap
85
85
  # @param options [Options]
86
86
  # @return [Storage::Interface] A fault-tolerant storage backend
87
87
  def wrap_one(storage, options)
@@ -93,7 +93,7 @@ class Faulty
93
93
 
94
94
  # Wrap storage in a CircuitProxy
95
95
  #
96
- # @param [Storage::Interface] The storage to wrap
96
+ # @param storage [Storage::Interface] The storage to wrap
97
97
  # @param options [Options]
98
98
  # @return [CircuitProxy]
99
99
  def circuit_proxy(storage, options)
@@ -69,8 +69,8 @@ class Faulty
69
69
  #
70
70
  # @param (see Interface#entry)
71
71
  # @return (see Interface#entry)
72
- def entry(circuit, time, success)
73
- send_chain(:entry, circuit, time, success) do |e|
72
+ def entry(circuit, time, success, status)
73
+ send_chain(:entry, circuit, time, success, status) do |e|
74
74
  options.notifier.notify(:storage_failure, circuit: circuit, action: :entry, error: e)
75
75
  end
76
76
  end
@@ -112,11 +112,11 @@ class Faulty
112
112
  # @see Interface#entry
113
113
  # @param (see Interface#entry)
114
114
  # @return (see Interface#entry)
115
- def entry(circuit, time, success)
116
- @storage.entry(circuit, time, success)
115
+ def entry(circuit, time, success, status)
116
+ @storage.entry(circuit, time, success, status)
117
117
  rescue StandardError => e
118
118
  options.notifier.notify(:storage_failure, circuit: circuit, action: :entry, error: e)
119
- []
119
+ stub_status(circuit) if status
120
120
  end
121
121
 
122
122
  # Safely mark a circuit as open
@@ -21,7 +21,7 @@ class Faulty
21
21
  # They should be returned exactly as given by {#set_options}
22
22
  #
23
23
  # @param circuit [Circuit] The circuit to set options for
24
- # @param options [Hash<Symbol, Object>] A hash of symbol option names to
24
+ # @param stored_options [Hash<Symbol, Object>] A hash of symbol option names to
25
25
  # circuit options. These option values are guranteed to be primive
26
26
  # values.
27
27
  # @return [void]
@@ -37,9 +37,10 @@ class Faulty
37
37
  # @param circuit [Circuit] The circuit that ran
38
38
  # @param time [Integer] The unix timestamp for the run
39
39
  # @param success [Boolean] True if the run succeeded
40
- # @return [Array<Array>] An array of the new history tuples after adding
41
- # the new entry, see {#history}
42
- def entry(circuit, time, success)
40
+ # @param status [Status, nil] The previous status. If given, this method must
41
+ # return an updated status object from the new entry data.
42
+ # @return [Status, nil] If `status` is not nil, the updated status object.
43
+ def entry(circuit, time, success, status)
43
44
  raise NotImplementedError
44
45
  end
45
46
 
@@ -99,13 +99,14 @@ class Faulty
99
99
  # @see Interface#entry
100
100
  # @param (see Interface#entry)
101
101
  # @return (see Interface#entry)
102
- def entry(circuit, time, success)
102
+ def entry(circuit, time, success, status)
103
103
  memory = fetch(circuit)
104
104
  memory.runs.borrow do |runs|
105
105
  runs.push([time, success])
106
106
  runs.shift if runs.size > options.max_sample_size
107
107
  end
108
- memory.runs.value
108
+
109
+ Status.from_entries(memory.runs.value, **status.to_h) if status
109
110
  end
110
111
 
111
112
  # Mark a circuit as open
@@ -24,8 +24,8 @@ class Faulty
24
24
 
25
25
  # @param (see Interface#entry)
26
26
  # @return (see Interface#entry)
27
- def entry(_circuit, _time, _success)
28
- []
27
+ def entry(circuit, _time, _success, status)
28
+ stub_status(circuit) if status
29
29
  end
30
30
 
31
31
  # @param (see Interface#open)
@@ -64,10 +64,7 @@ class Faulty
64
64
  # @param (see Interface#status)
65
65
  # @return (see Interface#status)
66
66
  def status(circuit)
67
- Faulty::Status.new(
68
- options: circuit.options,
69
- stub: true
70
- )
67
+ stub_status(circuit)
71
68
  end
72
69
 
73
70
  # @param (see Interface#history)
@@ -89,6 +86,15 @@ class Faulty
89
86
  def fault_tolerant?
90
87
  true
91
88
  end
89
+
90
+ private
91
+
92
+ def stub_status(circuit)
93
+ Faulty::Status.new(
94
+ options: circuit.options,
95
+ stub: true
96
+ )
97
+ end
92
98
  end
93
99
  end
94
100
  end
@@ -119,7 +119,7 @@ class Faulty
119
119
  # @see Interface#entry
120
120
  # @param (see Interface#entry)
121
121
  # @return (see Interface#entry)
122
- def entry(circuit, time, success)
122
+ def entry(circuit, time, success, status)
123
123
  key = entries_key(circuit)
124
124
  result = pipe do |r|
125
125
  r.sadd(list_key, circuit.name)
@@ -127,9 +127,10 @@ class Faulty
127
127
  r.lpush(key, "#{time}#{ENTRY_SEPARATOR}#{success ? 1 : 0}")
128
128
  r.ltrim(key, 0, options.max_sample_size - 1)
129
129
  r.expire(key, options.sample_ttl) if options.sample_ttl
130
- r.lrange(key, 0, -1)
130
+ r.lrange(key, 0, -1) if status
131
131
  end
132
- map_entries(result.last)
132
+
133
+ Status.from_entries(map_entries(result.last), **status.to_h) if status
133
134
  end
134
135
 
135
136
  # Mark a circuit as open
@@ -138,11 +139,14 @@ class Faulty
138
139
  # @param (see Interface#open)
139
140
  # @return (see Interface#open)
140
141
  def open(circuit, opened_at)
141
- redis do |r|
142
- opened = compare_and_set(r, state_key(circuit), ['closed', nil], 'open', ex: options.circuit_ttl)
143
- r.set(opened_at_key(circuit), opened_at, ex: options.circuit_ttl) if opened
144
- opened
142
+ key = state_key(circuit)
143
+ ex = options.circuit_ttl
144
+ result = watch_exec(key, ['closed', nil]) do |m|
145
+ m.set(key, 'open', ex: ex)
146
+ m.set(opened_at_key(circuit), opened_at, ex: ex)
145
147
  end
148
+
149
+ result && result[0] == 'OK'
146
150
  end
147
151
 
148
152
  # Mark a circuit as reopened
@@ -151,9 +155,12 @@ class Faulty
151
155
  # @param (see Interface#reopen)
152
156
  # @return (see Interface#reopen)
153
157
  def reopen(circuit, opened_at, previous_opened_at)
154
- redis do |r|
155
- compare_and_set(r, opened_at_key(circuit), [previous_opened_at.to_s], opened_at, ex: options.circuit_ttl)
158
+ key = opened_at_key(circuit)
159
+ result = watch_exec(key, [previous_opened_at.to_s]) do |m|
160
+ m.set(key, opened_at, ex: options.circuit_ttl)
156
161
  end
162
+
163
+ result && result[0] == 'OK'
157
164
  end
158
165
 
159
166
  # Mark a circuit as closed
@@ -162,11 +169,14 @@ class Faulty
162
169
  # @param (see Interface#close)
163
170
  # @return (see Interface#close)
164
171
  def close(circuit)
165
- redis do |r|
166
- closed = compare_and_set(r, state_key(circuit), ['open'], 'closed', ex: options.circuit_ttl)
167
- r.del(entries_key(circuit)) if closed
168
- closed
172
+ key = state_key(circuit)
173
+ ex = options.circuit_ttl
174
+ result = watch_exec(key, ['open']) do |m|
175
+ m.set(key, 'closed', ex: ex)
176
+ m.del(entries_key(circuit))
169
177
  end
178
+
179
+ result && result[0] == 'OK'
170
180
  end
171
181
 
172
182
  # Lock a circuit open or closed
@@ -220,11 +230,15 @@ class Faulty
220
230
  futures[:entries] = r.lrange(entries_key(circuit), 0, -1)
221
231
  end
222
232
 
233
+ state = futures[:state].value&.to_sym || :closed
234
+ opened_at = futures[:opened_at].value ? futures[:opened_at].value.to_i : nil
235
+ opened_at = Faulty.current_time - options.circuit_ttl if state == :open && opened_at.nil?
236
+
223
237
  Faulty::Status.from_entries(
224
238
  map_entries(futures[:entries].value),
225
- state: futures[:state].value&.to_sym || :closed,
239
+ state: state,
226
240
  lock: futures[:lock].value&.to_sym,
227
- opened_at: futures[:opened_at].value ? futures[:opened_at].value.to_i : nil,
241
+ opened_at: opened_at,
228
242
  options: circuit.options
229
243
  )
230
244
  end
@@ -329,23 +343,28 @@ class Faulty
329
343
  (Faulty.current_time.to_f / options.list_granularity).floor
330
344
  end
331
345
 
332
- # Set a value in Redis only if it matches a list of current values
346
+ # Watch a Redis key and exec commands only if the key matches the expected
347
+ # value. Internally this uses Redis transactions with WATCH/MULTI/EXEC.
333
348
  #
334
- # @param redis [Redis] The redis connection
335
- # @param key [String] The redis key to CAS
336
- # @param old [Array<String>] A list of previous values that pass the
337
- # comparison
338
- # @param new [String] The new value to set if the compare passes
339
- # @return [Boolean] True if the value was set to `new`, false if the CAS
340
- # failed
341
- def compare_and_set(redis, key, old, new, ex:)
342
- redis.watch(key) do
343
- if old.include?(redis.get(key))
344
- result = redis.multi { |m| m.set(key, new, ex: ex) }
345
- result && result[0] == 'OK'
346
- else
347
- redis.unwatch
348
- false
349
+ # @param key [String] The redis key to watch
350
+ # @param old [Array<String>] A list of previous values. The block will be
351
+ # run only if key is one of these values.
352
+ # @yield [Redis] A redis client. Commands executed using this client
353
+ # will be executed inside the MULTI context and will only be run if
354
+ # the watch succeeds and the comparison passes
355
+ # @return [Array] An array of Redis results from the commands executed
356
+ # inside the block
357
+ def watch_exec(key, old)
358
+ redis do |r|
359
+ r.watch(key) do
360
+ if old.include?(r.get(key))
361
+ r.multi do |m|
362
+ yield m
363
+ end
364
+ else
365
+ r.unwatch
366
+ nil
367
+ end
349
368
  end
350
369
  end
351
370
  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.8.4')
6
+ Gem::Version.new('0.8.5')
7
7
  end
8
8
  end
data/lib/faulty.rb CHANGED
@@ -4,6 +4,7 @@ require 'securerandom'
4
4
  require 'forwardable'
5
5
  require 'concurrent'
6
6
 
7
+ require 'faulty/deprecation'
7
8
  require 'faulty/immutable_options'
8
9
  require 'faulty/cache'
9
10
  require 'faulty/circuit'
@@ -143,14 +144,14 @@ class Faulty
143
144
  @disabled = true
144
145
  end
145
146
 
146
- # Re-enable Faulty if disabled with {#disable!}
147
+ # Re-enable Faulty if disabled with {.disable!}
147
148
  #
148
149
  # @return [void]
149
150
  def enable!
150
151
  @disabled = false
151
152
  end
152
153
 
153
- # Check whether Faulty was disabled with {#disable!}
154
+ # Check whether Faulty was disabled with {.disable!}
154
155
  #
155
156
  # @return [Boolean] True if disabled
156
157
  def disabled?
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.8.4
4
+ version: 0.8.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Howard
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-28 00:00:00.000000000 Z
11
+ date: 2022-02-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -159,6 +159,7 @@ files:
159
159
  - lib/faulty/cache/rails.rb
160
160
  - lib/faulty/circuit.rb
161
161
  - lib/faulty/circuit_registry.rb
162
+ - lib/faulty/deprecation.rb
162
163
  - lib/faulty/error.rb
163
164
  - lib/faulty/events.rb
164
165
  - lib/faulty/events/callback_listener.rb