faulty 0.8.4 → 0.8.5

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