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