resilient 0.3.1 → 0.4.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fff95e3775a3225c6694d41256726d7cf7dd97ed
4
- data.tar.gz: 3a60f913516f625995d884e8b738941f76d1e839
3
+ metadata.gz: d58c0c8c5444d5e583d6667644d2309fd1d76c75
4
+ data.tar.gz: 92b729571e650001c424eac22b707eabb5f02806
5
5
  SHA512:
6
- metadata.gz: a87785935c77f251a9e9dcd738a4b51aea2d27f87504b30fb7d958a7d94de04a83502d0754a0c05a2c72494b68e7b10ef1f424f89678a434844a44d1aa215bbb
7
- data.tar.gz: 54c77250844604e9dd4ff3db089a422a9bca05a4a5779ebfd73b3645fb8484df12bed9c419b44aa6977e960e584b3eb26c2416bf979cb75a0d64f588c0f92be7
6
+ metadata.gz: de26fd92ca043468781a0cc18544d8904a37da9170c86f347141519e07e36304f005be212618f3dd909e379e3f694b638be480a483107a2ef9627440c70153d8
7
+ data.tar.gz: 149eba99b8f5d6e3310d0ee4933d4b3ea23cdbee945730a74f4d3189665a6432fda514b1a40a43e7eb51f89c1cc16bc0368642e8e37850ad26724c18be8cd151
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Resilient
2
2
 
