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.
@@ -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