faulty 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'faulty'
6
+ require 'byebug'
7
+ require 'irb'
8
+
9
+ # For default cache support
10
+ require 'active_support'
11
+
12
+ IRB.start
@@ -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')
@@ -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')
@@ -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')
@@ -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')
@@ -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
@@ -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
@@ -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