gracefully 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/crowdworks/gracefully.svg?branch=master)](https://travis-ci.org/crowdworks/gracefully)
|
4
|
+
[![Coverage Status](https://coveralls.io/repos/crowdworks/gracefully/badge.png?branch=master)](https://coveralls.io/r/crowdworks/gracefully?branch=master)
|
5
|
+
[![Code Climate](https://codeclimate.com/github/crowdworks/gracefully/badges/gpa.svg)](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
|