faulty 0.2.0 → 0.3.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.
@@ -10,6 +10,8 @@ class Faulty
10
10
  # If the storage backend raises a `StandardError`, it will be captured and
11
11
  # sent to the notifier.
12
12
  class FaultTolerantProxy
13
+ extend Forwardable
14
+
13
15
  attr_reader :options
14
16
 
15
17
  # Options for {FaultTolerantProxy}
@@ -36,6 +38,53 @@ class Faulty
36
38
  @options = Options.new(options, &block)
37
39
  end
38
40
 
41
+ # Wrap a storage backend in a FaultTolerantProxy unless it's already
42
+ # fault tolerant
43
+ #
44
+ # @param storage [Storage::Interface] The storage to maybe wrap
45
+ # @return [Storage::Interface] The original storage or a {FaultTolerantProxy}
46
+ def self.wrap(storage, **options, &block)
47
+ return storage if storage.fault_tolerant?
48
+
49
+ new(storage, **options, &block)
50
+ end
51
+
52
+ # @!method lock(circuit, state)
53
+ # Lock is not called in normal operation, so it doesn't capture errors
54
+ #
55
+ # @see Interface#lock
56
+ # @param (see Interface#lock)
57
+ # @return (see Interface#lock)
58
+ #
59
+ # @!method unlock(circuit)
60
+ # Unlock is not called in normal operation, so it doesn't capture errors
61
+ #
62
+ # @see Interface#unlock
63
+ # @param (see Interface#unlock)
64
+ # @return (see Interface#unlock)
65
+ #
66
+ # @!method reset(circuit)
67
+ # Reset is not called in normal operation, so it doesn't capture errors
68
+ #
69
+ # @see Interface#reset
70
+ # @param (see Interface#reset)
71
+ # @return (see Interface#reset)
72
+ #
73
+ # @!method history(circuit)
74
+ # History is not called in normal operation, so it doesn't capture errors
75
+ #
76
+ # @see Interface#history
77
+ # @param (see Interface#history)
78
+ # @return (see Interface#history)
79
+ #
80
+ # @!method list
81
+ # List is not called in normal operation, so it doesn't capture errors
82
+ #
83
+ # @see Interface#list
84
+ # @param (see Interface#list)
85
+ # @return (see Interface#list)
86
+ def_delegators :@storage, :lock, :unlock, :reset, :history, :list
87
+
39
88
  # Add a history entry safely
40
89
  #
41
90
  # @see Interface#entry
@@ -84,36 +133,6 @@ class Faulty
84
133
  false
85
134
  end
86
135
 
87
- # Since lock is not called in normal operation, it does not capture
88
- # errors
89
- #
90
- # @see Interface#lock
91
- # @param (see Interface#lock)
92
- # @return (see Interface#lock)
93
- def lock(circuit, state)
94
- @storage.lock(circuit, state)
95
- end
96
-
97
- # Since unlock is not called in normal operation, it does not capture
98
- # errors
99
- #
100
- # @see Interface#unlock
101
- # @param (see Interface#unlock)
102
- # @return (see Interface#unlock)
103
- def unlock(circuit)
104
- @storage.unlock(circuit)
105
- end
106
-
107
- # Since reset is not called in normal operation, it does not capture
108
- # errors
109
- #
110
- # @see Interface#reset
111
- # @param (see Interface#reset)
112
- # @return (see Interface#reset)
113
- def reset(circuit)
114
- @storage.reset(circuit)
115
- end
116
-
117
136
  # Safely get the status of a circuit
118
137
  #
119
138
  # If the backend is unavailable, this returns a stub status that
@@ -129,30 +148,6 @@ class Faulty
129
148
  stub_status(circuit)
130
149
  end
131
150
 
