faulty 0.1.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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