gracefully 0.0.1 → 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 +4 -4
- data/.travis.yml +30 -0
- data/Gemfile +2 -0
- data/README.md +34 -1
- data/lib/gracefully.rb +22 -4
- data/lib/gracefully/all.rb +6 -0
- data/lib/gracefully/circuit_breaker.rb +88 -0
- data/lib/gracefully/command.rb +23 -0
- data/lib/gracefully/command_disabled_error.rb +6 -0
- data/lib/gracefully/consecutive_failures_based_health.rb +75 -0
- data/lib/gracefully/counter.rb +29 -0
- data/lib/gracefully/degradable.rb +36 -0
- data/lib/gracefully/{feature.rb → degradable_command.rb} +4 -3
- data/lib/gracefully/degradable_command_builder.rb +23 -0
- data/lib/gracefully/error.rb +37 -0
- data/lib/gracefully/health.rb +29 -0
- data/lib/gracefully/mutex_based_synchronized_counter.rb +30 -0
- data/lib/gracefully/retried_command.rb +25 -0
- data/lib/gracefully/short_circuited_command.rb +23 -0
- data/lib/gracefully/timed_command.rb +19 -0
- data/lib/gracefully/togglable_command.rb +20 -0
- data/lib/gracefully/try.rb +6 -34
- data/lib/gracefully/version.rb +1 -1
- data/spec/circuit_breaker_spec.rb +142 -0
- data/spec/command_spec.rb +34 -0
- data/spec/consecutive_failures_based_health_spec.rb +78 -0
- data/spec/degradable_spec.rb +49 -0
- data/spec/gracefully_spec.rb +136 -10
- data/spec/mutex_based_synchronized_counter_spec.rb +50 -0
- data/spec/retried_command_spec.rb +93 -0
- data/spec/short_circuited_command_spec.rb +136 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/timecop_helper.rb +3 -0
- data/spec/timed_command_spec.rb +73 -0
- data/spec/togglable_command_spec.rb +31 -0
- metadata +39 -5
- data/lib/gracefully/feature_builder.rb +0 -24
- data/lib/gracefully/health_meter.rb +0 -90
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cf954108bc3f1509923aeb64fff8704ce06d3023
|
4
|
+
data.tar.gz: 157d3f6c00fdcc349a93a1fb9893e9a8dae2ed7a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f461e469d24b71323e076f80e3b396c87d1cd36805a04b0668cad600af2e11da25b1e44ecb22d4ed8346e724ef88fefcc03a7390b5793b08321a9cfb859cc6e9
|
7
|
+
data.tar.gz: 07f36379d86f0d0ef94bd4c38abf7c1e962426b59d49646447294d765e2f46b61797d1a8f9e9494ee15f24813251476b0c07737c6eeed79a92b60dee14703152
|
data/.travis.yml
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
bundler_args: --without development
|
2
|
+
env:
|
3
|
+
global:
|
4
|
+
- JRUBY_OPTS="$JRUBY_OPTS --debug"
|
5
|
+
language: ruby
|
6
|
+
rvm:
|
7
|
+
- 1.9.3
|
8
|
+
- 2.1.2
|
9
|
+
- 2.1.3
|
10
|
+
- jruby-19mode
|
11
|
+
- ruby-head
|
12
|
+
jdk:
|
13
|
+
- openjdk7
|
14
|
+
- oraclejdk7
|
15
|
+
matrix:
|
16
|
+
exclude:
|
17
|
+
- rvm: 1.9.3
|
18
|
+
jdk: openjdk7
|
19
|
+
- rvm: 2.1.2
|
20
|
+
jdk: openjdk7
|
21
|
+
- rvm: 2.1.3
|
22
|
+
jdk: openjdk7
|
23
|
+
- rvm: ruby-head
|
24
|
+
jdk: openjdk7
|
25
|
+
allow_failures:
|
26
|
+
- rvm: ruby-head
|
27
|
+
fast_finish: true
|
28
|
+
sudo: false
|
29
|
+
before_install: gem install bundler
|
30
|
+
script: bundle exec rspec
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# Gracefully
|
2
2
|
|
3
|
+
[](https://travis-ci.org/crowdworks/gracefully)
|
4
|
+
[](https://coveralls.io/r/crowdworks/gracefully?branch=master)
|
5
|
+
[](https://codeclimate.com/github/crowdworks/gracefully)
|
6
|
+
|
3
7
|
ensures features gracefully degrade based on error rate or turnaround time.
|
4
8
|
|
5
9
|
## Installation
|
@@ -20,7 +24,36 @@ Or install it yourself as:
|
|
20
24
|
|
21
25
|
## Usage
|
22
26
|
|
23
|
-
|
27
|
+
Set up one instance per feature which is gracefully degradable.
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
the_feature = Gracefully.
|
31
|
+
degradable_command(retries: 0, allowed_failures: 1) do |a|
|
32
|
+
if rand < 0.5
|
33
|
+
'foo'
|
34
|
+
else
|
35
|
+
raise 'err1'
|
36
|
+
end
|
37
|
+
end.
|
38
|
+
fallback_to(retries: 2) do |a|
|
39
|
+
if rand < 0.5
|
40
|
+
'bar'
|
41
|
+
else
|
42
|
+
raise 'err2'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
10.times.map do
|
47
|
+
begin
|
48
|
+
the_feature.call
|
49
|
+
rescue => e
|
50
|
+
e.message
|
51
|
+
end
|
52
|
+
end
|
53
|
+
#=> ["bar", "bar", "bar", "bar", "bar", "bar", "bar", "bar", "bar", "Tried to get the value of a failure"]
|
54
|
+
```
|
55
|
+
|
56
|
+
See `spec/gracefully_spec.rb` for more usages.
|
24
57
|
|
25
58
|
## Contributing
|
26
59
|
|
data/lib/gracefully.rb
CHANGED
@@ -1,10 +1,28 @@
|
|
1
1
|
require "gracefully/version"
|
2
|
-
require "gracefully/
|
3
|
-
require "gracefully/
|
2
|
+
require "gracefully/degradable_command"
|
3
|
+
require "gracefully/degradable_command_builder"
|
4
4
|
require "gracefully/try"
|
5
5
|
|
6
6
|
module Gracefully
|
7
|
-
def self.
|
8
|
-
|
7
|
+
def self.degradable_command(*args, &block)
|
8
|
+
DegradableCommandBuilder.new.usually(*args, &block)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.command(*args, &block)
|
12
|
+
callable, options = Command.normalize_arguments(*args, &block)
|
13
|
+
|
14
|
+
options ||= {}
|
15
|
+
|
16
|
+
if options[:timeout]
|
17
|
+
command(TimedCommand.new(callable, options), options.dup.tap { |h| h.delete(:timeout) })
|
18
|
+
elsif options[:retries]
|
19
|
+
command(RetriedCommand.new(callable, options), options.dup.tap { |h| h.delete(:retries) })
|
20
|
+
elsif options[:allowed_failures]
|
21
|
+
command(ShortCircuitedCommand.new(callable, options), options.dup.tap { |h| h.delete(:allowed_failures) })
|
22
|
+
elsif options[:run_only_if]
|
23
|
+
TogglableCommand.new(callable, options)
|
24
|
+
else
|
25
|
+
Command.new(callable, options)
|
26
|
+
end
|
9
27
|
end
|
10
28
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Gracefully
|
2
|
+
class CircuitBreaker
|
3
|
+
attr_reader :opened_date
|
4
|
+
|
5
|
+
def initialize(*args)
|
6
|
+
if args.size > 0
|
7
|
+
options = args.first
|
8
|
+
|
9
|
+
@try_close_after = options[:try_close_after]
|
10
|
+
end
|
11
|
+
|
12
|
+
@closed = true
|
13
|
+
@health = options && options[:health] || Gracefully::ConsecutiveFailuresBasedHealth.new(become_unhealthy_after_consecutive_failures: 0)
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute(&block)
|
17
|
+
if open? && (@try_close_after.nil? || try_close_period_passed?.!)
|
18
|
+
raise CurrentlyOpenError, "Opened at #{opened_date}"
|
19
|
+
end
|
20
|
+
|
21
|
+
clear_opened_date!
|
22
|
+
|
23
|
+
begin
|
24
|
+
v = block.call
|
25
|
+
mark_success
|
26
|
+
v
|
27
|
+
rescue => e
|
28
|
+
mark_failure
|
29
|
+
raise e
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def mark_success
|
34
|
+
@health.mark_success
|
35
|
+
|
36
|
+
update!
|
37
|
+
end
|
38
|
+
|
39
|
+
def mark_failure
|
40
|
+
@health.mark_failure
|
41
|
+
|
42
|
+
update!
|
43
|
+
end
|
44
|
+
|
45
|
+
def open?
|
46
|
+
closed?.!
|
47
|
+
end
|
48
|
+
|
49
|
+
def closed?
|
50
|
+
@closed
|
51
|
+
end
|
52
|
+
|
53
|
+
def try_close_period_passed?
|
54
|
+
opened_date && opened_date + @try_close_after < Time.now
|
55
|
+
end
|
56
|
+
|
57
|
+
def opened_date
|
58
|
+
@opened_date
|
59
|
+
end
|
60
|
+
|
61
|
+
def update!
|
62
|
+
if @health.healthy?
|
63
|
+
close!
|
64
|
+
else
|
65
|
+
open!
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def close!
|
70
|
+
@closed = true
|
71
|
+
end
|
72
|
+
|
73
|
+
def open!
|
74
|
+
@closed = false
|
75
|
+
@opened_date = Time.now
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def clear_opened_date!
|
81
|
+
@opened_date = nil
|
82
|
+
end
|
83
|
+
|
84
|
+
class CurrentlyOpenError < StandardError
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Gracefully
|
2
|
+
class Command
|
3
|
+
def initialize(*args, &block)
|
4
|
+
@callable, @options = Command.normalize_arguments(*args, &block)
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.normalize_arguments(*args, &block)
|
8
|
+
if args.size == 0
|
9
|
+
[block, {}]
|
10
|
+
elsif args.size == 1
|
11
|
+
[block, args.first]
|
12
|
+
elsif args.size == 2
|
13
|
+
args
|
14
|
+
else
|
15
|
+
raise "Invalid number of arguments: #{args.size}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(*args, &block)
|
20
|
+
@callable.call *args, &block
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require_relative 'health'
|
2
|
+
require_relative 'counter'
|
3
|
+
|
4
|
+
module Gracefully
|
5
|
+
class ConsecutiveFailuresBasedHealth < Health
|
6
|
+
# @param [Hash] args
|
7
|
+
def initialize(args)
|
8
|
+
@healthy_count = 0
|
9
|
+
@unhealthy_count = 0
|
10
|
+
conf = Configuration.new(args)
|
11
|
+
super(state: Healthy.new(conf))
|
12
|
+
end
|
13
|
+
|
14
|
+
class Configuration
|
15
|
+
attr_reader :become_unhealthy_after_consecutive_failures
|
16
|
+
|
17
|
+
def initialize(args)
|
18
|
+
@become_unhealthy_after_consecutive_failures = args[:become_unhealthy_after_consecutive_failures]
|
19
|
+
@counter = args[:counter] || -> { SingletonInMemoryCounter.instance }
|
20
|
+
end
|
21
|
+
|
22
|
+
def counter
|
23
|
+
@counter.call
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Health < Gracefully::Health::State
|
28
|
+
end
|
29
|
+
|
30
|
+
class Healthy < State
|
31
|
+
# @param [Configuration] conf
|
32
|
+
def initialize(conf)
|
33
|
+
@failure_counter = conf.counter
|
34
|
+
@configuration = conf
|
35
|
+
end
|
36
|
+
|
37
|
+
def mark_success
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def mark_failure
|
42
|
+
@failure_counter.increment!
|
43
|
+
if @failure_counter.count <= @configuration.become_unhealthy_after_consecutive_failures
|
44
|
+
self
|
45
|
+
else
|
46
|
+
@failure_counter.reset!
|
47
|
+
Unhealthy.new @configuration
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def healthy?
|
52
|
+
true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Unhealthy < State
|
57
|
+
# @param [Configuration] conf
|
58
|
+
def initialize(conf)
|
59
|
+
@configuration = conf
|
60
|
+
end
|
61
|
+
|
62
|
+
def mark_success
|
63
|
+
Healthy.new @configuration
|
64
|
+
end
|
65
|
+
|
66
|
+
def mark_failure
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
def healthy?
|
71
|
+
false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Gracefully
|
2
|
+
class Counter
|
3
|
+
|
4
|
+
end
|
5
|
+
|
6
|
+
class SingletonInMemoryCounter
|
7
|
+
def self.instance
|
8
|
+
@instance ||= InMemoryCounter.new
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class InMemoryCounter < Counter
|
13
|
+
def initialize
|
14
|
+
@count = 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def reset!
|
18
|
+
@count = 0
|
19
|
+
end
|
20
|
+
|
21
|
+
def increment!
|
22
|
+
@count += 1
|
23
|
+
end
|
24
|
+
|
25
|
+
def count
|
26
|
+
@count
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'gracefully'
|
2
|
+
|
3
|
+
module Gracefully
|
4
|
+
module Degradable
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
def __call_gracefully_degradable_method__(method, *args, &block)
|
10
|
+
self.class.instance_variable_get(:@__gracefully_degradable_methods__)[method].call(self, *args, &block)
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def gracefully_degrade(method, options)
|
15
|
+
@__gracefully_degradable_methods__ ||= {}
|
16
|
+
|
17
|
+
fallback_method, fallback_options = options[:fallback].first
|
18
|
+
fallback_options ||= {}
|
19
|
+
|
20
|
+
original_method = "#{method}_without_graceful_degradation"
|
21
|
+
|
22
|
+
@__gracefully_degradable_methods__[method] =
|
23
|
+
Gracefully.degradable_command(options) { |instance, *args, &block|
|
24
|
+
instance.__send__(original_method, *args, &block)
|
25
|
+
}.fallback_to(fallback_options) { |instance, *args, &block|
|
26
|
+
instance.__send__(fallback_method, *args, &block)
|
27
|
+
}
|
28
|
+
|
29
|
+
alias_method original_method, method
|
30
|
+
define_method method do |*args, &block|
|
31
|
+
__call_gracefully_degradable_method__(method, *args, &block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -1,13 +1,14 @@
|
|
1
1
|
module Gracefully
|
2
|
-
class
|
2
|
+
class DegradableCommand
|
3
3
|
def initialize(args)
|
4
|
-
@name = args[:name]
|
5
4
|
@usually = args[:usually]
|
6
5
|
@fallback_to = args[:fallback_to]
|
7
6
|
end
|
8
7
|
|
9
8
|
def call(*args)
|
10
|
-
Try.to { @usually.call *args }.
|
9
|
+
Try.to { @usually.call *args }.
|
10
|
+
or_else(Try.to { @fallback_to.call *args }).
|
11
|
+
get
|
11
12
|
end
|
12
13
|
end
|
13
14
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Gracefully
|
2
|
+
class DegradableCommandBuilder
|
3
|
+
def initialize
|
4
|
+
end
|
5
|
+
|
6
|
+
def usually(*args, &block)
|
7
|
+
@usually = Gracefully.command(*args, &block)
|
8
|
+
self
|
9
|
+
end
|
10
|
+
|
11
|
+
def fallback_to(*args, &block)
|
12
|
+
@fallback_to = Gracefully.command(*args, &block)
|
13
|
+
|
14
|
+
build
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def build
|
20
|
+
DegradableCommand.new(usually: @usually, fallback_to: @fallback_to)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|