faulty 0.1.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 +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.rubocop.yml +85 -0
- data/.travis.yml +44 -0
- data/.yardopts +3 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +20 -0
- data/README.md +559 -0
- data/bin/check-version +10 -0
- data/bin/console +12 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/bin/yard +29 -0
- data/bin/yardoc +29 -0
- data/bin/yri +29 -0
- data/faulty.gemspec +43 -0
- data/lib/faulty.rb +118 -0
- data/lib/faulty/cache.rb +13 -0
- data/lib/faulty/cache/default.rb +48 -0
- data/lib/faulty/cache/fault_tolerant_proxy.rb +74 -0
- data/lib/faulty/cache/interface.rb +44 -0
- data/lib/faulty/cache/mock.rb +39 -0
- data/lib/faulty/cache/null.rb +23 -0
- data/lib/faulty/cache/rails.rb +37 -0
- data/lib/faulty/circuit.rb +436 -0
- data/lib/faulty/error.rb +66 -0
- data/lib/faulty/events.rb +25 -0
- data/lib/faulty/events/callback_listener.rb +42 -0
- data/lib/faulty/events/listener_interface.rb +18 -0
- data/lib/faulty/events/log_listener.rb +88 -0
- data/lib/faulty/events/notifier.rb +25 -0
- data/lib/faulty/immutable_options.rb +40 -0
- data/lib/faulty/result.rb +150 -0
- data/lib/faulty/scope.rb +117 -0
- data/lib/faulty/status.rb +165 -0
- data/lib/faulty/storage.rb +11 -0
- data/lib/faulty/storage/fault_tolerant_proxy.rb +178 -0
- data/lib/faulty/storage/interface.rb +161 -0
- data/lib/faulty/storage/memory.rb +195 -0
- data/lib/faulty/storage/redis.rb +335 -0
- data/lib/faulty/version.rb +8 -0
- metadata +306 -0
data/bin/check-version
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env sh
|
2
|
+
|
3
|
+
set -e
|
4
|
+
|
5
|
+
tag="$(git describe --abbrev=0 2>/dev/null || echo)"
|
6
|
+
tag="${tag#v}"
|
7
|
+
[ "$tag" = '' ] && exit 0
|
8
|
+
|
9
|
+
tag_gt_version=$(ruby -r ./lib/faulty/version -e "puts Faulty.version >= Gem::Version.new('${tag}')")
|
10
|
+
test "$tag_gt_version" = true
|
data/bin/console
ADDED
data/bin/rspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rspec' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'pathname'
|
12
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path('bundle', __dir__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'rubygems'
|
27
|
+
require 'bundler/setup'
|
28
|
+
|
29
|
+
load Gem.bin_path('rspec-core', 'rspec')
|
data/bin/rubocop
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rubocop' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'pathname'
|
12
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path('bundle', __dir__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'rubygems'
|
27
|
+
require 'bundler/setup'
|
28
|
+
|
29
|
+
load Gem.bin_path('rubocop', 'rubocop')
|
data/bin/yard
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'yard' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'pathname'
|
12
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path('bundle', __dir__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'rubygems'
|
27
|
+
require 'bundler/setup'
|
28
|
+
|
29
|
+
load Gem.bin_path('yard', 'yard')
|
data/bin/yardoc
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'yardoc' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'pathname'
|
12
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path('bundle', __dir__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'rubygems'
|
27
|
+
require 'bundler/setup'
|
28
|
+
|
29
|
+
load Gem.bin_path('yard', 'yardoc')
|
data/bin/yri
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'yri' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'pathname'
|
12
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path('bundle', __dir__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'rubygems'
|
27
|
+
require 'bundler/setup'
|
28
|
+
|
29
|
+
load Gem.bin_path('yard', 'yri')
|
data/faulty.gemspec
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'faulty/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'faulty'
|
9
|
+
spec.version = Faulty.version
|
10
|
+
spec.authors = ['Justin Howard']
|
11
|
+
spec.email = ['jmhoward0@gmail.com']
|
12
|
+
spec.licenses = ['MIT']
|
13
|
+
|
14
|
+
spec.summary = 'Fault-tolerance tools for ruby based on circuit-breakers'
|
15
|
+
spec.homepage = 'https://github.com/ParentSquare/faulty'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`
|
18
|
+
.split("\x0")
|
19
|
+
.reject { |f| f.match(%r{^spec/}) }
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.required_ruby_version = '>= 2.3'
|
23
|
+
|
24
|
+
spec.add_runtime_dependency 'concurrent-ruby', '~> 1.0'
|
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'
|
29
|
+
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'
|
33
|
+
spec.add_development_dependency 'rspec', '~> 3.8'
|
34
|
+
spec.add_development_dependency 'rspec_junit_formatter', '~> 0.4'
|
35
|
+
# 0.81 is the last rubocop version with Ruby 2.3 support
|
36
|
+
spec.add_development_dependency 'rubocop', '0.81.0'
|
37
|
+
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
|
+
spec.add_development_dependency 'timecop', '>= 0.9'
|
42
|
+
spec.add_development_dependency 'yard', '~> 0.9.25'
|
43
|
+
end
|
data/lib/faulty.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
require 'concurrent-ruby'
|
5
|
+
|
6
|
+
require 'faulty/immutable_options'
|
7
|
+
require 'faulty/cache'
|
8
|
+
require 'faulty/circuit'
|
9
|
+
require 'faulty/error'
|
10
|
+
require 'faulty/events'
|
11
|
+
require 'faulty/result'
|
12
|
+
require 'faulty/scope'
|
13
|
+
require 'faulty/status'
|
14
|
+
require 'faulty/storage'
|
15
|
+
|
16
|
+
# The top-level namespace for Faulty
|
17
|
+
#
|
18
|
+
# Fault-tolerance tools for ruby based on circuit-breakers
|
19
|
+
module Faulty
|
20
|
+
class << self
|
21
|
+
# Start the Faulty environment
|
22
|
+
#
|
23
|
+
# This creates a global shared Faulty state for configuration and for
|
24
|
+
# re-using State objects.
|
25
|
+
#
|
26
|
+
# Not thread safe, should be executed before any worker threads
|
27
|
+
# are spawned.
|
28
|
+
#
|
29
|
+
# If you prefer dependency-injection instead of global state, you can skip
|
30
|
+
# init and pass a {Scope} directly to your dependencies.
|
31
|
+
#
|
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
|
+
# @return [self]
|
37
|
+
def init(scope_name = :default, **config, &block)
|
38
|
+
raise AlreadyInitializedError if @scopes
|
39
|
+
|
40
|
+
@default_scope = scope_name
|
41
|
+
@scopes = Concurrent::Map.new
|
42
|
+
register(scope_name, Scope.new(**config, &block)) unless scope_name.nil?
|
43
|
+
self
|
44
|
+
rescue StandardError
|
45
|
+
@scopes = nil
|
46
|
+
raise
|
47
|
+
end
|
48
|
+
|
49
|
+
# Get the default scope given during {.init}
|
50
|
+
#
|
51
|
+
# @return [Scope, nil] The default scope if it is registered
|
52
|
+
def default
|
53
|
+
raise UninitializedError unless @scopes
|
54
|
+
raise MissingDefaultScopeError unless @default_scope
|
55
|
+
|
56
|
+
self[@default_scope]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Get a scope by name
|
60
|
+
#
|
61
|
+
# @return [Scope, nil] The named scope if it is registered
|
62
|
+
def [](scope_name)
|
63
|
+
raise UninitializedError unless @scopes
|
64
|
+
|
65
|
+
@scopes[scope_name]
|
66
|
+
end
|
67
|
+
|
68
|
+
# Register a scope to the global Faulty state
|
69
|
+
#
|
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.
|
72
|
+
#
|
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
|
76
|
+
# it already existed, otherwise nil.
|
77
|
+
def register(name, scope)
|
78
|
+
raise UninitializedError unless @scopes
|
79
|
+
|
80
|
+
@scopes.put_if_absent(name, scope)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Get the options for the default scope
|
84
|
+
#
|
85
|
+
# @raise MissingDefaultScopeError If the default scope has not been created
|
86
|
+
# @return [Scope::Options]
|
87
|
+
def options
|
88
|
+
default.options
|
89
|
+
end
|
90
|
+
|
91
|
+
# Get or create a circuit for the default scope
|
92
|
+
#
|
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
|
+
def circuit(name, **config, &block)
|
98
|
+
default.circuit(name, **config, &block)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Get a list of all circuit names for the default scope
|
102
|
+
#
|
103
|
+
# @return [Array<String>] The circuit names
|
104
|
+
def list_circuits
|
105
|
+
options.storage.list
|
106
|
+
end
|
107
|
+
|
108
|
+
# The current time
|
109
|
+
#
|
110
|
+
# Used by Faulty wherever the current time is needed. Can be overridden
|
111
|
+
# for testing
|
112
|
+
#
|
113
|
+
# @return [Time] The current time
|
114
|
+
def current_time
|
115
|
+
Time.now.to_i
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
data/lib/faulty/cache.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Faulty
|
4
|
+
# The namespace for Faulty caching
|
5
|
+
module Cache
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'faulty/cache/default'
|
10
|
+
require 'faulty/cache/fault_tolerant_proxy'
|
11
|
+
require 'faulty/cache/mock'
|
12
|
+
require 'faulty/cache/null'
|
13
|
+
require 'faulty/cache/rails'
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Faulty
|
4
|
+
module Cache
|
5
|
+
# The default cache implementation
|
6
|
+
#
|
7
|
+
# It tries to make a logical decision of what cache implementation to use
|
8
|
+
# based on the current environment.
|
9
|
+
#
|
10
|
+
# - If Rails is loaded, it will use Rails.cache
|
11
|
+
# - If ActiveSupport is available, it will use an `ActiveSupport::Cache::MemoryStore`
|
12
|
+
# - Otherwise it will use a {Faulty::Cache::Null}
|
13
|
+
class Default
|
14
|
+
def initialize
|
15
|
+
@cache = if defined?(::Rails)
|
16
|
+
Cache::Rails.new(::Rails.cache)
|
17
|
+
elsif defined?(::ActiveSupport::Cache::MemoryStore)
|
18
|
+
Cache::Rails.new(ActiveSupport::Cache::MemoryStore.new, fault_tolerant: true)
|
19
|
+
else
|
20
|
+
Cache::Null.new
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Read from the internal cache by key
|
25
|
+
#
|
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
|
33
|
+
#
|
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
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Faulty
|
4
|
+
module Cache
|
5
|
+
# A wrapper for cache backends that may raise errors
|
6
|
+
#
|
7
|
+
# {Scope} automatically wraps all non-fault-tolerant cache backends with
|
8
|
+
# this class.
|
9
|
+
#
|
10
|
+
# If the cache backend raises a `StandardError`, it will be captured and
|
11
|
+
# sent to the notifier.
|
12
|
+
class FaultTolerantProxy
|
13
|
+
attr_reader :options
|
14
|
+
|
15
|
+
# Options for {FaultTolerantProxy}
|
16
|
+
#
|
17
|
+
# @!attribute [r] notifier
|
18
|
+
# @return [Events::Notifier] A Faulty notifier
|
19
|
+
Options = Struct.new(
|
20
|
+
:notifier
|
21
|
+
) do
|
22
|
+
include ImmutableOptions
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def required
|
27
|
+
%i[notifier]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param cache [Cache::Interface] The cache backend to wrap
|
32
|
+
# @param options [Hash] Attributes for {Options}
|
33
|
+
# @yield [Options] For setting options in a block
|
34
|
+
def initialize(cache, **options, &block)
|
35
|
+
@cache = cache
|
36
|
+
@options = Options.new(options, &block)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Read from the cache safely
|
40
|
+
#
|
41
|
+
# If the backend raises a `StandardError`, this will return `nil`.
|
42
|
+
#
|
43
|
+
# @param (see Cache::Interface#read)
|
44
|
+
# @return [Object, nil] The value if found, or nil if not found or if an
|
45
|
+
# error was raised.
|
46
|
+
def read(key)
|
47
|
+
@cache.read(key)
|
48
|
+
rescue StandardError => e
|
49
|
+
options.notifier.notify(:cache_failure, key: key, action: :read, error: e)
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
# Write to the cache safely
|
54
|
+
#
|
55
|
+
# If the backend raises a `StandardError`, the write will be ignored
|
56
|
+
#
|
57
|
+
# @param (see Cache::Interface#write)
|
58
|
+
# @return [void]
|
59
|
+
def write(key, value, expires_in: nil)
|
60
|
+
@cache.write(key, value, expires_in: expires_in)
|
61
|
+
rescue StandardError
|
62
|
+
options.notifier.notify(:cache_failure, key: key, action: :write, error: e)
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
# This cache makes any cache fault tolerant, so this is always `true`
|
67
|
+
#
|
68
|
+
# @return [true]
|
69
|
+
def fault_tolerant?
|
70
|
+
true
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|