faulty 0.2.0 → 0.3.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: 55bf688c3615a59f51103633db0902abf3458324945ab855894e5975981868f5
4
- data.tar.gz: dab412aad317a79364782a85c2a6a184e55fa2214cd966655d74eeab69e248c0
3
+ metadata.gz: c0712dd797dfe2922e4e52a792f48c5ac72c4b9766d218dfa597e7ad630f9fd0
4
+ data.tar.gz: fb4a787507006131f34a301fc164466fd8befb72051550ca0a63fc2d69ec949c
5
5
  SHA512:
6
- metadata.gz: 7527a23df0c042ea317e99b8fafdf8c8f96ac5f9f68323ba4c4a2901f8e32dc7ca2b7c62b6b7197e446985eab6901f85dd41c6463407ae3632d86e022626d733
7
- data.tar.gz: 7e3d48b82905b0ad2303449a532e85736a0681fa4dcb8a5f9ee64e6ffd10c76fdc760ac97aa9dd882f7a019d335440f9d91f0ba1efbb92f978c5e9e43c5b977a
6
+ metadata.gz: 5381dd13f058045906330c67ff80aeefac55ae81077eb4552d5461d743344fa097e205b8ac3a39b385c52976e64315aec4e903c1a0c298a05452fab7788d1df2
7
+ data.tar.gz: af31cbf86cac54226189f8f63b1d8b0bcf82f9023085e98d07e4e9db65e402883c2219dbf255e21427c40b0c0a66ec71bd4dc9dfdd94fb9bc7b574ddf8426674
@@ -47,6 +47,9 @@ RSpec/FilePath:
47
47
  RSpec/NamedSubject:
48
48
  Enabled: false
49
49
 
50
+ RSpec/MessageSpies:
51
+ Enabled: false
52
+
50
53
  RSpec/MultipleExpectations:
51
54
  Enabled: false
52
55
 
@@ -6,8 +6,8 @@ rvm:
6
6
  - 2.5
7
7
  - 2.6
8
8
  - 2.7
9
- - jruby
10
- - truffleruby
9
+ - jruby-9.2.10
10
+ - truffleruby-20.2.0
11
11
 
12
12
  env:
13
13
  global:
@@ -1,3 +1,12 @@
1
+ ## Release v0.3.0
2
+
3
+ * Add tools for backend fault-tolerance #10
4
+ * CircuitProxy for wrapping storage in an internal circuit
5
+ * FallbackChain storage backend for falling back to stable storage
6
+ * Timeout warnings for Redis backend
7
+ * AutoWire wrappers for automatically configuring storage and cache
8
+ * Better documentation for fault-tolerance
9
+
1
10
  ## Release v0.2.0
2
11
 
3
12
  * Remove Scopes and replace them with Faulty instances #9
data/README.md CHANGED
@@ -172,7 +172,8 @@ end.or_default([])
172
172
 
173
173
  ## How it Works
174
174
 
