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 +4 -4
- data/README.md +31 -3
- data/lib/circuit_breakage/breaker.rb +27 -28
- data/lib/circuit_breakage/redis_backed_breaker.rb +61 -0
- data/lib/circuit_breakage/version.rb +1 -1
- data/spec/{caching_breaker_spec.rb → redis_backed_breaker_spec.rb} +5 -5
- metadata +5 -5
- data/lib/circuit_breakage/caching_breaker.rb +0 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1b30cd904f63ca99c1f7b95a89bfef2a5c29237d
|
4
|
+
data.tar.gz: c55ce5e3e2bf0463175dcd2c25ace63543ef2e1e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 (
|
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
|
-
|
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
|
-
|
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.
|
25
|
-
self.
|
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
|
-
|
29
|
+
case(state)
|
30
|
+
when 'open'
|
31
31
|
if time_to_retry?
|
32
|
-
|
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
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
57
|
-
|
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
|
-
|
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
|
@@ -2,11 +2,11 @@
|
|
2
2
|
# group for this and the vanilla Breaker spec.
|
3
3
|
|
4
4
|
module CircuitBreakage
|
5
|
-
describe
|
6
|
-
let(:breaker)
|
7
|
-
let(:
|
8
|
-
let(:key)
|
9
|
-
let(:block)
|
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
|
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-
|
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/
|
86
|
+
- lib/circuit_breakage/redis_backed_breaker.rb
|
87
87
|
- lib/circuit_breakage/version.rb
|
88
88
|
- spec/breaker_spec.rb
|
89
|
-
- spec/
|
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/
|
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
|