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