gracefully 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +30 -0
  3. data/Gemfile +2 -0
  4. data/README.md +34 -1
  5. data/lib/gracefully.rb +22 -4
  6. data/lib/gracefully/all.rb +6 -0
  7. data/lib/gracefully/circuit_breaker.rb +88 -0
  8. data/lib/gracefully/command.rb +23 -0
  9. data/lib/gracefully/command_disabled_error.rb +6 -0
  10. data/lib/gracefully/consecutive_failures_based_health.rb +75 -0
  11. data/lib/gracefully/counter.rb +29 -0
  12. data/lib/gracefully/degradable.rb +36 -0
  13. data/lib/gracefully/{feature.rb → degradable_command.rb} +4 -3
  14. data/lib/gracefully/degradable_command_builder.rb +23 -0
  15. data/lib/gracefully/error.rb +37 -0
  16. data/lib/gracefully/health.rb +29 -0
  17. data/lib/gracefully/mutex_based_synchronized_counter.rb +30 -0
  18. data/lib/gracefully/retried_command.rb +25 -0
  19. data/lib/gracefully/short_circuited_command.rb +23 -0
  20. data/lib/gracefully/timed_command.rb +19 -0
  21. data/lib/gracefully/togglable_command.rb +20 -0
  22. data/lib/gracefully/try.rb +6 -34
  23. data/lib/gracefully/version.rb +1 -1
  24. data/spec/circuit_breaker_spec.rb +142 -0
  25. data/spec/command_spec.rb +34 -0
  26. data/spec/consecutive_failures_based_health_spec.rb +78 -0
  27. data/spec/degradable_spec.rb +49 -0
  28. data/spec/gracefully_spec.rb +136 -10
  29. data/spec/mutex_based_synchronized_counter_spec.rb +50 -0
  30. data/spec/retried_command_spec.rb +93 -0
  31. data/spec/short_circuited_command_spec.rb +136 -0
  32. data/spec/spec_helper.rb +3 -0
  33. data/spec/timecop_helper.rb +3 -0
  34. data/spec/timed_command_spec.rb +73 -0
  35. data/spec/togglable_command_spec.rb +31 -0
  36. metadata +39 -5
  37. data/lib/gracefully/feature_builder.rb +0 -24
  38. data/lib/gracefully/health_meter.rb +0 -90
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f736eec744523457533b32c6df2f79cc26c4c154
4
- data.tar.gz: a02c22d1d338f3120bf7a712d6df7345552e7e88
3
+ metadata.gz: cf954108bc3f1509923aeb64fff8704ce06d3023
4
+ data.tar.gz: 157d3f6c00fdcc349a93a1fb9893e9a8dae2ed7a
5
5
  SHA512:
6
- metadata.gz: 548dcba4a7645ce353bac284e57ee5b3d250f75ed40d4818bb514f71db0bd0e78af12b79979b9dfcf9bc53072f531fa8b54cd9d774cda568a5049adf35ffb291
7
- data.tar.gz: b88c02168bcf7a204ae38188245cad7d655723c61a67a96e128b32815f2bb1d6dc92d95dd8d004e5a06b874153e51e630daeb6d695f4e023d236d224439dbe9e
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
@@ -5,4 +5,6 @@ gemspec
5
5
 
6
6
  group :test do
7
7
  gem 'rspec', '~> 3.1.0'
8
+ gem 'timecop', '~> 0.7.1'
9
+ gem 'coveralls', require: false
8
10
  end
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
- See `spec/gracefully_spec.rb` for basic usage.
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/feature"
3
- require "gracefully/feature_builder"
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.degrade(feature_name)
8
- FeatureBuilder.new(feature_name)
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,6 @@
1
+ require 'gracefully'
2
+
3
+ require_relative 'circuit_breaker'
4
+ require_relative 'retried_command'
5
+ require_relative 'timed_command'
6
+ require_relative 'short_circuited_command'
@@ -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,6 @@
1
+ require_relative 'error'
2
+
3
+ module Gracefully
4
+ class CommandDisabledError < ::StandardError
5
+ end
6
+ 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 Feature
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 }.or_else(Try.to { @fallback_to.call *args }).get
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