3
- Some tools to aid in resiliency in ruby. For now, just a circuit breaker (~~stolen from~~ based on [hystrix](https://github.com/netflix/hystrix)). Soon much more...
3
+ Some tools to aid in resiliency in Ruby. For now, just a circuit breaker (~~stolen from~~ based on [hystrix](https://github.com/netflix/hystrix)). Soon much more...
4
4
 
5
5
  Nothing asynchronous or thread safe yet either, but open to it and would like to see more around it in the future.
6
6
 
@@ -25,8 +25,12 @@ Or install it yourself as:
25
25
  ```ruby
26
26
  require "resilient/circuit_breaker"
27
27
 
28
- # default properties for circuit
29
- circuit_breaker = Resilient::CircuitBreaker.new(key: Resilient::Key.new("example"))
28
+ # default properties for circuit, CircuitBreaker.get is used instead of
29
+ # CircuitBreaker.new as `get` keeps a registry of circuits by key to prevent
30
+ # creating multiple instances of the same circuit breaker for a key; not using
31
+ # `get` means you would have multiple instances of the circuit breaker and thus
32
+ # separate state and metrics; you can read more in examples/for_vs_new.rb
33
+ circuit_breaker = Resilient::CircuitBreaker.get("example")
30
34
  if circuit_breaker.allow_request?
31
35
  begin
32
36
  # do something expensive
@@ -43,7 +47,7 @@ end
43
47
  customize properties of circuit:
44
48
 
45
49
  ```ruby
46
- properties = Resilient::CircuitBreaker::Properties.new({
50
+ circuit_breaker = Resilient::CircuitBreaker.get("example", {
47
51
  # at what percentage of errors should we open the circuit
48
52
  error_threshold_percentage: 50,
49
53
  # do not try request again for 5 seconds
@@ -51,37 +55,41 @@ properties = Resilient::CircuitBreaker::Properties.new({
51
55
  # do not open circuit until at least 5 requests have happened
52
56
  request_volume_threshold: 5,
53
57
  })
54
- circuit_breaker = Resilient::CircuitBreaker.new(properties: properties, key: Resilient::Key.new("example"))
55
58
  # etc etc etc
56
59
  ```
57
60
 
58
61
  force the circuit to be always open:
59
62
 
60
63
  ```ruby
61
- properties = Resilient::CircuitBreaker::Properties.new(force_open: true)
62
- circuit_breaker = Resilient::CircuitBreaker.new(properties: properties, key: Resilient::Key.new("example"))
64
+ circuit_breaker = Resilient::CircuitBreaker.get("example", force_open: true)
63
65
  # etc etc etc
64
66
  ```
65
67
 
66
68
  force the circuit to be always closed (great way to test in production with no impact, all instrumentation still runs which means you can measure in production with config and gain confidence while never actually opening a circuit incorrectly):
67
69
 
68
70
  ```ruby
69
- properties = Resilient::CircuitBreaker::Properties.new(force_closed: true)
70
- circuit_breaker = Resilient::CircuitBreaker.new(properties: properties, key: Resilient::Key.new("example"))
71
+ circuit_breaker = Resilient::CircuitBreaker.get("example", force_closed: true)
71
72
  # etc etc etc
72
73
  ```
73
74
 
74
75
  customize rolling window to be 10 buckets of 1 second each (10 seconds in all):
75
76
 
76
77
  ```ruby
77
- metrics = Resilient::CircuitBreaker::Metrics.new({
78
+ circuit_breaker = Resilient::CircuitBreaker.get("example", {
78
79
  window_size_in_seconds: 10,
79
80
  bucket_size_in_seconds: 1,
80
81
  })
81
- circuit_breaker = Resilient::CircuitBreaker.new(metrics: metrics, key: Resilient::Key.new("example"))
82
82
  # etc etc etc
83
83
  ```
84
84
 
85
+ ## Tests
86
+
87
+ To ensure that you have a clean circuit for each test case, be sure to run the following in the setup for your tests (which resets each registered circuit breaker) either before every test case or at a minimum each test case that uses circuit breakers.
88
+
89
+ ```ruby
90
+ Resilient::CircuitBreaker.reset
91
+ ```
92
+
85
93
  ## Development
86
94
 
87
95
  ```bash
@@ -8,12 +8,11 @@ $:.unshift(lib_path)
8
8
  require "pp"
9
9
  require "resilient/circuit_breaker"
10
10
 
11
- properties = Resilient::CircuitBreaker::Properties.new({
11
+ circuit_breaker = Resilient::CircuitBreaker.get("example", {
12
12
  sleep_window_seconds: 1,
13
13
  request_volume_threshold: 10,
14
14
  error_threshold_percentage: 25,
15
15
  })
16
- circuit_breaker = Resilient::CircuitBreaker.new(key: Resilient::Key.new("example"), properties: properties)
17
16
 
18
17
  # success
19
18
  if circuit_breaker.allow_request?
@@ -0,0 +1,40 @@
1
+ # setting up load path
2
+ require "pathname"
3
+ root_path = Pathname(__FILE__).dirname.join("..").expand_path
4
+ lib_path = root_path.join("lib")
5
+ $:.unshift(lib_path)
6
+
7
+ # by default new is private so people don't use it, this makes it possible to
8
+ # use it as resilient checks for this env var prior to privatizing new
9
+ ENV["RESILIENT_PUBLICIZE_NEW"] = "1"
10
+
11
+ # requiring stuff for this example
12
+ require "pp"
13
+ require "resilient/circuit_breaker"
14
+
15
+ instance = Resilient::CircuitBreaker.get("example")
16
+ instance_using_get = Resilient::CircuitBreaker.get("example")
17
+ instance_using_new = Resilient::CircuitBreaker.new("example")
18
+
19
+ puts "instance equals instance_using_get: #{instance.equal?(instance_using_get)}"
20
+ puts "instance equals instance_using_new: #{instance.equal?(instance_using_new)}"
21
+
22
+ instance.properties.request_volume_threshold.times do
23
+ instance.failure
24
+ end
25
+
26
+ puts "instance allow_request?: #{instance.allow_request?}"
27
+ puts "instance_using_get allow_request?: #{instance_using_get.allow_request?}"
28
+
29
+ # this instance allows the request because it isn't sharing internal state and
30
+ # metrics due to being a new allocated instance; the for instance does not
31
+ # suffer this because it looks up instances in a registry rather than always
32
+ # generating a new instance even if you use the exact same key as it bypasses
33
+ # the registry
34
+ puts "instance_using_new allow_request?: #{instance_using_new.allow_request?}"
35
+
36
+ # instance equals instance_using_get: true
37
+ # instance equals instance_using_new: false
38
+ # instance allow_request?: false
39
+ # instance_using_get allow_request?: false
40
+ # instance_using_new allow_request?: true
@@ -8,14 +8,13 @@ $:.unshift(lib_path)
8
8
  require "pp"
9
9
  require "resilient/circuit_breaker"
10
10
 
11
- properties = Resilient::CircuitBreaker::Properties.new({
11
+ circuit_breaker = Resilient::CircuitBreaker.get("example", {
12
12
  sleep_window_seconds: 5,
13
13
  request_volume_threshold: 20,
14
14
  error_threshold_percentage: 10,
15
15
  window_size_in_seconds: 60,
16
16
  bucket_size_in_seconds: 1,
17
17
  })
18
- circuit_breaker = Resilient::CircuitBreaker.new(key: Resilient::Key.new("example"), properties: properties)
19
18
 
20
19
  iterations = 0
21
20
  loop do
@@ -1,24 +1,67 @@
1
1
  require "resilient/key"
2
2
  require "resilient/circuit_breaker/metrics"
3
3
  require "resilient/circuit_breaker/properties"
4
+ require "resilient/circuit_breaker/registry"
4
5
 
5
6
  module Resilient
6
7
  class CircuitBreaker
8
+ # Public: Resets all registered circuit breakers (and thus their state/metrics).
9
+ # Useful for ensuring a clean environment between tests. If you are using a
10
+ # registry other than the default, you will need to handle resets on your own.
11
+ def self.reset
12
+ Registry.default.reset
13
+ end
14
+
15
+ # Public: Returns an instance of circuit breaker based on key and registry.
16
+ # Default registry is used if none is provided. If key does not exist, it is
17
+ # registered. If key does exist, it returns registered instance instead of
18
+ # allocating a new instance in order to ensure that state/metrics are the
19
+ # same per key.
20
+ #
21
+ # See #initialize for docs on key, properties and metrics.
22
+ def self.get(key, properties = nil, metrics = nil, registry = nil)
23
+ key = Key.wrap(key)
24
+ (registry || Registry.default).fetch(key) {
25
+ new(key, properties, metrics)
26
+ }
27
+ end
28
+
29
+ unless ENV.key?("RESILIENT_PUBLICIZE_NEW")
30
+ class << self
31
+ private :new
32
+ private :allocate
33
+ end
34
+ end
35
+
7
36
  attr_reader :key
8
37
  attr_reader :open
9
38
  attr_reader :opened_or_last_checked_at_epoch
10
39
  attr_reader :metrics
11
40
  attr_reader :properties
12
41
 
13
- def initialize(key: nil, open: false, properties: nil, metrics: nil)
14
- # ruby 2.0 does not support required keyword arguments, this gets around that
42
+ # Private: Builds new instance of a CircuitBreaker.
43
+ #
44
+ # key - The String or Resilient::Key that determines uniqueness of the
45
+ # circuit breaker in the registry and for instrumentation.
46
+ #
47
+ # properties - The Hash or Resilient::CircuitBreaker::Properties that determine how the
48
+ # circuit breaker should behave. Optional. Defaults to new
49
+ # Resilient::CircuitBreaker::Properties instance.
50
+ #
51
+ # metrics - The object that stores successes and failures. Optional.
52
+ # Defaults to new Resilient::CircuitBreaker::Metrics instance
53
+ # based on window size and bucket size properties.
54
+ #
55
+ # Returns CircuitBreaker instance.
56
+ def initialize(key, properties = nil, metrics = nil)
15
57
  raise ArgumentError, "key argument is required" if key.nil?
16
- @key = key
17
- @open = open
58
+
59
+ @key = Key.wrap(key)
60
+ @open = false
18
61
  @opened_or_last_checked_at_epoch = 0
19
62
 
20
63
  @properties = if properties
21
- properties
64
+ Properties.wrap(properties)
22
65
  else
23
66
  Properties.new
24
67
  end
@@ -3,6 +3,20 @@ require "resilient/instrumenters/noop"
3
3
  module Resilient
4
4
  class CircuitBreaker
5
5
  class Properties
6
+
7
+ # Internal: Takes a string name or instance of a Key and always returns a
8
+ # Key instance.
9
+ def self.wrap(hash_or_instance)
10
+ case hash_or_instance
11
+ when self
12
+ hash_or_instance
13
+ when Hash
14
+ new(hash_or_instance)
15
+ else
16
+ raise TypeError, "properties must be Hash or Resilient::Properties instance"
17
+ end
18
+ end
19
+
6
20
  # allows forcing the circuit open (stopping all requests)
7
21
  attr_reader :force_open
8
22
 
@@ -0,0 +1,46 @@
1
+ module Resilient
2
+ class CircuitBreaker
3
+ class Registry
4
+ # Internal: Default registry to use for circuit breakers.
5
+ def self.default
6
+ @default
7
+ end
8
+
9
+ # Internal: Allows overriding default registry for circuit breakers.
10
+ def self.default=(value)
11
+ @default = value
12
+ end
13
+
14
+ def initialize(source = nil)
15
+ @source = source || {}
16
+ end
17
+
18
+ # Setup default to new instance.
19
+ @default = new
20
+
21
+ # Internal: To be used by CircuitBreaker to either get an instance for a
22
+ # key or set a new instance for a key.
23
+ #
24
+ # Raises KeyError if key not found and no block provided.
25
+ def fetch(key, &block)
26
+ if value = @source[key]
27
+ value
28
+ else
29
+ if block_given?
30
+ @source[key] = yield
31
+ else
32
+ @source.fetch(key)
33
+ end
34
+ end
35
+ end
36
+
37
+ # Internal: To be used by CircuitBreaker to reset the stored circuit
38
+ # breakers, which should only really be used for cleaning up in
39
+ # test environment.
40
+ def reset
41
+ @source.each_value(&:reset)
42
+ nil
43
+ end
44
+ end
45
+ end
46
+ end
@@ -8,7 +8,7 @@ module Resilient
8
8
  attr_reader :events
9
9
 
10
10
  def initialize
11
- @events = []
11
+ reset
12
12
  end
13
13
 
14
14
  def instrument(name, payload = {})
@@ -25,6 +25,10 @@ module Resilient
25
25
  @events << Event.new(name, payload, result)
26
26
  result
27
27
  end
28
+
29
+ def reset
30
+ @events = []
31
+ end
28
32
  end
29
33
  end
30
34
  end
@@ -1,9 +1,31 @@
1
1
  module Resilient
2
2
  class Key
3
+
4
+ # Internal: Takes a string name or instance of a Key and always returns a
5
+ # Key instance.
6
+ def self.wrap(string_or_instance)
7
+ case string_or_instance
8
+ when self, NilClass
9
+ string_or_instance
10
+ else
11
+ new(string_or_instance)
12
+ end
13
+ end
14
+
3
15
  attr_reader :name
4
16
 
5
17
  def initialize(name)
18
+ raise TypeError, "name must be a String" unless name.is_a?(String)
6
19
  @name = name
7
20
  end
21
+
22
+ def ==(other)
23
+ self.class == other.class && name == other.name
24
+ end
25
+ alias_method :eql?, :==
26
+
27
+ def hash
28
+ @name.hash
29
+ end
8
30
  end
9
31
  end
@@ -0,0 +1,42 @@
1
+ module Resilient
2
+ class Test
3
+ module CircuitBreakerRegistryInterface
4
+ def test_responds_to_fetch
5
+ assert_respond_to @object, :fetch
6
+ end
7
+
8
+ def test_responds_to_reset
9
+ assert_respond_to @object, :reset
10
+ end
11
+
12
+ def test_fetch
13
+ key = "foo"
14
+ value = "bar".freeze
15
+
16
+ assert_raises(KeyError) { @object.fetch(key) }
17
+ assert_equal value, @object.fetch(key) { value }
18
+ assert_equal value, @object.fetch(key)
19
+ assert @object.fetch(key).equal?(value)
20
+ end
21
+
22
+ def test_reset
23
+ bar_value = Minitest::Mock.new
24
+ wick_value = Minitest::Mock.new
25
+ bar_value.expect :reset, nil, []
26
+ wick_value.expect :reset, nil, []
27
+
28
+ @object.fetch("foo") { bar_value }
29
+ @object.fetch("baz") { wick_value }
30
+
31
+ assert_nil @object.reset
32
+
33
+ bar_value.verify
34
+ wick_value.verify
35
+ end
36
+
37
+ def test_reset_empty_registry
38
+ assert_nil @object.reset
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,3 +1,3 @@
1
1
  module Resilient
2
- VERSION = "0.3.1"
2
+ VERSION = "0.4.0.beta1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resilient
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-01-04 00:00:00.000000000 Z
11
+ date: 2016-04-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,6 +66,7 @@ files:
66
66
  - LICENSE.txt
67
67
  - README.md
68
68
  - examples/basic.rb
69
+ - examples/get_vs_new.rb
69
70
  - examples/long_running.rb
70
71
  - lib/resilient.rb
71
72
  - lib/resilient/circuit_breaker.rb
@@ -76,10 +77,12 @@ files:
76
77
  - lib/resilient/circuit_breaker/metrics/storage/memory.rb
77
78
  - lib/resilient/circuit_breaker/metrics/window_size.rb
78
79
  - lib/resilient/circuit_breaker/properties.rb
80
+ - lib/resilient/circuit_breaker/registry.rb
79
81
  - lib/resilient/instrumenters/memory.rb
80
82
  - lib/resilient/instrumenters/noop.rb
81
83
  - lib/resilient/key.rb
82
84
  - lib/resilient/test/circuit_breaker_interface.rb
85
+ - lib/resilient/test/circuit_breaker_registry_interface.rb
83
86
  - lib/resilient/test/metrics_interface.rb
84
87
  - lib/resilient/test/metrics_storage_interface.rb
85
88
  - lib/resilient/test/properties_interface.rb
@@ -105,12 +108,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
105
108
  version: '0'
106
109
  required_rubygems_version: !ruby/object:Gem::Requirement
107
110
  requirements:
108
- - - ">="
111
+ - - ">"
109
112
  - !ruby/object:Gem::Version
110
- version: '0'
113
+ version: 1.3.1
111
114
  requirements: []
112
115
  rubyforge_project:
113
- rubygems_version: 2.2.2
116
+ rubygems_version: 2.4.5.1
114
117
  signing_key:
115
118
  specification_version: 4
116
119
  summary: toolkit for resilient ruby apps