175
- Faulty implements a version of circuit breakers inspired by
175
+ Faulty implements a version of circuit breakers inspired by "Release It!: Design
176
+ and Deploy Production-Ready Software" by [Michael T. Nygard][michael nygard] and
176
177
  [Martin Fowler's post][martin fowler] on the subject. A few notable features of
177
178
  Faulty's implementation are:
178
179
 
@@ -180,6 +181,7 @@ Faulty's implementation are:
180
181
  - Integrated caching inspired by Netflix's [Hystrix][hystrix] with automatic
181
182
  cache jitter and error fallback.
182
183
  - Event-based monitoring
184
+ - Flexible fault-tolerant storage with optional fallbacks
183
185
 
184
186
  Following the principals of the circuit-breaker pattern, the block given to
185
187
  `run` or `try_run` will always be executed as long as it never raises an error.
@@ -215,11 +217,18 @@ Faulty.init do |config|
215
217
  # The cache backend to use. By default, Faulty looks for a Rails cache. If
216
218
  # that's not available, it uses an ActiveSupport::Cache::Memory instance.
217
219
  # Otherwise, it uses a Faulty::Cache::Null and caching is disabled.
220
+ # Whatever backend is given here is automatically wrapped in
221
+ # Faulty::Cache::AutoWire. This adds fault-tolerance features, see the
222
+ # AutoWire API docs for more details.
218
223
  config.cache = Faulty::Cache::Default.new
219
224
 
220
225
  # The storage backend. By default, Faulty uses an in-memory store. For most
221
226
  # production applications, you'll want a more robust backend. Faulty also
222
227
  # provides Faulty::Storage::Redis for this.
228
+ # Whatever backend is given here is automatically wrapped in
229
+ # Faulty::Storage::AutoWire. This adds fault-tolerance features, see the
230
+ # AutoWire APi docs for more details. If an array of storage backends is
231
+ # given, each one will be tried in order until one succeeds.
223
232
  config.storage = Faulty::Storage::Memory.new
224
233
 
225
234
  # An array of event listeners. Each object in the array should implement
@@ -380,7 +389,12 @@ Faulty backends are fault-tolerant by default. Any `StandardError`s raised by
380
389
  the storage or cache backends are captured and suppressed. Failure events for
381
390
  these errors are sent to the notifier.
382
391
 
383
- If the storage backend fails, all circuits will default to open. If the cache
392
+ In case of a flaky storage or cache backend, Faulty also uses independent
393
+ in-memory circuits to track failures so that we don't keep calling a backend
394
+ that is failing. See the API docs for `Cache::AutoWire`, and `Storage::AutoWire`
395
+ for more details.
396
+
397
+ If the storage backend fails, circuits will default to closed. If the cache
384
398
  backend fails, all cache queries will miss.
385
399
 
386
400
  ## Event Handling
@@ -456,10 +470,19 @@ end
456
470
 
457
471
  ## Configuring the Storage Backend
458
472
 
473
+ A storage backend is required to use Faulty. By default, it uses in-memory
474
+ storage, but Redis is also available, along with a number of wrappers used to
475
+ improve resiliency and fault-tolerance.
476
+
459
477
  ### Memory
460
478
 
461
- The `Faulty::Cache::Memory` backend is the default storage backend. The default
462
- configuration:
479
+ The `Faulty::Storage::Memory` backend is the default storage backend. You may
480
+ prefer this implementation if you want to avoid the complexity and potential
481
+ failure-mode of cross-network circuit storage. The trade-off is that circuit
482
+ state is only contained within a single process and will not be saved across
483
+ application restarts. Locks will also be cleared on restart.
484
+
485
+ The default configuration:
463
486
 
464
487
  ```ruby
465
488
  Faulty.init do |config|
@@ -472,15 +495,22 @@ end
472
495
 
473
496
  ### Redis
474
497
 
475
- The `Faulty::Cache::Redis` backend provides distributed circuit storage using
476
- Redis. The default configuration:
498
+ The `Faulty::Storage::Redis` backend provides distributed circuit storage using
499
+ Redis. Although Faulty takes steps to reduce risk
500
+ (See [Fault Tolerance](#fault-tolerance)), using cross-network storage does
501
+ introduce some additional failure modes. To reduce this risk, be sure to set
502
+ conservative timeouts for your Redis connection. Setting high timeouts will
503
+ print warnings to stderr.
504
+
505
+ The default configuration:
477
506
 
478
507
  ```ruby
479
508
  Faulty.init do |config|
480
509
  config.storage = Faulty::Storage::Redis.new do |storage|
481
510
  # The Redis client. Accepts either a Redis instance, or a ConnectionPool
482
- # of Redis instances.
483
- storage.client = ::Redis.new
511
+ # of Redis instances. A low timeout is highly recommended to prevent
512
+ # cascading failures when evaluating circuits.
513
+ storage.client = ::Redis.new(timeout: 1)
484
514
 
485
515
  # The prefix to prepend to all redis keys used by Faulty circuits
486
516
  storage.key_prefix = 'faulty'
@@ -493,10 +523,126 @@ Faulty.init do |config|
493
523
 
494
524
  # The maximum number of seconds that a circuit run will be stored
495
525
  storage.sample_ttl = 1800
526
+
527
+ # The maximum number of seconds to store a circuit. Does not apply to
528
+ # locks, which are indefinite.
529
+ storage.circuit_ttl = 604_800 # 1 Week
530
+
531
+ # The number of seconds between circuit expirations. Changing this setting
532
+ # is not recommended. See API docs for more implementation details.
533
+ storage.list_granularity = 3600
534
+
535
+ # If true, disables warnings about recommended client settings like timeouts
536
+ storage.disable_warnings = false
496
537
  end
497
538
  end
498
539
  ```
499
540
 
541
+ ### FallbackChain
542
+
543
+ The `Faulty::Storage::FallbackChain` backend is a wrapper for multiple
544
+ prioritized storage backends. If the first backend in the chain fails,
545
+ consecutive backends are tried until one succeeds. The recommended use-case for
546
+ this is to fall back on reliable storage if a networked storage backend fails.
547
+
548
+ For example, you may configure Redis as your primary storage backend, with an
549
+ in-memory storage backend as a fallback:
550
+
551
+ ```ruby
552
+ Faulty.init do |config|
553
+ config.storage = Faulty::Storage::FallbackChain.new([
554
+ Faulty::Storage::Redis.new,
555
+ Faulty::Storage::Memory.new
556
+ ])
557
+ end
558
+ ```
559
+
560
+ Faulty instances will automatically use a fallback chain if an array is given to
561
+ the `storage` option, so this example is equivalent to the above:
562
+
563
+ ```ruby
564
+ Faulty.init do |config|
565
+ config.storage = [
566
+ Faulty::Storage::Redis.new,
567
+ Faulty::Storage::Memory.new
568
+ ]
569
+ end
570
+ ```
571
+
572
+ If the fallback chain fails-over to backup storage, circuit states will not
573
+ carry over, so failover could be temporarily disruptive to your application.
574
+ However, any calls to `#lock` or `#unlock` will always be persisted to all
575
+ backends so that locks are maintained during failover.
576
+
577
+ ### Storage::FaultTolerantProxy
578
+
579
+ This wrapper is applied to all non-fault-tolerant storage backends by default
580
+ (see the [API docs for `Faulty::Storage::AutoWire`](https://www.rubydoc.info/gems/faulty/Faulty/Storage/AutoWire)).
581
+
582
+ `Faulty::Storage::FaultTolerantProxy` is a wrapper that suppresses storage
583
+ errors and returns sensible defaults during failures. If a storage backend is
584
+ failing, all circuits will be treated as closed regardless of locks or previous
585
+ history.
586
+
587
+ If you wish your application to use a secondary storage backend instead of
588
+ failing closed, use `FallbackChain`.
589
+
590
+ ### Storage::CircuitProxy
591
+
592
+ This wrapper is applied to all non-fault-tolerant storage backends by default
593
+ (see the [API docs for `Faulty::Storage::AutoWire`](https://www.rubydoc.info/gems/faulty/Faulty/Cache/AutoWire)).
594
+
595
+ `Faulty::Storage::CircuitProxy` is a wrapper that uses an independent in-memory
596
+ circuit to track failures to storage backends. If a storage backend fails
597
+ continuously, it will be temporarily disabled and raise `Faulty::CircuitError`s.
598
+
599
+ Typically this is used inside a `FaultTolerantProxy` or `FallbackChain` so that
600
+ these storage failures are handled gracefully.
601
+
602
+ ## Configuring the Cache Backend
603
+
604
+ ### Null
605
+
606
+ The `Faulty::Cache::Null` cache disables caching. It is the default if Rails and
607
+ ActiveSupport are not present.
608
+
609
+ ### Rails
610
+
611
+ `Faulty::Cache::Rails` is the default cache if Rails or ActiveSupport are
612
+ present. If Rails is present, it uses `Rails.cache` as the backend. If
613
+ ActiveSupport is present, but Rails is not, it creates a new
614
+ `ActiveSupport::Cache::MemoryStore` by default. This backend can be used with
615
+ any `ActiveSupport::Cache`.
616
+
617
+ ```ruby
618
+ Faulty.init do |config|
619
+ config.cache = Faulty::Cache::Rails.new(
620
+ ActiveSupport::Cache::RedisCacheStore.new
621
+ )
622
+ end
623
+ ```
624
+
625
+ ### Cache::FaultTolerantProxy
626
+
627
+ This wrapper is applied to all non-fault-tolerant cache backends by default
628
+ (see the API docs for `Faulty::Cache::AutoWire`).
629
+
630
+ `Faulty::Cache::FaultTolerantProxy` is a wrapper that suppresses cache errors
631
+ and acts like a null cache during failures. Reads always return `nil`, and
632
+ writes are no-ops.
633
+
634
+ ### Cache::CircuitProxy
635
+
636
+ This wrapper is applied to all non-fault-tolerant circuit backends by default
637
+ (see the API docs for `Faulty::Circuit::AutoWire`).
638
+
639
+ `Faulty::Circuit::CircuitProxy` is a wrapper that uses an independent in-memory
640
+ circuit to track failures to circuit backends. If a circuit backend fails
641
+ continuously, it will be temporarily disabled and raise `Faulty::CircuitError`s.
642
+
643
+ Typically this is used inside a `FaultTolerantProxy` so that these cache
644
+ failures are handled gracefully.
645
+
500
646
  ## Listing Circuits
501
647
 
502
648
  For monitoring or debugging, you may need to retrieve a list of all circuit
@@ -661,7 +807,7 @@ but there are and have been many other options:
661
807
  - [semian](https://github.com/Shopify/semian): A resiliency toolkit that
662
808
  includes circuit breakers. It uses adapters to auto-wire circuits, and it has
663
809
  only host-local storage by design.
664
- - [circuitbox](https://github.com/yammer/circuitbox): Similar in function to
810
+ - [circuitbox](https://github.com/yammer/circuitbox): Similar in design to
665
811
  Faulty, but with a different API. It uses Moneta to abstract circuit storage
666
812
  to allow any key-value store.
667
813
 
@@ -680,10 +826,12 @@ but there are and have been many other options:
680
826
 
681
827
  - Simple API but configurable for advanced users
682
828
  - Pluggable storage backends (circuitbox also has this)
829
+ - Protected storage access with fallback to safe storage
683
830
  - Global, or object-oriented configuration with multiple instances
684
831
  - Integrated caching support tailored for fault-tolerance
685
832
  - Manually lock circuits open or closed
686
833
 
687
834
  [api docs]: https://www.rubydoc.info/github/ParentSquare/faulty/master
835
+ [michael nygard]: https://www.michaelnygard.com/
688
836
  [martin fowler]: https://www.martinfowler.com/bliki/CircuitBreaker.html
689
837
  [hystrix]: https://github.com/Netflix/Hystrix/wiki/How-it-Works
@@ -3,8 +3,12 @@
3
3
  set -e
4
4
 
5
5
  tag="$(git describe --abbrev=0 2>/dev/null || echo)"
6
+ echo "Tag: ${tag}"
6
7
  tag="${tag#v}"
8
+ echo "Git Version: ${tag}"
7
9
  [ "$tag" = '' ] && exit 0
10
+ gem_version="$(ruby -r ./lib/faulty/version -e "puts Faulty.version" | tail -n1)"
11
+ echo "Gem Version: ${gem_version}"
8
12
 
9
- tag_gt_version=$(ruby -r ./lib/faulty/version -e "puts Faulty.version >= Gem::Version.new('${tag}')")
13
+ tag_gt_version="$(ruby -r ./lib/faulty/version -e "puts Faulty.version >= Gem::Version.new('${tag}')" | tail -n1)"
10
14
  test "$tag_gt_version" = true
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'securerandom'
4
+ require 'forwardable'
4
5
  require 'concurrent-ruby'
5
6
 
6
7
  require 'faulty/immutable_options'
@@ -124,14 +125,17 @@ class Faulty
124
125
  # Options for {Faulty}
125
126
  #
126
127
  # @!attribute [r] cache
128
+ # @see Cache::AutoWire
127
129
  # @return [Cache::Interface] A cache backend if you want
128
130
  # to use Faulty's cache support. Automatically wrapped in a
129
- # {Cache::FaultTolerantProxy}. Default `Cache::Default.new`.
131
+ # {Cache::AutoWire}. Default `Cache::AutoWire.new`.
130
132
  # @!attribute [r] storage
131
- # @return [Storage::Interface] The storage backend.
132
- # Automatically wrapped in a {Storage::FaultTolerantProxy}.
133
- # Default `Storage::Memory.new`.
133
+ # @see Storage::AutoWire
134
+ # @return [Storage::Interface, Array<Storage::Interface>] The storage
135
+ # backend. Automatically wrapped in a {Storage::AutoWire}, so this can also
136
+ # be given an array of prioritized backends. Default `Storage::AutoWire.new`.
134
137
  # @!attribute [r] listeners
138
+ # @see Events::ListenerInterface
135
139
  # @return [Array] listeners Faulty event listeners
136
140
  # @!attribute [r] notifier
137
141
  # @return [Events::Notifier] A Faulty notifier. If given, listeners are
@@ -148,16 +152,8 @@ class Faulty
148
152
 
149
153
  def finalize
150
154
  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
155
+ self.storage = Storage::AutoWire.new(storage, notifier: notifier)
156
+ self.cache = Cache::AutoWire.new(cache, notifier: notifier)
161
157
  end
162
158
 
163
159
  def required
@@ -223,8 +219,6 @@ class Faulty
223
219
  #
224
220
  # @return [Hash] The circuit options
225
221
  def circuit_options
226
- options = @options.to_h
227
- options.delete(:listeners)
228
- options
222
+ @options.to_h.select { |k, _v| %i[cache storage notifier].include?(k) }
229
223
  end
230
224
  end
@@ -6,7 +6,9 @@ class Faulty
6
6
  end
7
7
  end
8
8
 
9
+ require 'faulty/cache/auto_wire'
9
10
  require 'faulty/cache/default'
11
+ require 'faulty/cache/circuit_proxy'
10
12
  require 'faulty/cache/fault_tolerant_proxy'
11
13
  require 'faulty/cache/mock'
12
14
  require 'faulty/cache/null'
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Faulty
4
+ module Cache
5
+ # Automatically configure a cache backend
6
+ #
7
+ # Used by {Faulty#initialize} to setup sensible cache defaults
8
+ class AutoWire
9
+ extend Forwardable
10
+
11
+ # Options for {AutoWire}
12
+ Options = Struct.new(
13
+ :notifier
14
+ ) do
15
+ include ImmutableOptions
16
+
17
+ private
18
+
19
+ def required
20
+ %i[notifier]
21
+ end
22
+ end
23
+
24
+ # Wrap a cache backend with sensible defaults
25
+ #
26
+ # If the cache is `nil`, create a new {Default}.
27
+ #
28
+ # If the backend is not fault tolerant, wrap it in {CircuitProxy} and
29
+ # {FaultTolerantProxy}.
30
+ #
31
+ # @param cache [Interface] A cache backend
32
+ # @param options [Hash] Attributes for {Options}
33
+ # @yield [Options] For setting options in a block
34
+ def initialize(cache, **options, &block)
35
+ @options = Options.new(options, &block)
36
+ @cache = if cache.nil?
37
+ Cache::Default.new
38
+ elsif cache.fault_tolerant?
39
+ cache
40
+ else
41
+ Cache::FaultTolerantProxy.new(
42
+ Cache::CircuitProxy.new(cache, notifier: @options.notifier),
43
+ notifier: @options.notifier
44
+ )
45
+ end
46
+
47
+ freeze
48
+ end
49
+
50
+ # @!method read(key)
51
+ # (see Faulty::Cache::Interface#read)
52
+ #
53
+ # @!method write(key, value, expires_in: expires_in)
54
+ # (see Faulty::Cache::Interface#write)
55
+ def_delegators :@cache, :read, :write
56
+
57
+ # Auto-wired caches are always fault tolerant
58
+ #
59
+ # @return [true]
60
+ def fault_tolerant?
61
+ true
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Faulty
4
+ module Cache
5
+ # A circuit wrapper for cache backends
6
+ #
7
+ # This class uses an internal {Circuit} to prevent the cache backend from
8
+ # causing application issues. If the backend fails continuously, this
9
+ # circuit will trip to prevent cascading failures. This internal circuit
10
+ # uses an independent in-memory backend by default.
11
+ class CircuitProxy
12
+ attr_reader :options
13
+
14
+ # Options for {CircuitProxy}
15
+ #
16
+ # @!attribute [r] circuit
17
+ # @return [Circuit] A replacement for the internal circuit. When
18
+ # modifying this, be careful to use only a reliable circuit storage
19
+ # backend so that you don't introduce cascading failures.
20
+ # @!attribute [r] notifier
21
+ # @return [Events::Notifier] A Faulty notifier to use for failure
22
+ # notifications. If `circuit` is given, this is ignored.
23
+ Options = Struct.new(
24
+ :circuit,
25
+ :notifier
26
+ ) do
27
+ include ImmutableOptions
28
+
29
+ private
30
+
31
+ def finalize
32
+ raise ArgumentError, 'The circuit or notifier option must be given' unless notifier || circuit
33
+
34
+ self.circuit ||= Circuit.new(
35
+ Faulty::Storage::CircuitProxy.name,
36
+ notifier: notifier,
37
+ cache: Cache::Null.new
38
+ )
39
+ end
40
+ end
41
+
42
+ # @param cache [Cache::Interface] The cache backend to wrap
43
+ # @param options [Hash] Attributes for {Options}
44
+ # @yield [Options] For setting options in a block
45
+ def initialize(cache, **options, &block)
46
+ @cache = cache
47
+ @options = Options.new(options, &block)
48
+ end
49
+
50
+ %i[read write].each do |method|
51
+ define_method(method) do |*args|
52
+ options.circuit.run { @cache.public_send(method, *args) }
53
+ end
54
+ end
55
+
56
+ def fault_tolerant?
57
+ @cache.fault_tolerant?
58
+ end
59
+ end
60
+ end
61
+ end