faulty 0.1.4 → 0.2.0

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: 9af8f5d6a81fdf1b625c2d588f2d9f6f55380ebaf00c398d5ed29454bfafea48
4
- data.tar.gz: 2d15beb6ffec3967bb740a3567245fe40e7b381408b7203f7cb2cce020802f89
3
+ metadata.gz: 55bf688c3615a59f51103633db0902abf3458324945ab855894e5975981868f5
4
+ data.tar.gz: dab412aad317a79364782a85c2a6a184e55fa2214cd966655d74eeab69e248c0
5
5
  SHA512:
6
- metadata.gz: 4d1a6b24d30ceac93393aef209ac8acb9b24648698aa1f046d32f9e181e3e7208829c62b235139e34cbbf6d4fa3528a9aef24ed48ff359d3b2bc29cd27205165
7
- data.tar.gz: a95a10dfdc4ea878a35c1955a6c586ecf254f0fc6c02e02b5aab5cb2da09e5bd3ea4181a8fd652877a213cb0c519eee39735e6da3092cfc270a20832979f3ebc
6
+ metadata.gz: 7527a23df0c042ea317e99b8fafdf8c8f96ac5f9f68323ba4c4a2901f8e32dc7ca2b7c62b6b7197e446985eab6901f85dd41c6463407ae3632d86e022626d733
7
+ data.tar.gz: 7e3d48b82905b0ad2303449a532e85736a0681fa4dcb8a5f9ee64e6ffd10c76fdc760ac97aa9dd882f7a019d335440f9d91f0ba1efbb92f978c5e9e43c5b977a
@@ -62,6 +62,9 @@ Metrics/BlockLength:
62
62
  Metrics/MethodLength:
63
63
  Max: 30
64
64
 
65
+ Naming/MethodParameterName:
66
+ MinNameLength: 1
67
+
65
68
  Style/Documentation:
66
69
  Enabled: false
67
70
 
@@ -1,3 +1,16 @@
1
+ ## Release v0.2.0
2
+
3
+ * Remove Scopes and replace them with Faulty instances #9
4
+
5
+ ### Breaking Changes
6
+
7
+ * `Faulty::Scope` has been removed. Use `Faulty.new` instead.
8
+ * `Faulty` is now a class, not a module
9
+
10
+ ## Release v0.1.5
11
+
12
+ * Fix redis storage to expire state key when using CAS #8
13
+
1
14
  ## Release v0.1.4
2
15
 
3
16
  * Improve spec coverage for supporting classes #6
