circuit_breakage 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dad99a3f4f0f6b939a2092e865ac7dca10b5fe4b
4
- data.tar.gz: 6e7684487d5a883b407013767e8f6a454933c9fb
3
+ metadata.gz: 1b30cd904f63ca99c1f7b95a89bfef2a5c29237d
4
+ data.tar.gz: c55ce5e3e2bf0463175dcd2c25ace63543ef2e1e
5
5
  SHA512:
6
- metadata.gz: 4895d56acb08b9c942e1f571162017b46b40da00b5872b5a5891d411dd5db3461d48d0f21c3ef8227efbf3aab48e3c7df10a1f79ea32e50b45cefc43a2b6d6cf
7
- data.tar.gz: 3f9da79d01746dcf5e3d8e14ae4adc7eb3f831370ed1cbf55842d97eca744fd15f85662c572905038c7e4f7c61a38a141ab9a26c8de0df0c803bf6aa0fabb963
6
+ metadata.gz: 7dfae02fe9c5876c2a1caa6454231dd33f325e9ddd6814b8a9fbc107e302fb15e44afb8cb6d55a0f09991caf6d6194ad1fc28af26387b9afc0db0d89687b6db7
7
+ data.tar.gz: 4fa1879643290ddc117d4ff8a3aac6cd0137230dba40953b11f6ab468fdcab9a7e69e407e121e40ab56dc8a74e4ea697484bdaf761ebd8ae4c0c5c26a64af5cf
data/README.md CHANGED
@@ -1,20 +1,48 @@
1
1
  # CircuitBreakage
2
2
 
3
3
  A simple Circuit Breaker implementation in Ruby with a timeout. A Circuit
