faulty 0.1.4 → 0.5.1

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +49 -0
  3. data/.rubocop.yml +9 -0
  4. data/CHANGELOG.md +55 -0
  5. data/Gemfile +8 -3
  6. data/README.md +883 -310
  7. data/bin/check-version +5 -1
  8. data/faulty.gemspec +1 -1
  9. data/lib/faulty.rb +167 -43
  10. data/lib/faulty/cache.rb +3 -1
  11. data/lib/faulty/cache/auto_wire.rb +58 -0
  12. data/lib/faulty/cache/circuit_proxy.rb +61 -0
  13. data/lib/faulty/cache/default.rb +10 -21
  14. data/lib/faulty/cache/fault_tolerant_proxy.rb +15 -4
  15. data/lib/faulty/cache/interface.rb +1 -1
  16. data/lib/faulty/cache/mock.rb +1 -1
  17. data/lib/faulty/cache/null.rb +1 -1
  18. data/lib/faulty/cache/rails.rb +9 -10
  19. data/lib/faulty/circuit.rb +31 -16
  20. data/lib/faulty/error.rb +29 -7
  21. data/lib/faulty/events.rb +1 -1
  22. data/lib/faulty/events/callback_listener.rb +1 -1
  23. data/lib/faulty/events/honeybadger_listener.rb +1 -1
  24. data/lib/faulty/events/listener_interface.rb +1 -1
  25. data/lib/faulty/events/log_listener.rb +5 -6
  26. data/lib/faulty/events/notifier.rb +1 -1
  27. data/lib/faulty/immutable_options.rb +1 -1
  28. data/lib/faulty/patch.rb +154 -0
  29. data/lib/faulty/patch/base.rb +46 -0
  30. data/lib/faulty/patch/redis.rb +60 -0
  31. data/lib/faulty/result.rb +2 -2
  32. data/lib/faulty/status.rb +3 -2
  33. data/lib/faulty/storage.rb +4 -1
  34. data/lib/faulty/storage/auto_wire.rb +107 -0
  35. data/lib/faulty/storage/circuit_proxy.rb +64 -0
  36. data/lib/faulty/storage/fallback_chain.rb +207 -0
  37. data/lib/faulty/storage/fault_tolerant_proxy.rb +52 -57
  38. data/lib/faulty/storage/interface.rb +3 -2
  39. data/lib/faulty/storage/memory.rb +8 -4
  40. data/lib/faulty/storage/redis.rb +75 -13
  41. data/lib/faulty/version.rb +2 -2
  42. metadata +14 -7
  43. data/.travis.yml +0 -46
  44. data/lib/faulty/scope.rb +0 -117
data/bin/check-version CHANGED
@@ -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
data/faulty.gemspec CHANGED
@@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
27
27
  # Other non-essential development dependencies go in the Gemfile.
28
28
  spec.add_development_dependency 'connection_pool', '~> 2.0'
29
29
  spec.add_development_dependency 'honeybadger', '>= 2.0'
30
- spec.add_development_dependency 'redis', '~> 3.0'
30
+ spec.add_development_dependency 'redis', '>= 3.0'
31
31
  spec.add_development_dependency 'rspec', '~> 3.8'
32
32
  # 0.81 is the last rubocop version with Ruby 2.3 support
33
33
  spec.add_development_dependency 'rubocop', '0.81.0'
data/lib/faulty.rb CHANGED
@@ -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'
@@ -8,15 +9,18 @@ require 'faulty/cache'
8
9
  require 'faulty/circuit'
9
10
  require 'faulty/error'
10
11
  require 'faulty/events'
12
+ require 'faulty/patch'
11
13
  require 'faulty/result'
12
- require 'faulty/scope'
13
14
  require 'faulty/status'
14
15
  require 'faulty/storage'
15
16
 
16
- # The top-level namespace for Faulty
17
+ # The {Faulty} class has class-level methods for global state or can be
18
+ # instantiated to create an independent configuration.
17
19
  #
18
- # Fault-tolerance tools for ruby based on circuit-breakers
19
- module Faulty
20
+ # If you are using global state, call {Faulty#init} during your application's
21
+ # initialization. This is the simplest way to use {Faulty}. If you prefer, you
22
+ # can also call {Faulty.new} to create independent {Faulty} instances.
23
+ class Faulty
20
24
  class << self
