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