4
- Breaker wraps potentially troublesome logic and will "trip" the circuit (and
4
+ Breaker wraps potentially troublesome logic and will "trip" the circuit (ie,
5
5
  stop trying to run the logic) if it sees too many failures. After a while, it
6
6
  will retry.
7
7
 
8
8
  ## Usage
9
9
 
10
+ ### Normal Boring Circuit Breakers
11
+
10
12
  ```ruby
11
13
  block = ->(*args) do
12
14
  # Some dangerous thing.
13
15
  end
14
16
 
15
- breaker = CircuitBreakage.new(block)
17
+ breaker = CircuitBreakage::Breaker.new(block)
16
18
  breaker.failure_threshold = 3 # only 3 failures before tripping circuit
17
19
  breaker.duration = 10 # 10 seconds before retry
18
20
  breaker.timeout = 0.5 # 500 milliseconds allowed before auto-fail
19
21
 
20
- breaker.call(*some_args) # args are passed through to block
22
+ begin
23
+ breaker.call(*some_args) # args are passed through to block
24
+ rescue CircuitBreaker::CircuitOpen
25
+ puts "Too many recent failures!"
26
+ rescue CircuitBreaker::CircuitTimeout
27
+ puts "Operation timed out!"
28
+ end
29
+ ```
30
+
31
+ ### Redis-backed "Shared" Circuit Breakers
32
+
33
+ The unique feature of this particular Circuit Breaker gem is that it also
34
+ supports shared state via Redis, using the SETNX and GETSET commands. This
35
+ allows a number of circuit breakers running in separate processes to trip and
36
+ un-trip in unison.
37
+
38
+ ```ruby
39
+ connection = some_redis_connection
40
+ key = 'my_app/some_operation'
41
+
42
+ breaker = CircuitBreakage::RedisBackedBreaker.new(connection, key, block)
43
+ # Everything else is the same as above.
44
+ ```
45
+
46
+ So, if you have the same piece of code running on 27 instances across 3
47
+ different servers, as soon as one trips, they all trip, and as soon as one
48
+ resets, they all reset.
@@ -16,63 +16,62 @@ module CircuitBreakage
16
16
  DEFAULT_TIMEOUT = 10 # Number of seconds before the call times out
17
17
 
18
18
  def initialize(block)
19
- @block = block
19
+ self.block = block
20
20
  self.failure_threshold = DEFAULT_FAILURE_THRESHOLD
21
21
  self.duration = DEFAULT_DURATION
22
22
  self.timeout = DEFAULT_TIMEOUT
23
-
24
- self.failure_count ||= 0
25
- self.last_failed ||= Time.at(0)
26
- closed!
23
+ self.failure_count ||= 0
24
+ self.last_failed ||= Time.at(0)
25
+ self.state ||= 'closed'
27
26
  end
28
27
 
29
28
  def call(*args)
30
- if open?
29
+ case(state)
30
+ when 'open'
31
31
  if time_to_retry?
32
- half_open!
32
+ do_retry(*args)
33
33
  else
34
34
  raise CircuitOpen
35
35
  end
36
+ when 'closed'
37
+ do_call(*args)
36
38
  end
39
+ end
37
40
 
38
- begin
39
- ret_value = nil
40
- Timeout.timeout(self.timeout, CircuitTimeout) do
41
- ret_value = @block.call(*args)
42
- end
43
- handle_success
41
+ private
44
42
 
45
- return ret_value
46
- rescue Exception => e
47
- handle_failure
48
- end
43
+ # Defined independently so that it can be overridden.
44
+ def do_retry(*args)
45
+ do_call(*args)
49
46
  end
50
47
 
51
- [:open, :closed, :half_open].each do |state|
52
- define_method("#{state}?") {
53
- self.state == state
54
- }
48
+ def do_call(*args)
49
+ ret_value = nil
50
+ Timeout.timeout(self.timeout, CircuitTimeout) do
51
+ ret_value = @block.call(*args)
52
+ end
53
+ handle_success
55
54
 
56
- define_method("#{state}!") {
57
- self.state = state
58
- }
55
+ return ret_value
56
+ rescue Exception => e
57
+ handle_failure
59
58
  end
60
59
 
61
- private
62
-
63
60
  def time_to_retry?
64
61
  Time.now >= self.last_failed + self.duration
65
62
  end
66
63
 
67
64
  def handle_success
68
- closed!
69
65
  self.failure_count = 0
66
+ self.state = 'closed'
70
67
  end
71
68
 
72
69
  def handle_failure
73
70
  self.last_failed = Time.now
74
71
  self.failure_count += 1
75
- open! if self.failure_count >= self.failure_threshold
72
+ if self.failure_count >= self.failure_threshold
73
+ self.state = 'open'
74
+ end
76
75
  end
77
76
  end
78
77
  end
@@ -0,0 +1,61 @@
1
+ module CircuitBreakage
2
+ # Similar to Breaker, but accepts a Redis connection and an arbitrary key,
3
+ # and will share state among an arbitrary number of RedisBackedBreakers via
4
+ # Redis. Relies on the SETNX redis command.
5
+ #
6
+ class RedisBackedBreaker < Breaker
7
+
8
+ # How long before we decide a lock-holder has crashed, in seconds.
9
+ LOCK_TIMEOUT = DEFAULT_TIMEOUT + 10
10
+
11
+ attr_reader :connection, :key
12
+
13
+ def initialize(connection, key, block)
14
+ raise NotImplementedError.new("Still working on it!")
15
+
16
+ @connection = connection
17
+ @key = key
18
+ super(block)
19
+ end
20
+
21
+ private
22
+
23
+ def do_retry(*args)
24
+ try_with_mutex('half_open_retry') do
25
+ super
26
+ end
27
+ end
28
+
29
+ def try_with_mutex(lock, &block)
30
+ mutex_key = "#{@key}/locks/#{lock}"
31
+
32
+ acquired = @connection.setnx(mutex_key, Time.now.to_i)
33
+ if acquired = 0 # mutex is already acquired
34
+ locked_at = @connection.get(mutex_key)
35
+ return if locked_at + LOCK_TIMEOUT < Time.now.to_i # unexpired lock
36
+ locked_at_second_check = @connection.getset(mutex_key, Time.now.to_i)
37
+ return if locked_at_second_check != locked_at # expired lock, but somebody beat us to it
38
+ end
39
+ # If we get this far, we have successfully acquired the mutex.
40
+
41
+ begin
42
+ block.call
43
+ ensure
44
+ @connection.del(mutex_key)
45
+ end
46
+ end
47
+
48
+ [:state, :failure_count, :last_failed].each do |attr|
49
+ attr_key = "#{@key}/attrs/#{attr}"
50
+
51
+ define_method(attr) do
52
+ @connection.get(attr_key)
53
+ end
54
+
55
+ define_method("#{attr}=") do |value|
56
+ @connection.set(attr_key, value)
57
+ end
58
+ end
59
+
60
+ end
61
+ end
@@ -1,3 +1,3 @@
1
1
  module CircuitBreakage
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -2,11 +2,11 @@
2
2
  # group for this and the vanilla Breaker spec.
3
3
 
4
4
  module CircuitBreakage
5
- describe CachingBreaker do
6
- let(:breaker) { CachingBreaker.new(cache, key, block) }
7
- let(:cache) { MockCache.new }
8
- let(:key) { 'test/data' }
9
- let(:block) { ->(x) { return x } }
5
+ describe RedisBackedBreaker do
6
+ let(:breaker) { RedisBackedBreaker.new(connection, key, block) }
7
+ let(:connection) { MockCache.new }
8
+ let(:key) { 'test/data' }
9
+ let(:block) { ->(x) { return x } }
10
10
 
11
11
  describe '#call' do
12
12
  subject { -> { breaker.call(arg) } }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: circuit_breakage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Hyland
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-09-04 00:00:00.000000000 Z
11
+ date: 2014-12-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -83,10 +83,10 @@ files:
83
83
  - circuit_breakage.gemspec
84
84
  - lib/circuit_breakage.rb
85
85
  - lib/circuit_breakage/breaker.rb
86
- - lib/circuit_breakage/caching_breaker.rb
86
+ - lib/circuit_breakage/redis_backed_breaker.rb
87
87
  - lib/circuit_breakage/version.rb
88
88
  - spec/breaker_spec.rb
89
- - spec/caching_breaker_spec.rb
89
+ - spec/redis_backed_breaker_spec.rb
90
90
  - spec/spec_helper.rb
91
91
  homepage: https://source.datanerd.us/jhyland/circuit_breakage
92
92
  licenses:
@@ -114,5 +114,5 @@ specification_version: 4
114
114
  summary: Provides a simple circuit breaker pattern.
115
115
  test_files:
116
116
  - spec/breaker_spec.rb
117
- - spec/caching_breaker_spec.rb
117
+ - spec/redis_backed_breaker_spec.rb
118
118
  - spec/spec_helper.rb
@@ -1,31 +0,0 @@
1
- module CircuitBreakage
2
- # Similar to Breaker, but accepts a cache object, and will call #write and
3
- # #fetch on that object to store and retrieve all state, instead of keeping
4
- # it in memory.
5
- #
6
- class CachingBreaker < Breaker
7
- attr_reader :cache, :key
8
-
9
- def initialize(cache, key, block)
10
- @cache = cache
11
- @key = key
12
- super(block)
13
- end
14
-
15
- def self.cached_attr(*attrs)
16
- attrs.each do |attr|
17
- define_method attr do
18
- raise "You must define the cache and key on a CachingBreaker!" unless cache && key
19
- cache.fetch "#{key}/#{attr}"
20
- end
21
-
22
- define_method "#{attr}=" do |value|
23
- raise "You must define the cache and key on a CachingBreaker!" unless cache && key
24
- cache.write "#{key}/#{attr}", value
25
- end
26
- end
27
- end
28
-
29
- cached_attr :failure_count, :last_failed, :state
30
- end
31
- end