21
25
  # Start the Faulty environment
22
26
  #
@@ -27,78 +31,88 @@ module Faulty
27
31
  # are spawned.
28
32
  #
29
33
  # If you prefer dependency-injection instead of global state, you can skip
30
- # init and pass a {Scope} directly to your dependencies.
34
+ # `init` and use {Faulty.new} to pass an instance directoy to your
35
+ # dependencies.
31
36
  #
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
37
+ # @param default_name [Symbol] The name of the default instance. Can be set
38
+ # to `nil` to skip creating a default instance.
39
+ # @param config [Hash] Attributes for {Faulty::Options}
40
+ # @yield [Faulty::Options] For setting options in a block
36
41
  # @return [self]
37
- def init(scope_name = :default, **config, &block)
38
- raise AlreadyInitializedError if @scopes
42
+ def init(default_name = :default, **config, &block)
43
+ raise AlreadyInitializedError if @instances
39
44
 
40
- @default_scope = scope_name
41
- @scopes = Concurrent::Map.new
42
- register(scope_name, Scope.new(**config, &block)) unless scope_name.nil?
45
+ @default_instance = default_name
46
+ @instances = Concurrent::Map.new
47
+ register(default_name, new(**config, &block)) unless default_name.nil?
43
48
  self
44
49
  rescue StandardError
45
- @scopes = nil
50
+ @instances = nil
46
51
  raise
47
52
  end
48
53
 
49
- # Get the default scope given during {.init}
54
+ # Get the default instance given during {.init}
50
55
  #
51
- # @return [Scope, nil] The default scope if it is registered
56
+ # @return [Faulty, nil] The default instance if it is registered
52
57
  def default
53
- raise UninitializedError unless @scopes
54
- raise MissingDefaultScopeError unless @default_scope
58
+ raise UninitializedError unless @instances
59
+ raise MissingDefaultInstanceError unless @default_instance
55
60
 
56
- self[@default_scope]
61
+ self[@default_instance]
57
62
  end
58
63
 
59
- # Get a scope by name
64
+ # Get an instance by name
60
65
  #
61
- # @return [Scope, nil] The named scope if it is registered
62
- def [](scope_name)
63
- raise UninitializedError unless @scopes
66
+ # @return [Faulty, nil] The named instance if it is registered
67
+ def [](name)
68
+ raise UninitializedError unless @instances
64
69
 
65
- @scopes[scope_name]
70
+ @instances[name.to_s]
66
71
  end
67
72
 
68
- # Register a scope to the global Faulty state
73
+ # Register an instance to the global Faulty state
69
74
  #
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.
75
+ # Will not replace an existing instance with the same name. Check the
76
+ # return value if you need to know whether the instance already existed.
72
77
  #
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
78
+ # @param name [Symbol] The name of the instance to register
79
+ # @param instance [Faulty] The instance to register. If nil, a new instance
80
+ # will be created from the given options or block.
81
+ # @param config [Hash] Attributes for {Faulty::Options}
82
+ # @yield [Faulty::Options] For setting options in a block
83
+ # @return [Faulty, nil] The previously-registered instance of that name if
76
84
  # it already existed, otherwise nil.
77
- def register(name, scope)
78
- raise UninitializedError unless @scopes
85
+ def register(name, instance = nil, **config, &block)
86
+ raise UninitializedError unless @instances
79
87
 
80
- @scopes.put_if_absent(name, scope)
88
+ if instance
89
+ raise ArgumentError, 'Do not give config options if an instance is given' if !config.empty? || block
90
+ else
91
+ instance = new(**config, &block)
92
+ end
93
+
94
+ @instances.put_if_absent(name.to_s, instance)
81
95
  end
82
96
 
83
- # Get the options for the default scope
97
+ # Get the options for the default instance
84
98
  #
85
- # @raise MissingDefaultScopeError If the default scope has not been created
86
- # @return [Scope::Options]
99
+ # @raise MissingDefaultInstanceError If the default instance has not been created
100
+ # @return [Faulty::Options]
87
101
  def options
88
102
  default.options
89
103
  end
90
104
 
