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