faulty 0.1.1 → 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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -0
  3. data/.travis.yml +4 -2
  4. data/CHANGELOG.md +37 -1
  5. data/Gemfile +17 -0
  6. data/README.md +333 -55
  7. data/bin/check-version +5 -1
  8. data/bin/console +1 -1
  9. data/faulty.gemspec +3 -10
  10. data/lib/faulty.rb +149 -43
  11. data/lib/faulty/cache.rb +3 -1
  12. data/lib/faulty/cache/auto_wire.rb +65 -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 +1 -1
  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 +1 -1
  31. data/lib/faulty/storage.rb +4 -1
  32. data/lib/faulty/storage/auto_wire.rb +122 -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 +55 -60
  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 +13 -118
  41. data/lib/faulty/scope.rb +0 -117
@@ -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
@@ -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
@@ -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'
29
+ spec.add_development_dependency 'honeybadger', '>= 2.0'
32
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
@@ -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,106 @@ 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] storage
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`.
137
+ # @!attribute [r] listeners
138
+ # @see Events::ListenerInterface
139
+ # @return [Array] listeners Faulty event listeners
140
+ # @!attribute [r] notifier
141
+ # @return [Events::Notifier] A Faulty notifier. If given, listeners are
142
+ # ignored.
143
+ Options = Struct.new(
144
+ :cache,
145
+ :storage,
146
+ :listeners,
147
+ :notifier
148
+ ) do
149
+ include ImmutableOptions
150
+
151
+ private
152
+
153
+ def finalize
154
+ self.notifier ||= Events::Notifier.new(listeners || [])
155
+ self.storage = Storage::AutoWire.new(storage, notifier: notifier)
156
+ self.cache = Cache::AutoWire.new(cache, notifier: notifier)
157
+ end
158
+
159
+ def required
160
+ %i[cache storage notifier]
161
+ end
162
+
163
+ def defaults
164
+ {
165
+ listeners: [Events::LogListener.new]
166
+ }
167
+ end
168
+ end
169
+
170
+ # Create a new {Faulty} instance
171
+ #
172
+ # Note, the process of creating a new instance is not thread safe,
173
+ # so make sure instances are setup during your application's initialization
174
+ # phase.
175
+ #
176
+ # For the most part, {Faulty} instances are independent, however for some
177
+ # cache and storage backends, you will need to ensure that the cache keys
178
+ # and circuit names don't overlap between instances. For example, if using the
179
+ # {Storage::Redis} storage backend, you should specify different key
180
+ # prefixes for each instance.
181
+ #
182
+ # @see Options
183
+ # @param options [Hash] Attributes for {Options}
184
+ # @yield [Options] For setting options in a block
185
+ def initialize(**options, &block)
186
+ @circuits = Concurrent::Map.new
187
+ @options = Options.new(options, &block)
188
+ end
189
+
190
+ # Create or retrieve a circuit
191
+ #
192
+ # Within an instance, circuit instances have unique names, so if the given circuit
193
+ # name already exists, then the existing circuit will be returned, otherwise
194
+ # a new circuit will be created. If an existing circuit is returned, then
195
+ # the {options} param and block are ignored.
196
+ #
197
+ # @param name [String] The name of the circuit
198
+ # @param options [Hash] Attributes for {Circuit::Options}
199
+ # @yield [Circuit::Options] For setting options in a block
200
+ # @return [Circuit] The new circuit or the existing circuit if it already exists
201
+ def circuit(name, **options, &block)
202
+ name = name.to_s
203
+ options = options.merge(circuit_options)
204
+ @circuits.compute_if_absent(name) do
205
+ Circuit.new(name, **options, &block)
206
+ end
207
+ end
208
+
209
+ # Get a list of all circuit names
210
+ #
211
+ # @return [Array<String>] The circuit names
212
+ def list_circuits
213
+ options.storage.list
214
+ end
215
+
216
+ private
217
+
218
+ # Get circuit options from the {Faulty} options
219
+ #
220
+ # @return [Hash] The circuit options
221
+ def circuit_options
222
+ @options.to_h.select { |k, _v| %i[cache storage notifier].include?(k) }
223
+ end
118
224
  end
@@ -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,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
@@ -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