91
- # Get or create a circuit for the default scope
105
+ # Get or create a circuit for the default instance
92
106
  #
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)
107
+ # @raise UninitializedError If the default instance has not been created
108
+ # @param (see Faulty#circuit)
109
+ # @yield (see Faulty#circuit)
110
+ # @return (see Faulty#circuit)
97
111
  def circuit(name, **config, &block)
98
112
  default.circuit(name, **config, &block)
99
113
  end
100
114
 
101
- # Get a list of all circuit names for the default scope
115
+ # Get a list of all circuit names for the default instance
102
116
  #
103
117
  # @return [Array<String>] The circuit names
104
118
  def list_circuits
@@ -115,4 +129,114 @@ module Faulty
115
129
  Time.now.to_i
116
130
  end
117
131
  end
132
+
133
+ attr_reader :options
134
+
135
+ # Options for {Faulty}
136
+ #
137
+ # @!attribute [r] cache
138
+ # @see Cache::AutoWire
139
+ # @return [Cache::Interface] A cache backend if you want
140
+ # to use Faulty's cache support. Automatically wrapped in a
141
+ # {Cache::AutoWire}. Default `Cache::AutoWire.new`.
142
+ # @!attribute [r] circuit_defaults
143
+ # @see Circuit::Options
144
+ # @return [Hash] A hash of default options to be used when creating
145
+ # new circuits. See {Circuit::Options} for a full list.
146
+ # @!attribute [r] storage
147
+ # @see Storage::AutoWire
148
+ # @return [Storage::Interface, Array<Storage::Interface>] The storage
149
+ # backend. Automatically wrapped in a {Storage::AutoWire}, so this can also
150
+ # be given an array of prioritized backends. Default `Storage::AutoWire.new`.
151
+ # @!attribute [r] listeners
152
+ # @see Events::ListenerInterface
153
+ # @return [Array] listeners Faulty event listeners
154
+ # @!attribute [r] notifier
155
+ # @return [Events::Notifier] A Faulty notifier. If given, listeners are
156
+ # ignored.
157
+ Options = Struct.new(
158
+ :cache,
159
+ :circuit_defaults,
160
+ :storage,
161
+ :listeners,
162
+ :notifier
163
+ ) do
164
+ include ImmutableOptions
165
+
166
+ private
167
+
168
+ def finalize
169
+ self.notifier ||= Events::Notifier.new(listeners || [])
170
+ self.storage = Storage::AutoWire.wrap(storage, notifier: notifier)
171
+ self.cache = Cache::AutoWire.wrap(cache, notifier: notifier)
172
+ end
173
+
174
+ def required
175
+ %i[cache circuit_defaults storage notifier]
176
+ end
177
+
178
+ def defaults
179
+ {
180
+ circuit_defaults: {},
181
+ listeners: [Events::LogListener.new]
182
+ }
183
+ end
184
+ end
185
+
186
+ # Create a new {Faulty} instance
187
+ #
188
+ # Note, the process of creating a new instance is not thread safe,
189
+ # so make sure instances are setup during your application's initialization
190
+ # phase.
191
+ #
192
+ # For the most part, {Faulty} instances are independent, however for some
193
+ # cache and storage backends, you will need to ensure that the cache keys
194
+ # and circuit names don't overlap between instances. For example, if using the
195
+ # {Storage::Redis} storage backend, you should specify different key
196
+ # prefixes for each instance.
197
+ #
198
+ # @see Options
199
+ # @param options [Hash] Attributes for {Options}
200
+ # @yield [Options] For setting options in a block
201
+ def initialize(**options, &block)
202
+ @circuits = Concurrent::Map.new
203
+ @options = Options.new(options, &block)
204
+ end
205
+
206
+ # Create or retrieve a circuit
207
+ #
208
+ # Within an instance, circuit instances have unique names, so if the given circuit
209
+ # name already exists, then the existing circuit will be returned, otherwise
210
+ # a new circuit will be created. If an existing circuit is returned, then
211
+ # the {options} param and block are ignored.
212
+ #
213
+ # @param name [String] The name of the circuit
214
+ # @param options [Hash] Attributes for {Circuit::Options}
215
+ # @yield [Circuit::Options] For setting options in a block
216
+ # @return [Circuit] The new circuit or the existing circuit if it already exists
217
+ def circuit(name, **options, &block)
218
+ name = name.to_s
219
+ @circuits.compute_if_absent(name) do
220
+ options = circuit_options.merge(options)
221
+ Circuit.new(name, **options, &block)
222
+ end
223
+ end
224
+
225
+ # Get a list of all circuit names
226
+ #
227
+ # @return [Array<String>] The circuit names
228
+ def list_circuits
229
+ options.storage.list
230
+ end
231
+
232
+ private
233
+
234
+ # Get circuit options from the {Faulty} options
235
+ #
236
+ # @return [Hash] The circuit options
237
+ def circuit_options
238
+ @options.to_h
239
+ .select { |k, _v| %i[cache storage notifier].include?(k) }
240
+ .merge(options.circuit_defaults)
241
+ end
118
242
  end
