raidis 0.1.0 → 0.2.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.
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![Build Status](https://travis-ci.org/bukowskis/raidis.png)](https://travis-ci.org/bukowskis/raidis)
2
+
1
3
  # Raid-is
2
4
 
3
5
  Raidis is yet another failover solution for Redis.
@@ -27,7 +29,7 @@ Raidis.redis.class # => Redis::Namespace
27
29
 
28
30
  # How does it work?
29
31
 
30
- Whenever you call `Raidis.redis`, the connectivity to the remote redis server is monitored. If connectivity problems occur, or you're trying to make write-calls to a Redis slave, Raidis will immediately reload the `/etc/redis_master` file and try to perform the call to Redis again (hoping to have connected to a working redis server this time). If that second attempt failed (or you set `config.retries` to `0`) a `Raidis::ConnectionError` is raised.
32
+ Whenever you call `Raidis.redis`, the connectivity to the remote redis server is monitored. If connectivity problems occur, or you're trying to make write-calls to a Redis slave, Raidis will immediately reload the `/etc/redis_master` file and try to perform the call to Redis again (hoping to have connected to a working redis server this time). If that second attempt failed (or you set `config.retries` to `0`) a `Raidis::ConnectionError` is raised. To prevent too many retries in too short time, there is a minimum interval that Radis will wait after every retry attempt (see config `retry_interval`, default is 3 seconds).
31
33
 
32
34
  You should not have too many retries, because you don't want the end user's browser to hang and wait too long. That's where the `#available?` feature comes in. As soon as one of those connection errors occurs, the global variable `Raidis.available?` turns from `true` to `false` and any further damage can be mitigated, by simply not making any further calls to redis. You should inform your end-users about the outage in a friendly manner. E.g. like so:
33
35
 
@@ -60,6 +62,7 @@ Raidis.configure do |config|
60
62
  config.redis_timeout = 3 # seconds # default is whatever Redis.new has as default
61
63
 
62
64
  config.retries = 3 # times # default is 1
65
+ config.retry_interval = 3 # seconds # default is 3
63
66
  config.unavailability_timeout = 60 # seconds # default is 15 seconds
64
67
  config.info_file_path = '/opt/redis_server' # default is '/etc/redis_master'
65
68
 
@@ -13,7 +13,7 @@ module Raidis
13
13
  end
14
14
 
15
15
  attr_accessor :redis_namespace, :redis_timeout, :redis_db
16
- attr_writer :logger, :unavailability_timeout, :master, :retries
16
+ attr_writer :logger, :unavailability_timeout, :master, :retries, :retry_interval
17
17
 
18
18
  def logger
19
19
  @logger ||= begin
@@ -41,6 +41,10 @@ module Raidis
41
41
  @retries ||= 1
42
42
  end
43
43
 
44
+ def retry_interval
45
+ @retry_interval ||= 3
46
+ end
47
+
44
48
  def master
45
49
  unless @master
46
50
  unless info_file_path.exist?
@@ -1,3 +1,5 @@
1
+ require 'raidis/throttle'
2
+
1
3
  module Raidis
2
4
  class RedisWrapper
3
5
 
@@ -24,7 +26,9 @@ module Raidis
24
26
  result = block.call
25
27
  rescue Raidis::ConnectionError => exception
26
28
  # Try again a couple of times.
29
+ throttle.sleep_if_needed
27
30
  if (tries -= 1) >= 0
31
+ throttle.action!
28
32
  reconnect!
29
33
  retry
30
34
  end
@@ -81,6 +85,10 @@ module Raidis
81
85
  @redis = nil
82
86
  end
83
87
 
88
+ def throttle
89
+ @throttle ||= Throttle.new config.retry_interval
90
+ end
91
+
84
92
  # Internal: Establishes a brand-new, raw connection to Redis.
85
93
  #
86
94
  # Returns a Redis::Namespace instance or nil if we don't know where the Redis server is.
@@ -0,0 +1,22 @@
1
+ module Raidis
2
+ class Throttle
3
+
4
+ attr_reader :interval, :last_action
5
+
6
+ def initialize(seconds = 3)
7
+ @interval = seconds.to_i
8
+ end
9
+
10
+ def sleep_if_needed
11
+ return unless last_action
12
+ duration = last_action + interval - Time.now
13
+ return unless duration.to_i > 0
14
+ sleep duration
15
+ end
16
+
17
+ def action!
18
+ @last_action = Time.now
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+
3
+ describe Raidis::Throttle do
4
+
5
+ let(:interval) { 3 }
6
+ let(:throttle) { Raidis::Throttle.new }
7
+
8
+ before do
9
+ Raidis::Throttle.stub!(:sleep)
10
+ end
11
+
12
+ context 'no action took place yet' do
13
+ describe '#sleep_if_needed' do
14
+ it 'does not sleep' do
15
+ throttle.should_not_receive(:sleep)
16
+ throttle.sleep_if_needed
17
+ end
18
+ end
19
+ end
20
+
21
+ context 'action took place 1 second ago' do
22
+ before do
23
+ Timecop.freeze
24
+ throttle.action!
25
+ Timecop.freeze Time.now + 1
26
+ end
27
+
28
+ describe '#sleep_if_needed' do
29
+ it 'sleeps for 2 seconds' do
30
+ throttle.should_receive(:sleep).with(2)
31
+ throttle.sleep_if_needed
32
+ end
33
+ end
34
+ end
35
+
36
+ context 'action took place 2 seconds ago' do
37
+ before do
38
+ Timecop.freeze
39
+ throttle.action!
40
+ Timecop.freeze Time.now + 2
41
+ end
42
+
43
+ describe '#sleep_if_needed' do
44
+ it 'sleeps for 1 seconds' do
45
+ throttle.should_receive(:sleep).with(1)
46
+ throttle.sleep_if_needed
47
+ end
48
+ end
49
+ end
50
+
51
+ context 'action took place 4 seconds ago' do
52
+ before do
53
+ Timecop.freeze
54
+ throttle.action!
55
+ Timecop.freeze Time.now + 4
56
+ end
57
+
58
+ describe '#sleep_if_needed' do
59
+ it 'does not sleep' do
60
+ throttle.should_not_receive(:sleep)
61
+ throttle.sleep_if_needed
62
+ end
63
+ end
64
+ end
65
+
66
+ end
@@ -21,7 +21,7 @@ end
21
21
 
22
22
  describe Raidis::RedisWrapper do
23
23
 
24
- let(:config) { mock(:config, retries: 3) }
24
+ let(:config) { mock(:config, retries: 3, retry_interval: 0) }
25
25
  let(:backend) { mock(:backend) }
26
26
  let(:shaky_backend) { ShakyRedis.new }
27
27
  let(:wrapper) { Raidis::RedisWrapper.new }
@@ -71,13 +71,27 @@ describe Raidis::RedisWrapper do
71
71
  wrapper.some_redis_command.should == :shaky_result
72
72
  end
73
73
 
74
- context 'when running out of retries' do
74
+ context 'with a retry interval' do
75
75
  before do
76
- config.stub!(:retries).and_return 2
76
+ config.stub!(:retry_interval).and_return 10
77
77
  end
78
78
 
79
- it 'finally raises a unified connection error' do
80
- expect { wrapper.some_redis_command }.to raise_error(Raidis::ConnectionError)
79
+ it 'waits some time before each retry' do
80
+ wrapper.throttle.should_receive(:sleep).exactly(2).times.with(10)
81
+ Timecop.freeze
82
+ wrapper.some_redis_command.should == :shaky_result
83
+ end
84
+
85
+ context 'when running out of retries' do
86
+ before do
87
+ config.stub!(:retries).and_return 1
88
+ end
89
+
90
+ it 'finally raises a unified connection error' do
91
+ wrapper.throttle.should_receive(:sleep).with(10)
92
+ Timecop.freeze
93
+ expect { wrapper.some_redis_command }.to raise_error(Raidis::ConnectionError)
94
+ end
81
95
  end
82
96
  end
83
97
  end
@@ -34,6 +34,7 @@ RSpec.configure do |config|
34
34
  Raidis.configure do |config|
35
35
  config.redis_db = 15
36
36
  config.redis_timeout = 0.5
37
+ config.retry_interval = 0
37
38
  end
38
39
  Raidis.config.stub!(:info_file_path).and_return mock(:info_file_path, exist?: true, readable?: true, read: '127.0.0.1')
39
40
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: raidis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -100,7 +100,9 @@ files:
100
100
  - lib/raidis/availability.rb
101
101
  - lib/raidis/configuration.rb
102
102
  - lib/raidis/redis_wrapper.rb
103
+ - lib/raidis/throttle.rb
103
104
  - lib/raidis.rb
105
+ - spec/lib/raidis/throttle_spec.rb
104
106
  - spec/lib/raidis_spec.rb
105
107
  - spec/lib/redis_wrapper_spec.rb
106
108
  - spec/spec_helper.rb