132
- # Since history is not called in normal operation, it does not capture
133
- # errors
134
- #
135
- # @see Interface#history
136
- # @param (see Interface#history)
137
- # @return (see Interface#history)
138
- def history(circuit)
139
- @storage.history(circuit)
140
- end
141
-
142
- # Safely get the list of circuit names
143
- #
144
- # If the backend is unavailable, this returns an empty array
145
- #
146
- # @see Interface#list
147
- # @param (see Interface#list)
148
- # @return (see Interface#list)
149
- def list
150
- @storage.list
151
- rescue StandardError => e
152
- options.notifier.notify(:storage_failure, action: :list, error: e)
153
- []
154
- end
155
-
156
151
  # This cache makes any storage fault tolerant, so this is always `true`
157
152
  #
158
153
  # @return [true]
@@ -4,8 +4,9 @@ class Faulty
4
4
  module Storage
5
5
  # The default in-memory storage for circuits
6
6
  #
7
- # This implementation is most suitable to single-process, low volume
8
- # usage. It is thread-safe and circuit state is shared across threads.
7
+ # This implementation is thread-safe and circuit state is shared across
8
+ # threads. Since state is stored in-memory, this state is not shared across
9
+ # processes, or persisted across application restarts.
9
10
  #
10
11
  # Circuit state and runs are stored in memory. Although runs have a maximum
11
12
  # size within a circuit, there is no limit on the number of circuits that
@@ -18,6 +19,9 @@ class Faulty
18
19
  #
19
20
  # This can be used as a reference implementation for storage backends that
20
21
  # store a list of circuit run entries.
22
+ #
23
+ # @todo Add a more sophsticated implmentation that can limit the number of
24
+ # circuits stored.
21
25
  class Memory
22
26
  attr_reader :options
23
27
 
@@ -2,6 +2,13 @@
2
2
 
3
3
  class Faulty
4
4
  module Storage
5
+ # A storage backend for storing circuit state in Redis.
6
+ #
7
+ # When using this or any networked backend, be sure to evaluate the risk,
8
+ # and set conservative timeouts so that the circuit storage does not cause
9
+ # cascading failures in your application when evaluating circuits. Always
10
+ # wrap this backend with a {FaultTolerantProxy} to limit the effect of
11
+ # these types of events.
5
12
  class Redis # rubocop:disable Metrics/ClassLength
6
13
  # Separates the time/status for history entry strings
7
14
  ENTRY_SEPARATOR = ':'
@@ -27,12 +34,17 @@ class Faulty
27
34
  # circuit run history entry. Default `100`.
28
35
  # @!attribute [r] circuit_ttl
29
36
  # @return [Integer] The maximum number of seconds to keep a circuit.
30
- # A value of `nil` disables circuit expiration.
37
+ # A value of `nil` disables circuit expiration. This does not apply to
38
+ # locks, which have an indefinite storage time.
31
39
  # Default `604_800` (1 week).
32
40
  # @!attribute [r] list_granularity
33
41
  # @return [Integer] The number of seconds after which a new set is
34
42
  # created to store circuit names. The old set is kept until
35
43
  # circuit_ttl expires. Default `3600` (1 hour).
44
+ # @!attribute [r] disable_warnings
45
+ # @return [Boolean] By default, this class warns if the client options
46
+ # are outside the recommended values. Set to true to disable these
47
+ # warnings.
36
48
  Options = Struct.new(
37
49
  :client,
38
50
  :key_prefix,
@@ -40,7 +52,8 @@ class Faulty
40
52
  :max_sample_size,
41
53
  :sample_ttl,
42
54
  :circuit_ttl,
43
- :list_granularity
55
+ :list_granularity,
56
+ :disable_warnings
44
57
  ) do
45
58
  include ImmutableOptions
46
59
 
@@ -53,7 +66,8 @@ class Faulty
53
66
  max_sample_size: 100,
54
67
  sample_ttl: 1800,
55
68
  circuit_ttl: 604_800,
56
- list_granularity: 3600
69
+ list_granularity: 3600,
70
+ disable_warnings: false
57
71
  }
58
72
  end
59
73
 
@@ -62,7 +76,7 @@ class Faulty
62
76
  end