data/lib/faulty/cache.rb CHANGED
@@ -1,12 +1,14 @@
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
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,58 @@
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
+ # Options for {AutoWire}
10
+ #
11
+ # @!attribute [r] circuit
12
+ # @return [Circuit] A circuit for {CircuitProxy} if one is created.
13
+ # When modifying this, be careful to use only a reliable circuit
14
+ # storage backend so that you don't introduce cascading failures.
15
+ # @!attribute [r] notifier
16
+ # @return [Events::Notifier] A Faulty notifier. If given, listeners are
17
+ # ignored.
18
+ Options = Struct.new(
19
+ :circuit,
20
+ :notifier
21
+ ) do
22
+ include ImmutableOptions
23
+
24
+ private
25
+
26
+ def required
27
+ %i[notifier]
28
+ end
29
+ end
30
+
31
+ class << self
32
+ # Wrap a cache backend with sensible defaults
33
+ #
34
+ # If the cache is `nil`, create a new {Default}.
35
+ #
36
+ # If the backend is not fault tolerant, wrap it in {CircuitProxy} and
37
+ # {FaultTolerantProxy}.
38
+ #
39
+ # @param cache [Interface] A cache backend
40
+ # @param options [Hash] Attributes for {Options}
41
+ # @yield [Options] For setting options in a block
42
+ def wrap(cache, **options, &block)
43
+ options = Options.new(options, &block)
44
+ if cache.nil?
45
+ Cache::Default.new
46
+ elsif cache.fault_tolerant?
47
+ cache
48
+ else
49
+ Cache::FaultTolerantProxy.new(
50
+ Cache::CircuitProxy.new(cache, circuit: options.circuit, notifier: options.notifier),
51
+ notifier: options.notifier
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ 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
@@ -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
  #
@@ -11,6 +11,8 @@ module Faulty
11
11
  # - If ActiveSupport is available, it will use an `ActiveSupport::Cache::MemoryStore`
12
12
  # - Otherwise it will use a {Faulty::Cache::Null}
13
13
  class Default
14
+ extend Forwardable
15
+
14
16
  def initialize
15
17
  @cache = if defined?(::Rails)
16
18
  Cache::Rails.new(::Rails.cache)
@@ -21,28 +23,15 @@ module Faulty
21
23
  end
22
24
  end
23
25
 
24
- # Read from the internal cache by key
26
+ # @!method read(key)
27
+ # (see Faulty::Cache::Interface#read)
25
28
  #
26
- # @param (see Cache::Interface#read)
27
- # @return (see Cache::Interface#read)
28
- def read(key)
29
- @cache.read(key)
30
- end
31
-
32
- # Write to the internal cache
29
+ # @!method write(key, value, expires_in: expires_in)
30
+ # (see Faulty::Cache::Interface#write)
33
31
  #
34
- # @param (see Cache::Interface#read)
35
- # @return (see Cache::Interface#read)
36
- def write(key, value, expires_in: nil)
37
- @cache.write(key, value, expires_in: expires_in)
38
- end
39
-
40
- # This cache is fault tolerant if the internal one is
41
- #
42
- # @return [Boolean]
43
- def fault_tolerant?
44
- @cache.fault_tolerant?
45
- end
32
+ # @!method fault_tolerant
33
+ # (see Faulty::Cache::Interface#fault_tolerant?)
34
+ def_delegators :@cache, :read, :write, :fault_tolerant?
46
35
  end
47
36
  end
48
37
  end