data/README.md CHANGED
@@ -203,11 +203,12 @@ In addition to the classic circuit breaker design, Faulty implements caching
203
203
  that is integrated with the circuit state. See [Caching](#caching) for more
204
204
  detail.
205
205
 
206
- ## Global Configuration
206
+ ## Configuration
207
207
 
208
- `Faulty.init` can set the following global configuration options. This example
209
- illustrates the default values. It is also possible to define multiple
210
- non-global configuration scopes (see [Scopes](#scopes)).
208
+ Faulty can be configured with the following configuration options. This example
209
+ illustrates the default values. In the first example, we configure Faulty
210
+ globally. The second example shows the same configuration using an instance of
211
+ Faulty instead of global configuration.
211
212
 
212
213
  ```ruby
213
214
  Faulty.init do |config|
@@ -233,6 +234,25 @@ Faulty.init do |config|
233
234
  end
234
235
  ```
235
236
 
237
+ Here is the same configuration using an instance of `Faulty`. This is a more
238
+ object-oriented approach.
239
+
240
+ ```ruby
241
+ faulty = Faulty.new do |config|
242
+ config.cache = Faulty::Cache::Default.new
243
+ config.storage = Faulty::Storage::Memory.new
244
+ config.listeners = [Faulty::Events::LogListener.new]
245
+ config.notifier = Faulty::Events::Notifier.new(config.listeners)
246
+ end
247
+ ```
248
+
249
+ Most of the examples in this README use the global Faulty class methods, but
250
+ they work the same way when using an instance. Just substitute your instance
251
+ instead of `Faulty`. There is no preferred way to use Faulty. Choose whichever
252
+ configuration mechanism works best for your application. Also see
253
+ [Multiple Configurations](#multiple-configurations) if your application needs
254
+ to set different options in different scenarios.
255
+
236
256
  For all Faulty APIs that have configuration, you can also pass in an options
237
257
  hash. For example, `Faulty.init` could be called like this:
238
258
 
@@ -244,9 +264,9 @@ Faulty.init(cache: Faulty::Cache::Null.new)
244
264
 
245
265
  A circuit can be created with the following configuration options. Those options
246
266
  are only set once, synchronized across threads, and will persist in-memory until
247
- the process exits. If you're using [scopes](#scopes), the options are retained
248
- within the context of each scope. All options given after the first call to
249
- `Faulty.circuit` (or `Scope.circuit`) are ignored.
267
+ the process exits. If you're using [multiple configurations](#multiple-configurations),
268
+ the options are retained within the context of each instance. All options given
269
+ after the first call to `Faulty.circuit` (or `Faulty#circuit`) are ignored.
250
270
 
251
271
  This is because the circuit objects themselves are internally memoized, and are
252
272
  read-only once created.
@@ -307,8 +327,8 @@ Faulty.circuit(:api, cache_expires_in: 1800)
307
327
 
308
328
  Faulty integrates caching into it's circuits in a way that is particularly
309
329
  suited to fault-tolerance. To make use of caching, you must specify the `cache`
310
- configuration option when initializing Faulty or creating a scope. If you're
311
- using Rails, this is automatically set to the Rails cache.
330
+ configuration option when initializing Faulty or creating a new Faulty instance.
331
+ If you're using Rails, this is automatically set to the Rails cache.
312
332
 
313
333
  Once your cache is configured, you can use the `cache` parameter when running
314
334
  a circuit to specify a cache key:
@@ -480,8 +500,8 @@ end
480
500
  ## Listing Circuits
481
501
 
482
502
  For monitoring or debugging, you may need to retrieve a list of all circuit
483
- names. This is possible with `Faulty.list_circuits` (or the equivalent method on
484
- your [scope](#scopes)).
503
+ names. This is possible with `Faulty.list_circuits` (or `Faulty#list_circuits`
504
+ if you're using an instance).
485
505
 
486
506
  You can get a list of all circuit statuses by mapping those names to their
487
507
  status objects. Be careful though, since this could cause performance issues for
@@ -523,73 +543,73 @@ Locking or unlocking a circuit has no concurrency guarantees, so it's not
523
543
  recommended to lock or unlock circuits from production code. Instead, locks are
524
544
  intended as an emergency tool for troubleshooting and debugging.
525
545
 
526
- ## Scopes
546
+ ## Multiple Configurations
527
547
 
528
548
  It is possible to have multiple configurations of Faulty running within the same
529
- process. The most common configuration is to simply use `Faulty.init` to
549
+ process. The most common setup is to simply use `Faulty.init` to
530
550
  configure Faulty globally, however it is possible to have additional
531
- configurations using scopes.
551
+ configurations.
532
552
 
533
- ### The default scope
553
+ ### The default instance
534
554
 
535
- When you call `Faulty.init`, you are actually creating the default scope. You
536
- can access this scope directly by calling `Faulty.default`.
555
+ When you call `Faulty.init`, you are actually creating the default instance of
556
+ `Faulty`. You can access this instance directly by calling `Faulty.default`.
537
557
 
538
558
  ```ruby
539
- # We create the default scope
559
+ # We create the default instance
540
560
  Faulty.init
541
561
 
542
- # Access the default scope
543
- scope = Faulty.default
562
+ # Access the default instance
563
+ faulty = Faulty.default
544
564
 
545
- # Alternatively, access the scope by name
546
- scope = Faulty[:default]
565
+ # Alternatively, access the instance by name
566
+ faulty = Faulty[:default]
547
567
  ```
548
568
 
549
- You can rename the default scope if desired:
569
+ You can rename the default instance if desired:
550
570
 
551
571
  ```ruby
552
572
  Faulty.init(:custom_default)
553
573
 
554
- scope = Faulty.default
555
- scope = Faulty[:custom_default]
574
+ instance = Faulty.default
575
+ instance = Faulty[:custom_default]
556
576
  ```
557
577
 
558
- ### Multiple Scopes
578
+ ### Multiple Instances
559
579
 
560
- If you want multiple scopes, but want global, thread-safe access to
580
+ If you want multiple instance, but want global, thread-safe access to
561
581
  them, you can use `Faulty.register`:
562
582
 
563
583
  ```ruby
564
- api_scope = Faulty::Scope.new do |config|
584
+ api_faulty = Faulty.new do |config|
565
585
  # This accepts the same options as Faulty.init
566
586
  end
567
587
 
568
- Faulty.register(:api, api_scope)
588
+ Faulty.register(:api, api_faulty)
569
589
 
570
- # Now access the scope globally
590
+ # Now access the instance globally
571
591
  Faulty[:api]
572
592
  ```
573
593
 
574
594
  When you call `Faulty.circuit`, that's the same as calling
575
- `Faulty.default.circuit`, so you can apply the same API to any other Faulty
576
- scope:
595
+ `Faulty.default.circuit`, so you can apply the same principal to any other
596
+ registered Faulty instance:
577
597
 
578
598
  ```ruby
579
599
  Faulty[:api].circuit(:api_circuit).run { 'ok' }
580
600
  ```
581
601
 
582
- ### Standalone Scopes
602
+ ### Standalone Instances
583
603
 
584
- If you choose, you can use Faulty scopes without registering them globally. This
585
- could be useful if you prefer dependency injection over global state.
604
+ If you choose, you can use Faulty instances without registering them globally.
605
+ This is more object-oriented and is necessary if you use dependency injection.
586
606
 
587
607
  ```ruby
588
- faulty = Faulty::Scope.new
608
+ faulty = Faulty.new
589
609
  faulty.circuit(:standalone_circuit)
590
610
  ```
591
611
 
592
- Calling `circuit` on the scope still has the same memoization behavior that
612
+ Calling `#circuit` on the instance still has the same memoization behavior that
593
613
  `Faulty.circuit` has, so subsequent calls to the same circuit will return a
594
614
  memoized circuit object.
595
615
 
@@ -660,7 +680,7 @@ but there are and have been many other options:
660
680
 
661
681
  - Simple API but configurable for advanced users
662
682
  - Pluggable storage backends (circuitbox also has this)
663
- - Global, or local configuration with scopes
683
+ - Global, or object-oriented configuration with multiple instances
664
684
  - Integrated caching support tailored for fault-tolerance
665
685
  - Manually lock circuits open or closed
666
686
 
@@ -9,14 +9,16 @@ require 'faulty/circuit'
9
9
  require 'faulty/error'
10
10
  require 'faulty/events'
11
11
  require 'faulty/result'
12
- require 'faulty/scope'
13
12
  require 'faulty/status'
14
13
  require 'faulty/storage'
15
14
 
16
- # The top-level namespace for Faulty
15
+ # The {Faulty} class has class-level methods for global state or can be
16
+ # instantiated to create an independent configuration.
17
17
  #
18
- # Fault-tolerance tools for ruby based on circuit-breakers
19
- module Faulty
18
+ # If you are using global state, call {Faulty#init} during your application's
19
+ # initialization. This is the simplest way to use {Faulty}. If you prefer, you
20
+ # can also call {Faulty.new} to create independent {Faulty} instances.
21
+ class Faulty
20
22
  class << self
21
23
  # Start the Faulty environment
22
24
  #
@@ -27,78 +29,79 @@ module Faulty
27
29
  # are spawned.
28
30
  #
29
31
  # If you prefer dependency-injection instead of global state, you can skip
30
- # init and pass a {Scope} directly to your dependencies.
32
+ # `init` and use {Faulty.new} to pass an instance directoy to your
33
+ # dependencies.
31
34
  #
32
- # @param scope_name [Symbol] The name of the default scope. Can be set to
33
- # `nil` to skip creating a default scope.
34
- # @param config [Hash] Attributes for {Scope::Options}
35
- # @yield [Scope::Options] For setting options in a block
35
+ # @param default_name [Symbol] The name of the default instance. Can be set
36
+ # to `nil` to skip creating a default instance.
37
+ # @param config [Hash] Attributes for {Faulty::Options}
38
+ # @yield [Faulty::Options] For setting options in a block
36
39
  # @return [self]
37
- def init(scope_name = :default, **config, &block)
38
- raise AlreadyInitializedError if @scopes
40
+ def init(default_name = :default, **config, &block)
41
+ raise AlreadyInitializedError if @instances
39
42
 
40
- @default_scope = scope_name
41
- @scopes = Concurrent::Map.new
42
- register(scope_name, Scope.new(**config, &block)) unless scope_name.nil?
43
+ @default_instance = default_name
44
+ @instances = Concurrent::Map.new
45
+ register(default_name, new(**config, &block)) unless default_name.nil?
43
46
  self
44
47
  rescue StandardError
45
- @scopes = nil
48
+ @instances = nil
46
49
  raise
47
50
  end
48
51
 
49
- # Get the default scope given during {.init}
52
+ # Get the default instance given during {.init}
50
53
  #
51
- # @return [Scope, nil] The default scope if it is registered
54
+ # @return [Faulty, nil] The default instance if it is registered
52
55
  def default
53
- raise UninitializedError unless @scopes
54
- raise MissingDefaultScopeError unless @default_scope
56
+ raise UninitializedError unless @instances
57
+ raise MissingDefaultInstanceError unless @default_instance
55
58
 
56
- self[@default_scope]
59
+ self[@default_instance]
57
60
  end
58
61
 
59
- # Get a scope by name
62
+ # Get an instance by name
60
63
  #
61
- # @return [Scope, nil] The named scope if it is registered
62
- def [](scope_name)
63
- raise UninitializedError unless @scopes
64
+ # @return [Faulty, nil] The named instance if it is registered
65
+ def [](name)
66
+ raise UninitializedError unless @instances
64
67
 
65
- @scopes[scope_name]
68
+ @instances[name]
66
69
  end
67
70
 
68
- # Register a scope to the global Faulty state
71
+ # Register an instance to the global Faulty state
69
72
  #
70
- # Will not replace an existing scope with the same name. Check the
71
- # return value if you need to know whether the scope already existed.
73
+ # Will not replace an existing instance with the same name. Check the
74
+ # return value if you need to know whether the instance already existed.
72
75
  #
73
- # @param name [Symbol] The name of the scope to register
74
- # @param scope [Scope] The scope to register
75
- # @return [Scope, nil] The previously-registered scope of that name if
76
+ # @param name [Symbol] The name of the instance to register
77
+ # @param instance [Faulty] The instance to register
78
+ # @return [Faulty, nil] The previously-registered instance of that name if
76
79
  # it already existed, otherwise nil.
77
- def register(name, scope)
78
- raise UninitializedError unless @scopes
80
+ def register(name, instance)
81
+ raise UninitializedError unless @instances
79
82
 
80
- @scopes.put_if_absent(name, scope)
83
+ @instances.put_if_absent(name, instance)
81
84
  end
82
85
 
83
- # Get the options for the default scope
86
+ # Get the options for the default instance
84
87
  #
85
- # @raise MissingDefaultScopeError If the default scope has not been created
86
- # @return [Scope::Options]
88
+ # @raise MissingDefaultInstanceError If the default instance has not been created
89
+ # @return [Faulty::Options]
87
90
  def options
88
91
  default.options
89
92
  end
90
93
 
91
- # Get or create a circuit for the default scope
94
+ # Get or create a circuit for the default instance
92
95
  #
93
- # @raise UninitializedError If the default scope has not been created
94
- # @param (see Scope#circuit)
95
- # @yield (see Scope#circuit)
96
- # @return (see Scope#circuit)
96
+ # @raise UninitializedError If the default instance has not been created
97
+ # @param (see Faulty#circuit)
98
+ # @yield (see Faulty#circuit)
99
+ # @return (see Faulty#circuit)
97
100
  def circuit(name, **config, &block)
98
101
  default.circuit(name, **config, &block)
99
102
  end
100
103
 
101
- # Get a list of all circuit names for the default scope
104
+ # Get a list of all circuit names for the default instance
102
105
  #
103
106
  # @return [Array<String>] The circuit names
104
107
  def list_circuits
@@ -115,4 +118,113 @@ module Faulty
115
118
  Time.now.to_i
116
119
  end
117
120
  end
121
+
122
+ attr_reader :options
123
+
124
+ # Options for {Faulty}
125
+ #
126
+ # @!attribute [r] cache
127
+ # @return [Cache::Interface] A cache backend if you want
128
+ # to use Faulty's cache support. Automatically wrapped in a
129
+ # {Cache::FaultTolerantProxy}. Default `Cache::Default.new`.
130
+ # @!attribute [r] storage
131
+ # @return [Storage::Interface] The storage backend.
132
+ # Automatically wrapped in a {Storage::FaultTolerantProxy}.
133
+ # Default `Storage::Memory.new`.
134
+ # @!attribute [r] listeners
135
+ # @return [Array] listeners Faulty event listeners
136
+ # @!attribute [r] notifier
137
+ # @return [Events::Notifier] A Faulty notifier. If given, listeners are
138
+ # ignored.
139
+ Options = Struct.new(
140
+ :cache,
141
+ :storage,
142
+ :listeners,
143
+ :notifier
144
+ ) do
145
+ include ImmutableOptions
146
+
147
+ private
148
+
149
+ def finalize
150
+ self.notifier ||= Events::Notifier.new(listeners || [])
151
+
152
+ self.storage ||= Storage::Memory.new
153
+ unless storage.fault_tolerant?
154
+ self.storage = Storage::FaultTolerantProxy.new(storage, notifier: notifier)
155
+ end
156
+
157
+ self.cache ||= Cache::Default.new
158
+ unless cache.fault_tolerant?
159
+ self.cache = Cache::FaultTolerantProxy.new(cache, notifier: notifier)
160
+ end
161
+ end
162
+
163
+ def required
164
+ %i[cache storage notifier]
165
+ end
166
+
167
+ def defaults
168
+ {
169
+ listeners: [Events::LogListener.new]
170
+ }
171
+ end
172
+ end
173
+
174
+ # Create a new {Faulty} instance
175
+ #
176
+ # Note, the process of creating a new instance is not thread safe,
177
+ # so make sure instances are setup during your application's initialization
178
+ # phase.
179
+ #
180
+ # For the most part, {Faulty} instances are independent, however for some
181
+ # cache and storage backends, you will need to ensure that the cache keys
182
+ # and circuit names don't overlap between instances. For example, if using the
183
+ # {Storage::Redis} storage backend, you should specify different key
184
+ # prefixes for each instance.
185
+ #
186
+ # @see Options
187
+ # @param options [Hash] Attributes for {Options}
188
+ # @yield [Options] For setting options in a block
189
+ def initialize(**options, &block)
190
+ @circuits = Concurrent::Map.new
191
+ @options = Options.new(options, &block)
192
+ end
193
+
194
+ # Create or retrieve a circuit
195
+ #
196
+ # Within an instance, circuit instances have unique names, so if the given circuit
197
+ # name already exists, then the existing circuit will be returned, otherwise
198
+ # a new circuit will be created. If an existing circuit is returned, then
199
+ # the {options} param and block are ignored.
200
+ #
201
+ # @param name [String] The name of the circuit
202
+ # @param options [Hash] Attributes for {Circuit::Options}
203
+ # @yield [Circuit::Options] For setting options in a block
204
+ # @return [Circuit] The new circuit or the existing circuit if it already exists
205
+ def circuit(name, **options, &block)
206
+ name = name.to_s
207
+ options = options.merge(circuit_options)
208
+ @circuits.compute_if_absent(name) do
209
+ Circuit.new(name, **options, &block)
210
+ end
211
+ end
212
+
213
+ # Get a list of all circuit names
214
+ #
215
+ # @return [Array<String>] The circuit names
216
+ def list_circuits
217
+ options.storage.list
218
+ end
219
+
220
+ private
221
+
222
+ # Get circuit options from the {Faulty} options
223
+ #
224
+ # @return [Hash] The circuit options
225
+ def circuit_options
226
+ options = @options.to_h
227
+ options.delete(:listeners)
228
+ options
229
+ end
118
230
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  # The namespace for Faulty caching
5
5
  module Cache
6
6
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Cache
5
5
  # The default cache implementation
6
6
  #
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Cache
5
5
  # A wrapper for cache backends that may raise errors
6
6
  #
7
- # {Scope} automatically wraps all non-fault-tolerant cache backends with
7
+ # {Faulty#initialize} automatically wraps all non-fault-tolerant cache backends with
8
8
  # this class.
9
9
  #
10
10
  # If the cache backend raises a `StandardError`, it will be captured and
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Cache
5
5
  # The interface required for a cache backend implementation
6
6
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Cache
5
5
  # A mock cache for testing
6
6
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Cache
5
5
  # A cache backend that does nothing
6
6
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Cache
5
5
  # A wrapper for a Rails or ActiveSupport cache
6
6
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  # Runs code protected by a circuit breaker
5
5
  #
6
6
  # https://www.martinfowler.com/bliki/CircuitBreaker.html
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  # The base error for all Faulty errors
5
5
  class FaultyError < StandardError; end
6
6
 
@@ -20,10 +20,10 @@ module Faulty
20
20
  end
21
21
  end
22
22
 
23
- # Raised if getting the default scope without initializing one
24
- class MissingDefaultScopeError < FaultyError
23
+ # Raised if getting the default instance without initializing one
24
+ class MissingDefaultInstanceError < FaultyError
25
25
  def initialize(message = nil)
26
- message ||= 'No default scope. Create one with init or get your scope with Faulty[:scope_name]'
26
+ message ||= 'No default instance. Create one with init or get your instance with Faulty[:name]'
27
27
  super(message)
28
28
  end
29
29
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  # The namespace for Faulty events and event listeners
5
5
  module Events
6
6
  # All possible events that can be raised by Faulty
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Events
5
5
  # A simple listener implementation that uses callback blocks as handlers
6
6
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Events
5
5
  # Reports circuit errors to Honeybadger
6
6
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Events
5
5
  # The interface required to implement a event listener
6
6
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Events
5
5
  # A default listener that logs Faulty events
6
6
  class LogListener
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Events
5
5
  # The default event dispatcher for Faulty
6
6
  class Notifier
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  # A struct that cannot be modified after initialization
5
5
  module ImmutableOptions
6
6
  # @param hash [Hash] A hash of attributes to initialize with
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  # An approximation of the `Result` type from some strongly-typed languages.
5
5
  #
6
6
  # F#: https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/results
@@ -53,7 +53,7 @@ module Faulty
53
53
  #
54
54
  # @param ok An ok value
55
55
  # @param error [Error] An error instance
56
- def initialize(ok: NOTHING, error: NOTHING) # rubocop:disable Naming/MethodParameterName
56
+ def initialize(ok: NOTHING, error: NOTHING)
57
57
  if ok.equal?(NOTHING) && error.equal?(NOTHING)
58
58
  raise ArgumentError, 'Result must have an ok or error value'
59
59
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  # The status of a circuit
5
5
  #
6
6
  # Includes information like the state and locks. Also calculates
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  # The namespace for Faulty storage
5
5
  module Storage
6
6
  end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Storage
5
5
  # A wrapper for storage backends that may raise errors
6
6
  #
7
- # {Scope} automatically wraps all non-fault-tolerant storage backends with
7
+ # {Faulty#initialize} automatically wraps all non-fault-tolerant storage backends with
8
8
  # this class.
9
9
  #
10
10
  # If the storage backend raises a `StandardError`, it will be captured and
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Storage
5
5
  # The interface required for a storage backend implementation
6
6
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Storage
5
5
  # The default in-memory storage for circuits
6
6
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  module Storage
5
5
  class Redis # rubocop:disable Metrics/ClassLength
6
6
  # Separates the time/status for history entry strings
@@ -97,7 +97,7 @@ module Faulty
97
97
  # @return (see Interface#open)
98
98
  def open(circuit, opened_at)
99
99
  redis do |r|
100
- opened = compare_and_set(r, state_key(circuit), ['closed', nil], 'open')
100
+ opened = compare_and_set(r, state_key(circuit), ['closed', nil], 'open', ex: options.circuit_ttl)
101
101
  r.set(opened_at_key(circuit), opened_at, ex: options.circuit_ttl) if opened
102
102
  opened
103
103
  end
@@ -110,7 +110,7 @@ module Faulty
110
110
  # @return (see Interface#reopen)
111
111
  def reopen(circuit, opened_at, previous_opened_at)
112
112
  redis do |r|
113
- compare_and_set(r, opened_at_key(circuit), [previous_opened_at.to_s], opened_at)
113
+ compare_and_set(r, opened_at_key(circuit), [previous_opened_at.to_s], opened_at, ex: options.circuit_ttl)
114
114
  end
115
115
  end
116
116
 
@@ -121,7 +121,7 @@ module Faulty
121
121
  # @return (see Interface#close)
122
122
  def close(circuit)
123
123
  redis do |r|
124
- closed = compare_and_set(r, state_key(circuit), ['open'], 'closed')
124
+ closed = compare_and_set(r, state_key(circuit), ['open'], 'closed', ex: options.circuit_ttl)
125
125
  r.del(entries_key(circuit)) if closed
126
126
  closed
127
127
  end
@@ -287,10 +287,10 @@ module Faulty
287
287
  # @param new [String] The new value to set if the compare passes
288
288
  # @return [Boolean] True if the value was set to `new`, false if the CAS
289
289
  # failed
290
- def compare_and_set(redis, key, old, new)
290
+ def compare_and_set(redis, key, old, new, ex:)
291
291
  redis.watch(key) do
292
292
  if old.include?(redis.get(key))
293
- result = redis.multi { |m| m.set(key, new) }
293
+ result = redis.multi { |m| m.set(key, new, ex: ex) }
294
294
  result && result[0] == 'OK'
295
295
  else
296
296
  redis.unwatch
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Faulty
3
+ class Faulty
4
4
  # The current Faulty version
5
5
  def self.version
6
- Gem::Version.new('0.1.4')
6
+ Gem::Version.new('0.2.0')
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: faulty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Howard
@@ -164,7 +164,6 @@ files:
164
164
  - lib/faulty/events/notifier.rb
165
165
  - lib/faulty/immutable_options.rb
166
166
  - lib/faulty/result.rb
167
- - lib/faulty/scope.rb
168
167
  - lib/faulty/status.rb
169
168
  - lib/faulty/storage.rb
170
169
  - lib/faulty/storage/fault_tolerant_proxy.rb
@@ -1,117 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Faulty
4
- # A {Scope} is a group of options and circuits
5
- #
6
- # For most use-cases the default scope should be used, however, it's possible
7
- # to create any number of scopes for applications that require a more complex
8
- # configuration or for testing.
9
- #
10
- # For the most part, scopes are independent, however for some cache and
11
- # storage backends, you will need to ensure that the cache keys and circuit
12
- # names don't overlap between scopes. For example, if using the Redis storage
13
- # backend, you should specify different key prefixes for each scope.
14
- class Scope
15
- attr_reader :options
16
-
17
- # Options for {Scope}
18
- #
19
- # @!attribute [r] cache
20
- # @return [Cache::Interface] A cache backend if you want
21
- # to use Faulty's cache support. Automatically wrapped in a
22
- # {Cache::FaultTolerantProxy}. Default `Cache::Default.new`.
23
- # @!attribute [r] storage
24
- # @return [Storage::Interface] The storage backend.
25
- # Automatically wrapped in a {Storage::FaultTolerantProxy}.
26
- # Default `Storage::Memory.new`.
27
- # @!attribute [r] listeners
28
- # @return [Array] listeners Faulty event listeners
29
- # @!attribute [r] notifier
30
- # @return [Events::Notifier] A Faulty notifier. If given, listeners are
31
- # ignored.
32
- Options = Struct.new(
33
- :cache,
34
- :storage,
35
- :listeners,
36
- :notifier
37
- ) do
38
- include ImmutableOptions
39
-
40
- private
41
-
42
- def finalize
43
- self.notifier ||= Events::Notifier.new(listeners || [])
44
-
45
- self.storage ||= Storage::Memory.new
46
- unless storage.fault_tolerant?
47
- self.storage = Storage::FaultTolerantProxy.new(storage, notifier: notifier)
48
- end
49
-
50
- self.cache ||= Cache::Default.new
51
- unless cache.fault_tolerant?
52
- self.cache = Cache::FaultTolerantProxy.new(cache, notifier: notifier)
53
- end
54
- end
55
-
56
- def required
57
- %i[cache storage notifier]
58
- end
59
-
60
- def defaults
61
- {
62
- listeners: [Events::LogListener.new]
63
- }
64
- end
65
- end
66
-
67
- # Create a new Faulty Scope
68
- #
69
- # Note, the process of creating a new scope is not thread safe,
70
- # so make sure scopes are setup before spawning threads.
71
- #
72
- # @see Options
73
- # @param options [Hash] Attributes for {Options}
74
- # @yield [Options] For setting options in a block
75
- def initialize(**options, &block)
76
- @circuits = Concurrent::Map.new
77
- @options = Options.new(options, &block)
78
- end
79
-
80
- # Create or retrieve a circuit
81
- #
82
- # Within a scope, circuit instances have unique names, so if the given circuit
83
- # name already exists, then the existing circuit will be returned, otherwise
84
- # a new circuit will be created. If an existing circuit is returned, then
85
- # the {options} param and block are ignored.
86
- #
87
- # @param name [String] The name of the circuit
88
- # @param options [Hash] Attributes for {Circuit::Options}
89
- # @yield [Circuit::Options] For setting options in a block
90
- # @return [Circuit] The new circuit or the existing circuit if it already exists
91
- def circuit(name, **options, &block)
92
- name = name.to_s
93
- options = options.merge(circuit_options)
94
- @circuits.compute_if_absent(name) do
95
- Circuit.new(name, **options, &block)
96
- end
97
- end
98
-
99
- # Get a list of all circuit names
100
- #
101
- # @return [Array<String>] The circuit names
102
- def list_circuits
103
- options.storage.list
104
- end
105
-
106
- private
107
-
108
- # Get circuit options from the scope options
109
- #
110
- # @return [Hash] The circuit options
111
- def circuit_options
112
- options = @options.to_h
113
- options.delete(:listeners)
114
- options
115
- end
116
- end
117
- end