63
77
 
64
78
  def finalize
65
- self.client = ::Redis.new unless client
79
+ self.client = ::Redis.new(timeout: 1) unless client
66
80
  end
67
81
  end
68
82
 
@@ -70,6 +84,8 @@ class Faulty
70
84
  # @yield [Options] For setting options in a block
71
85
  def initialize(**options, &block)
72
86
  @options = Options.new(options, &block)
87
+
88
+ check_client_options!
73
89
  end
74
90
 
75
91
  # Add an entry to storage
@@ -196,6 +212,9 @@ class Faulty
196
212
  map_entries(entries).reverse
197
213
  end
198
214
 
215
+ # List all unexpired circuits
216
+ #
217
+ # @return (see Interface#list)
199
218
  def list
200
219
  redis { |r| r.sunion(*all_list_keys) }
201
220
  end
@@ -330,6 +349,49 @@ class Faulty
330
349
  [time.to_i, state == '1']
331
350
  end
332
351
  end
352
+
353
+ def check_client_options!
354
+ return if options.disable_warnings
355
+
356
+ check_redis_options!
357
+ check_pool_options!
358
+ rescue StandardError => e
359
+ warn "Faulty error while checking client options: #{e.message}"
360
+ end
361
+
362
+ def check_redis_options!
363
+ ropts = redis { |r| r.client.options }
364
+
365
+ bad_timeouts = {}
366
+ %i[connect_timeout read_timeout write_timeout].each do |time_opt|
367
+ bad_timeouts[time_opt] = ropts[time_opt] if ropts[time_opt] > 2
368
+ end
369
+
370
+ unless bad_timeouts.empty?
371
+ warn <<~MSG
372
+ Faulty recommends setting Redis timeouts <= 2 to prevent cascading
373
+ failures when evaluating circuits. Your options are:
374
+ #{bad_timeouts}
375
+ MSG
376
+ end
377
+
378
+ if ropts[:reconnect_attempts] > 1
379
+ warn <<~MSG
380
+ Faulty recommends setting Redis reconnect_attempts to <= 1 to
381
+ prevent cascading failures. Your setting is #{ropts[:reconnect_attempts]}
382
+ MSG
383
+ end
384
+ end
385
+
386
+ def check_pool_options!
387
+ if options.client.is_a?(ConnectionPool)
388
+ timeout = options.client.instance_variable_get(:@timeout)
389
+ warn(<<~MSG) if timeout > 2
390
+ Faulty recommends setting ConnectionPool timeouts <= 2 to prevent
391
+ cascading failures when evaluating circuits. Your setting is #{timeout}
392
+ MSG
393
+ end
394
+ end
333
395
  end
334
396
  end
335
397
  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.2.0')
6
+ Gem::Version.new('0.3.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.2.0
4
+ version: 0.3.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-18 00:00:00.000000000 Z
11
+ date: 2020-10-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -148,6 +148,8 @@ files:
148
148
  - faulty.gemspec
149
149
  - lib/faulty.rb
150
150
  - lib/faulty/cache.rb
151
+ - lib/faulty/cache/auto_wire.rb
152
+ - lib/faulty/cache/circuit_proxy.rb
151
153
  - lib/faulty/cache/default.rb
152
154
  - lib/faulty/cache/fault_tolerant_proxy.rb
153
155
  - lib/faulty/cache/interface.rb
@@ -166,6 +168,9 @@ files:
166
168
  - lib/faulty/result.rb
167
169
  - lib/faulty/status.rb
168
170
  - lib/faulty/storage.rb
171
+ - lib/faulty/storage/auto_wire.rb
172
+ - lib/faulty/storage/circuit_proxy.rb
173
+ - lib/faulty/storage/fallback_chain.rb
169
174
  - lib/faulty/storage/fault_tolerant_proxy.rb
170
175
  - lib/faulty/storage/interface.rb
171
176
  - lib/faulty/storage/memory.rb