faulty 0.1.2 → 0.4.0

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