faulty 0.1.4 → 0.2.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +13 -0
- data/README.md +58 -38
- data/lib/faulty.rb +155 -43
- data/lib/faulty/cache.rb +1 -1
- data/lib/faulty/cache/default.rb +1 -1
- data/lib/faulty/cache/fault_tolerant_proxy.rb +2 -2
- data/lib/faulty/cache/interface.rb +1 -1
- data/lib/faulty/cache/mock.rb +1 -1
- data/lib/faulty/cache/null.rb +1 -1
- data/lib/faulty/cache/rails.rb +1 -1
- data/lib/faulty/circuit.rb +1 -1
- data/lib/faulty/error.rb +4 -4
- data/lib/faulty/events.rb +1 -1
- data/lib/faulty/events/callback_listener.rb +1 -1
- data/lib/faulty/events/honeybadger_listener.rb +1 -1
- data/lib/faulty/events/listener_interface.rb +1 -1
- data/lib/faulty/events/log_listener.rb +1 -1
- data/lib/faulty/events/notifier.rb +1 -1
- data/lib/faulty/immutable_options.rb +1 -1
- data/lib/faulty/result.rb +2 -2
- data/lib/faulty/status.rb +1 -1
- data/lib/faulty/storage.rb +1 -1
- data/lib/faulty/storage/fault_tolerant_proxy.rb +2 -2
- data/lib/faulty/storage/interface.rb +1 -1
- data/lib/faulty/storage/memory.rb +1 -1
- data/lib/faulty/storage/redis.rb +6 -6
- data/lib/faulty/version.rb +2 -2
- metadata +1 -2
- data/lib/faulty/scope.rb +0 -117
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 55bf688c3615a59f51103633db0902abf3458324945ab855894e5975981868f5
|
4
|
+
data.tar.gz: dab412aad317a79364782a85c2a6a184e55fa2214cd966655d74eeab69e248c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7527a23df0c042ea317e99b8fafdf8c8f96ac5f9f68323ba4c4a2901f8e32dc7ca2b7c62b6b7197e446985eab6901f85dd41c6463407ae3632d86e022626d733
|
7
|
+
data.tar.gz: 7e3d48b82905b0ad2303449a532e85736a0681fa4dcb8a5f9ee64e6ffd10c76fdc760ac97aa9dd882f7a019d335440f9d91f0ba1efbb92f978c5e9e43c5b977a
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -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
|
-
##
|
206
|
+
## Configuration
|
207
207
|
|
208
|
-
|
209
|
-
illustrates the default values.
|
210
|
-
|
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 [
|
248
|
-
within the context of each
|
249
|
-
`Faulty.circuit` (or `
|
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
|
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
|
484
|
-
|
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
|
-
##
|
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
|
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
|
551
|
+
configurations.
|
532
552
|
|
533
|
-
### The default
|
553
|
+
### The default instance
|
534
554
|
|
535
|
-
When you call `Faulty.init`, you are actually creating the default
|
536
|
-
can access this
|
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
|
559
|
+
# We create the default instance
|
540
560
|
Faulty.init
|
541
561
|
|
542
|
-
# Access the default
|
543
|
-
|
562
|
+
# Access the default instance
|
563
|
+
faulty = Faulty.default
|
544
564
|
|
545
|
-
# Alternatively, access the
|
546
|
-
|
565
|
+
# Alternatively, access the instance by name
|
566
|
+
faulty = Faulty[:default]
|
547
567
|
```
|
548
568
|
|
549
|
-
You can rename the default
|
569
|
+
You can rename the default instance if desired:
|
550
570
|
|
551
571
|
```ruby
|
552
572
|
Faulty.init(:custom_default)
|
553
573
|
|
554
|
-
|
555
|
-
|
574
|
+
instance = Faulty.default
|
575
|
+
instance = Faulty[:custom_default]
|
556
576
|
```
|
557
577
|
|
558
|
-
### Multiple
|
578
|
+
### Multiple Instances
|
559
579
|
|
560
|
-
If you want multiple
|
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
|
-
|
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,
|
588
|
+
Faulty.register(:api, api_faulty)
|
569
589
|
|
570
|
-
# Now access the
|
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
|
576
|
-
|
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
|
602
|
+
### Standalone Instances
|
583
603
|
|
584
|
-
If you choose, you can use Faulty
|
585
|
-
|
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
|
608
|
+
faulty = Faulty.new
|
589
609
|
faulty.circuit(:standalone_circuit)
|
590
610
|
```
|
591
611
|
|
592
|
-
Calling
|
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
|
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
|
|
data/lib/faulty.rb
CHANGED
@@ -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
|
15
|
+
# The {Faulty} class has class-level methods for global state or can be
|
16
|
+
# instantiated to create an independent configuration.
|
17
17
|
#
|
18
|
-
#
|
19
|
-
|
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
|
32
|
+
# `init` and use {Faulty.new} to pass an instance directoy to your
|
33
|
+
# dependencies.
|
31
34
|
#
|
32
|
-
# @param
|
33
|
-
#
|
34
|
-
# @param config [Hash] Attributes for {
|
35
|
-
# @yield [
|
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(
|
38
|
-
raise AlreadyInitializedError if @
|
40
|
+
def init(default_name = :default, **config, &block)
|
41
|
+
raise AlreadyInitializedError if @instances
|
39
42
|
|
40
|
-
@
|
41
|
-
@
|
42
|
-
register(
|
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
|
-
@
|
48
|
+
@instances = nil
|
46
49
|
raise
|
47
50
|
end
|
48
51
|
|
49
|
-
# Get the default
|
52
|
+
# Get the default instance given during {.init}
|
50
53
|
#
|
51
|
-
# @return [
|
54
|
+
# @return [Faulty, nil] The default instance if it is registered
|
52
55
|
def default
|
53
|
-
raise UninitializedError unless @
|
54
|
-
raise
|
56
|
+
raise UninitializedError unless @instances
|
57
|
+
raise MissingDefaultInstanceError unless @default_instance
|
55
58
|
|
56
|
-
self[@
|
59
|
+
self[@default_instance]
|
57
60
|
end
|
58
61
|
|
59
|
-
# Get
|
62
|
+
# Get an instance by name
|
60
63
|
#
|
61
|
-
# @return [
|
62
|
-
def [](
|
63
|
-
raise UninitializedError unless @
|
64
|
+
# @return [Faulty, nil] The named instance if it is registered
|
65
|
+
def [](name)
|
66
|
+
raise UninitializedError unless @instances
|
64
67
|
|
65
|
-
@
|
68
|
+
@instances[name]
|
66
69
|
end
|
67
70
|
|
68
|
-
# Register
|
71
|
+
# Register an instance to the global Faulty state
|
69
72
|
#
|
70
|
-
# Will not replace an existing
|
71
|
-
# return value if you need to know whether the
|
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
|
74
|
-
# @param
|
75
|
-
# @return [
|
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,
|
78
|
-
raise UninitializedError unless @
|
80
|
+
def register(name, instance)
|
81
|
+
raise UninitializedError unless @instances
|
79
82
|
|
80
|
-
@
|
83
|
+
@instances.put_if_absent(name, instance)
|
81
84
|
end
|
82
85
|
|
83
|
-
# Get the options for the default
|
86
|
+
# Get the options for the default instance
|
84
87
|
#
|
85
|
-
# @raise
|
86
|
-
# @return [
|
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
|
94
|
+
# Get or create a circuit for the default instance
|
92
95
|
#
|
93
|
-
# @raise UninitializedError If the default
|
94
|
-
# @param (see
|
95
|
-
# @yield (see
|
96
|
-
# @return (see
|
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
|
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
|
data/lib/faulty/cache.rb
CHANGED
data/lib/faulty/cache/default.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
class Faulty
|
4
4
|
module Cache
|
5
5
|
# A wrapper for cache backends that may raise errors
|
6
6
|
#
|
7
|
-
# {
|
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
|
data/lib/faulty/cache/mock.rb
CHANGED
data/lib/faulty/cache/null.rb
CHANGED
data/lib/faulty/cache/rails.rb
CHANGED
data/lib/faulty/circuit.rb
CHANGED
data/lib/faulty/error.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
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
|
24
|
-
class
|
23
|
+
# Raised if getting the default instance without initializing one
|
24
|
+
class MissingDefaultInstanceError < FaultyError
|
25
25
|
def initialize(message = nil)
|
26
|
-
message ||= 'No default
|
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
|
data/lib/faulty/events.rb
CHANGED
data/lib/faulty/result.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
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)
|
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
|
data/lib/faulty/status.rb
CHANGED
data/lib/faulty/storage.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
class Faulty
|
4
4
|
module Storage
|
5
5
|
# A wrapper for storage backends that may raise errors
|
6
6
|
#
|
7
|
-
# {
|
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
|
data/lib/faulty/storage/redis.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
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
|
data/lib/faulty/version.rb
CHANGED
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.
|
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
|
data/lib/faulty/scope.rb
DELETED
@@ -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
|