raidis 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.
- data/README.md +69 -0
- data/lib/raidis.rb +51 -0
- data/lib/raidis/availability.rb +34 -0
- data/lib/raidis/configuration.rb +89 -0
- data/lib/raidis/redis_wrapper.rb +104 -0
- data/spec/lib/raidis_spec.rb +162 -0
- data/spec/lib/redis_wrapper_spec.rb +88 -0
- data/spec/spec_helper.rb +47 -0
- metadata +132 -0
data/README.md
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# Raid-is
|
2
|
+
|
3
|
+
Raidis is yet another failover solution for Redis.
|
4
|
+
|
5
|
+
## Why not use RedisFailover?
|
6
|
+
|
7
|
+
* Firstly, because RedisFailover may fail itself (it depends on Zookeeper, remember?), while the Redis server is perfectly healthy
|
8
|
+
* Seondly, RedisFailover is utterly incompatible with the thread'n'fork chaos introduced by Resque workers
|
9
|
+
* Thirdly, if you have but one single application not using Ruby, you'll have to come up with a _global_ failover solution anyway
|
10
|
+
|
11
|
+
## How do I get started?
|
12
|
+
|
13
|
+
Raidis knows where the redis master is by checking the file `/etc/redis_master` and there looking for content such as `127.0.0.1:6379` (you can omit the port if you like to). Note that you can use a RedisFailover daemon to update that file if you wish. At any rate, you won't need to use `Redis.new` or `RedisFailover.new` anymore. You'll find your ready-to-go redis in `Raidis.redis` with zero-configuration in your application.
|
14
|
+
|
15
|
+
```bash
|
16
|
+
# Bash
|
17
|
+
echo "127.0.0.1:6379" > /etc/redis_master
|
18
|
+
```
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
# Ruby application
|
22
|
+
require 'raidis'
|
23
|
+
Raidis.redis.get('some_key')
|
24
|
+
|
25
|
+
Raidis.redis.class # => Redis::Namespace
|
26
|
+
```
|
27
|
+
|
28
|
+
# How does it work?
|
29
|
+
|
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.
|
31
|
+
|
32
|
+
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
|
+
|
34
|
+
```ruby
|
35
|
+
if Raidis.available?
|
36
|
+
counter = Raidis.redis.get('visits')
|
37
|
+
else
|
38
|
+
counter = "Sorry, the visits counter is not available right now."
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
Note that it is one of the design goals of this gem that there are no performance penalties when using `.available?`.
|
43
|
+
|
44
|
+
After 15 seconds (or whichever timeout you configure), `Raidis.available?` turns to `true` again automatically and the file `/etc/redis_master` is read again in order to find the remote redis server. If you wish, you may use `Raidis.reconnect!` to evoke the end of that unavailability period manually.
|
45
|
+
|
46
|
+
# Connection Pools
|
47
|
+
|
48
|
+
If you need to maintain a ConnectionPool with multiple connections to Redis (e.g. when you use [Sidekiq](https://github.com/mperham/sidekiq/issues/794)), you may use the something like `ConnectionPool.new { Raidis.redis! }` (note the exclamation mark) to populate your pool. `.raidis!` returns a new connection each time you call it. Note that `Raidis.available?` is a global variable, so if any of the pools fail, Raidis will go into unavailable mode as described above.
|
49
|
+
|
50
|
+
## Configuration
|
51
|
+
|
52
|
+
Example:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
Raidis.configure do |config|
|
56
|
+
|
57
|
+
config.logger = Rails.logger # default is Rails.logger (if defined) otherwise: Logger.new(STDOUT)
|
58
|
+
config.redis_namespace = :myapp # default is nil
|
59
|
+
config.redis_db = (Rails.env.test? ? 1 : 0) # default is whatever Redis.new has as default
|
60
|
+
config.redis_timeout = 3 # seconds # default is whatever Redis.new has as default
|
61
|
+
|
62
|
+
config.retries = 3 # times # default is 1
|
63
|
+
config.unavailability_timeout = 60 # seconds # default is 15 seconds
|
64
|
+
config.info_file_path = '/opt/redis_server' # default is '/etc/redis_master'
|
65
|
+
|
66
|
+
# You can override the info_file content if you like
|
67
|
+
config.master = '127.0.0.1' # if omitted, the content of /etc/redis_master is used
|
68
|
+
end
|
69
|
+
```
|
data/lib/raidis.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'redis/namespace'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
require 'raidis/availability'
|
5
|
+
require 'raidis/configuration'
|
6
|
+
require 'raidis/redis_wrapper'
|
7
|
+
|
8
|
+
module Raidis
|
9
|
+
|
10
|
+
ConnectionError = Class.new(RuntimeError)
|
11
|
+
|
12
|
+
extend self
|
13
|
+
extend Availability
|
14
|
+
|
15
|
+
# Public: The singleton Redis connection object.
|
16
|
+
#
|
17
|
+
# Returns a RedisWrapper instance.
|
18
|
+
#
|
19
|
+
def redis
|
20
|
+
return @redis if @redis
|
21
|
+
@redis = redis!
|
22
|
+
connected?
|
23
|
+
@redis
|
24
|
+
end
|
25
|
+
|
26
|
+
# Public: Updates the #available? flag by actually testing the Redis connection.
|
27
|
+
#
|
28
|
+
# Returns true or false.
|
29
|
+
#
|
30
|
+
def connected?
|
31
|
+
return unavailable! unless @redis
|
32
|
+
@redis.setex(:raidis, 1, :rocks) && available!
|
33
|
+
rescue Raidis::ConnectionError
|
34
|
+
unavailable!
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: Evokes a fresh lookup of the Redis server endpoint.
|
38
|
+
#
|
39
|
+
def reconnect!
|
40
|
+
@redis = nil
|
41
|
+
redis
|
42
|
+
end
|
43
|
+
|
44
|
+
# Public: Creates a brand-new failsafe-wrapped connection to Redis.
|
45
|
+
# This is ONLY useful if you need to maintain your own ConnectionPool.
|
46
|
+
# See https://github.com/mperham/sidekiq/issues/794
|
47
|
+
#
|
48
|
+
def redis!
|
49
|
+
RedisWrapper.new
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Raidis
|
2
|
+
module Availability
|
3
|
+
|
4
|
+
def available?
|
5
|
+
if unavailability_age_in_seconds >= config.unavailability_timeout
|
6
|
+
available!
|
7
|
+
else
|
8
|
+
!!@available
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def available!
|
13
|
+
checking_availability
|
14
|
+
@available = true
|
15
|
+
end
|
16
|
+
|
17
|
+
def unavailable!
|
18
|
+
checking_availability
|
19
|
+
@available = false
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def checking_availability
|
25
|
+
@last_availability_check = Time.now.to_i
|
26
|
+
end
|
27
|
+
|
28
|
+
def unavailability_age_in_seconds
|
29
|
+
return 0 unless @last_availability_check
|
30
|
+
Time.now.to_i - @last_availability_check.to_i
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Raidis
|
2
|
+
class Configuration
|
3
|
+
|
4
|
+
InfoFilePathNotFound = Class.new(RuntimeError)
|
5
|
+
|
6
|
+
class Master
|
7
|
+
attr_accessor :endpoint
|
8
|
+
attr_writer :port
|
9
|
+
|
10
|
+
def port
|
11
|
+
@port ||= 6379
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_accessor :redis_namespace, :redis_timeout, :redis_db
|
16
|
+
attr_writer :logger, :unavailability_timeout, :master, :retries
|
17
|
+
|
18
|
+
def logger
|
19
|
+
@logger ||= begin
|
20
|
+
if defined?(Rails)
|
21
|
+
Rails.logger
|
22
|
+
else
|
23
|
+
Logger.new(STDOUT)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def info_file_path
|
29
|
+
@info_file_path ||= Pathname.new('/etc/redis_master')
|
30
|
+
end
|
31
|
+
|
32
|
+
def info_file_path=(path)
|
33
|
+
Pathname.new path
|
34
|
+
end
|
35
|
+
|
36
|
+
def unavailability_timeout
|
37
|
+
@unavailability_timeout ||= 15 # seconds
|
38
|
+
end
|
39
|
+
|
40
|
+
def retries
|
41
|
+
@retries ||= 1
|
42
|
+
end
|
43
|
+
|
44
|
+
def master
|
45
|
+
unless @master
|
46
|
+
unless info_file_path.exist?
|
47
|
+
Trouble.notify(InfoFilePathNotFound.new, code: :info_file_not_found, message: 'Raidis could not find the redis master info file', location: info_file_path) if defined?(Trouble)
|
48
|
+
return
|
49
|
+
end
|
50
|
+
|
51
|
+
unless info_file_path.readable?
|
52
|
+
Trouble.notify(InfoFilePathNotFound.new, code: :info_file_not_readable, message: 'The redis master info file exists but is not readable for Raidis', location: info_file_path) if defined?(Trouble)
|
53
|
+
return
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
content = @master || info_file_path.read
|
58
|
+
server = Master.new
|
59
|
+
server.endpoint, server.port = content.strip.to_s.split(':')
|
60
|
+
|
61
|
+
unless server.endpoint
|
62
|
+
if @master
|
63
|
+
Trouble.notify(InfoFilePathNotFound.new, code: :invalid_master, message: 'Raidis does not understand the config.master value you provided', value: config.master.inspect) if defined?(Trouble)
|
64
|
+
else
|
65
|
+
Trouble.notify(InfoFilePathNotFound.new, code: :invalid_info_file_content, message: 'Raidis found the redis master info file, but there was no valid endpoint in it', location: info_file_path, content: info_file_path.read.inspect) if defined?(Trouble)
|
66
|
+
end
|
67
|
+
return
|
68
|
+
end
|
69
|
+
server
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
module Raidis
|
75
|
+
extend self
|
76
|
+
|
77
|
+
def config
|
78
|
+
@config ||= Configuration.new
|
79
|
+
end
|
80
|
+
|
81
|
+
def configure(&block)
|
82
|
+
yield config
|
83
|
+
end
|
84
|
+
|
85
|
+
def reset!
|
86
|
+
config = nil
|
87
|
+
reconnect!
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Raidis
|
2
|
+
class RedisWrapper
|
3
|
+
|
4
|
+
# Public: Proxies everything to the Redis backend.
|
5
|
+
#
|
6
|
+
# Returns whatever the backend returns.
|
7
|
+
# Raises Raidis::ConnectionError if there is a connection problem.
|
8
|
+
#
|
9
|
+
def method_missing(method, *args, &block)
|
10
|
+
raise(Raidis::ConnectionError, 'No Redis backend found.') unless redis
|
11
|
+
reloading_connection do
|
12
|
+
observing_connection { redis.send(method, *args, &block) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Internal: If a Raidis::ConnectionError is detected during the execution of the block,
|
17
|
+
# try to reconnect to Redis and try again. Updates the availability state.
|
18
|
+
#
|
19
|
+
# Returns whatever the block returns.
|
20
|
+
# Raises Raidis::ConnectionError if the connection problem persists even after the retries.
|
21
|
+
#
|
22
|
+
def reloading_connection(&block)
|
23
|
+
tries ||= config.retries
|
24
|
+
result = block.call
|
25
|
+
rescue Raidis::ConnectionError => exception
|
26
|
+
# Try again a couple of times.
|
27
|
+
if (tries -= 1) >= 0
|
28
|
+
reconnect!
|
29
|
+
retry
|
30
|
+
end
|
31
|
+
# Giving up.
|
32
|
+
Raidis.unavailable!
|
33
|
+
raise exception
|
34
|
+
else
|
35
|
+
# No exception was raised, reaffirming the availability.
|
36
|
+
Raidis.available!
|
37
|
+
result
|
38
|
+
end
|
39
|
+
|
40
|
+
# Internal: Raises a Raidis::ConnectionError if there are connection-related problems during the execution of the block.
|
41
|
+
# More specifically, if the connection is lost or a write is performed against a slave, the Exception will be raised.
|
42
|
+
#
|
43
|
+
def observing_connection(&block)
|
44
|
+
yield
|
45
|
+
|
46
|
+
rescue *connection_errors => exception
|
47
|
+
Trouble.notify(exception, code: :lost_connection, message: 'Raidis lost connection to the Redis server.', client: redis.inspect) if defined?(Trouble)
|
48
|
+
raise Raidis::ConnectionError, exception
|
49
|
+
|
50
|
+
rescue Redis::CommandError => exception
|
51
|
+
if exception.message.to_s.split.first == 'READONLY'
|
52
|
+
Trouble.notify(exception, code: :readonly, message: 'Raidis detected an illegal write against a Redis slave.', client: redis.inspect) if defined?(Trouble)
|
53
|
+
raise Raidis::ConnectionError, exception
|
54
|
+
else
|
55
|
+
# Passing through Exceptions unrelated to the Connection. E.g. Redis::CommandError.
|
56
|
+
raise exception
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Internal: A list of known connection-related Exceptions the backend may raise.
|
61
|
+
#
|
62
|
+
def connection_errors
|
63
|
+
[
|
64
|
+
Redis::BaseConnectionError,
|
65
|
+
IOError,
|
66
|
+
Timeout::Error,
|
67
|
+
Errno::EADDRNOTAVAIL,
|
68
|
+
Errno::EAGAIN,
|
69
|
+
Errno::EBADF,
|
70
|
+
Errno::ECONNABORTED,
|
71
|
+
Errno::ECONNREFUSED,
|
72
|
+
Errno::ECONNRESET,
|
73
|
+
Errno::EHOSTUNREACH,
|
74
|
+
Errno::EINVAL,
|
75
|
+
Errno::ENETUNREACH,
|
76
|
+
Errno::EPIPE,
|
77
|
+
]
|
78
|
+
end
|
79
|
+
|
80
|
+
def reconnect!
|
81
|
+
@redis = nil
|
82
|
+
end
|
83
|
+
|
84
|
+
# Internal: Establishes a brand-new, raw connection to Redis.
|
85
|
+
#
|
86
|
+
# Returns a Redis::Namespace instance or nil if we don't know where the Redis server is.
|
87
|
+
#
|
88
|
+
def redis
|
89
|
+
@redis ||= redis!
|
90
|
+
end
|
91
|
+
|
92
|
+
def redis!
|
93
|
+
return unless master = config.master
|
94
|
+
raw_redis = Redis.new db: config.redis_db, host: master.endpoint, port: master.port, timeout: config.redis_timeout
|
95
|
+
Redis::Namespace.new config.redis_namespace, redis: raw_redis
|
96
|
+
end
|
97
|
+
|
98
|
+
# Internal: Convenience wrapper
|
99
|
+
#
|
100
|
+
def config
|
101
|
+
Raidis.config
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# This is an integration test that requires a Redis server.
|
4
|
+
|
5
|
+
describe Raidis do
|
6
|
+
|
7
|
+
let(:raidis) { Raidis }
|
8
|
+
let(:redis) { raidis.redis }
|
9
|
+
|
10
|
+
context 'when connected' do
|
11
|
+
context 'to a Redis master' do
|
12
|
+
before do
|
13
|
+
redis.slaveof :no, :one
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '.redis' do
|
17
|
+
it 'is is a RedisWrapper' do
|
18
|
+
redis.should be_instance_of Raidis::RedisWrapper
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'lets through command errors unrelated to the connection' do
|
22
|
+
expect { redis.setex(:invalid, -1, :parameters) }.to raise_error(Redis::CommandError)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '.connected?' do
|
27
|
+
it 'is true' do
|
28
|
+
raidis.should be_connected
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '.available?' do
|
33
|
+
it 'is true' do
|
34
|
+
raidis.should be_available
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'to a Redis slave' do
|
40
|
+
before do
|
41
|
+
redis.slaveof '127.0.0.1', 12345
|
42
|
+
raidis.reconnect!
|
43
|
+
end
|
44
|
+
|
45
|
+
after do
|
46
|
+
redis.slaveof :no, :one
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '.redis' do
|
50
|
+
it 'detects illegal writes to a slave' do
|
51
|
+
Trouble.should_receive(:notify) do |exception, metadata|
|
52
|
+
exception.should be_instance_of Redis::CommandError
|
53
|
+
metadata[:code].should == :readonly
|
54
|
+
end
|
55
|
+
expect { redis.setex(:writing, 1, :against_a_slave) }.to raise_error(Raidis::ConnectionError)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'is fine with read-only commands' do
|
59
|
+
redis.get(:some_key).should be_nil
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'lets through command errors unrelated to the connection' do
|
63
|
+
expect { redis.lrange(:invalid, :command, :arguments) }.to raise_error(Redis::CommandError)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe '.connected?' do
|
68
|
+
it 'is false' do
|
69
|
+
raidis.should_not be_connected
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe '.available?' do
|
74
|
+
it 'is false' do
|
75
|
+
raidis.should_not be_available
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'when not connected' do
|
82
|
+
before do
|
83
|
+
Raidis.config.master = '127.0.0.1:0'
|
84
|
+
Raidis.reconnect!
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'unavailability timeout' do
|
88
|
+
before do
|
89
|
+
Raidis.config.unavailability_timeout = 5 # Seconds
|
90
|
+
end
|
91
|
+
|
92
|
+
describe '.available?' do
|
93
|
+
it 'becomes available again after the unavailability_timeout' do
|
94
|
+
raidis.should_not be_available
|
95
|
+
Timecop.travel Time.now + 4
|
96
|
+
raidis.should_not be_available
|
97
|
+
Timecop.travel Time.now + 1
|
98
|
+
raidis.should be_available
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context 'because the Network is unreachable' do
|
104
|
+
before do
|
105
|
+
Raidis.config.master = '192.0.2.1' # RFC 5737
|
106
|
+
Raidis.reconnect!
|
107
|
+
end
|
108
|
+
|
109
|
+
describe '.redis' do
|
110
|
+
it 'detects that there is no connection' do
|
111
|
+
Trouble.should_receive(:notify) do |exception, metadata|
|
112
|
+
exception.should be_instance_of Redis::CannotConnectError
|
113
|
+
metadata[:code].should == :lost_connection
|
114
|
+
end
|
115
|
+
expect { redis.get(:some_key) }.to raise_error(Raidis::ConnectionError)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe '.connected?' do
|
120
|
+
it 'is false' do
|
121
|
+
raidis.should_not be_connected
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe '.available?' do
|
126
|
+
it 'is false' do
|
127
|
+
raidis.should_not be_available
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
context 'because of a wrong port' do
|
133
|
+
before do
|
134
|
+
Raidis.config.master = '127.0.0.1:80'
|
135
|
+
Raidis.reconnect!
|
136
|
+
end
|
137
|
+
|
138
|
+
describe '.redis' do
|
139
|
+
it 'detects that there is no connection' do
|
140
|
+
Trouble.should_receive(:notify) do |exception, metadata|
|
141
|
+
exception.should be_instance_of Redis::CannotConnectError
|
142
|
+
metadata[:code].should == :lost_connection
|
143
|
+
end
|
144
|
+
expect { redis.get(:some_key) }.to raise_error(Raidis::ConnectionError)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe '.connected?' do
|
149
|
+
it 'is false' do
|
150
|
+
raidis.should_not be_connected
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
describe '.available?' do
|
155
|
+
it 'is false' do
|
156
|
+
raidis.should_not be_available
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class ShakyRedis
|
4
|
+
def initialize(failures = 3)
|
5
|
+
@failures = 3
|
6
|
+
end
|
7
|
+
|
8
|
+
def perform
|
9
|
+
@calls ||= 0
|
10
|
+
if @calls < @failures
|
11
|
+
@calls += 1
|
12
|
+
raise Errno::ECONNREFUSED
|
13
|
+
end
|
14
|
+
:shaky_result
|
15
|
+
end
|
16
|
+
|
17
|
+
def method_missing(method, *args, &block)
|
18
|
+
perform
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe Raidis::RedisWrapper do
|
23
|
+
|
24
|
+
let(:config) { mock(:config, retries: 3) }
|
25
|
+
let(:backend) { mock(:backend) }
|
26
|
+
let(:shaky_backend) { ShakyRedis.new }
|
27
|
+
let(:wrapper) { Raidis::RedisWrapper.new }
|
28
|
+
|
29
|
+
before do
|
30
|
+
wrapper.stub!(:config).and_return config
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#method_missing' do
|
34
|
+
context 'with a stable connection' do
|
35
|
+
before do
|
36
|
+
wrapper.stub!(:redis!).and_return backend
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'proxies everything to the backend' do
|
40
|
+
backend.should_receive(:any_redis_command).with(:some_key).and_return 'value'
|
41
|
+
wrapper.any_redis_command(:some_key).should == 'value'
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'passes on Exceptions from the backend' do
|
45
|
+
backend.should_receive(:invalid_redis_command).and_raise Redis::CommandError
|
46
|
+
expect { wrapper.invalid_redis_command }.to raise_error(Redis::CommandError)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'raises a Raidis::ConnectionError if the backend could not be instantiated' do
|
50
|
+
wrapper.stub!(:redis)
|
51
|
+
expect { wrapper.redis_command }.to raise_error(Raidis::ConnectionError)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'wraps the call in reloading_connection' do
|
55
|
+
wrapper.should_receive(:reloading_connection).with(no_args()).and_return 'some_value'
|
56
|
+
wrapper.get(:some_key).should == 'some_value'
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'wraps the call in observing_connection' do
|
60
|
+
wrapper.should_receive(:observing_connection).with(no_args()).and_return 'some_value'
|
61
|
+
wrapper.get(:some_key).should == 'some_value'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'with an unstable connection' do
|
66
|
+
before do
|
67
|
+
wrapper.stub!(:redis!).and_return shaky_backend
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'retrieves the result even when the backend fails several times' do
|
71
|
+
wrapper.some_redis_command.should == :shaky_result
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'when running out of retries' do
|
75
|
+
before do
|
76
|
+
config.stub!(:retries).and_return 2
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'finally raises a unified connection error' do
|
80
|
+
expect { wrapper.some_redis_command }.to raise_error(Raidis::ConnectionError)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'timecop'
|
2
|
+
require 'raidis'
|
3
|
+
|
4
|
+
def ensure_class_or_module(full_name, class_or_module)
|
5
|
+
full_name.to_s.split(/::/).inject(Object) do |context, name|
|
6
|
+
begin
|
7
|
+
context.const_get(name)
|
8
|
+
rescue NameError
|
9
|
+
if class_or_module == :class
|
10
|
+
context.const_set(name, Class.new)
|
11
|
+
else
|
12
|
+
context.const_set(name, Module.new)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def ensure_module(name)
|
19
|
+
ensure_class_or_module(name, :module)
|
20
|
+
end
|
21
|
+
|
22
|
+
def ensure_class(name)
|
23
|
+
ensure_class_or_module(name, :class)
|
24
|
+
end
|
25
|
+
|
26
|
+
ensure_module :Trouble
|
27
|
+
|
28
|
+
RSpec.configure do |config|
|
29
|
+
|
30
|
+
# Global before hook
|
31
|
+
config.before do
|
32
|
+
Trouble.stub!(:notify)
|
33
|
+
|
34
|
+
Raidis.configure do |config|
|
35
|
+
config.redis_db = 15
|
36
|
+
config.redis_timeout = 0.5
|
37
|
+
end
|
38
|
+
Raidis.config.stub!(:info_file_path).and_return mock(:info_file_path, exist?: true, readable?: true, read: '127.0.0.1')
|
39
|
+
end
|
40
|
+
|
41
|
+
# Global after hooks
|
42
|
+
config.after do
|
43
|
+
Raidis.reset!
|
44
|
+
Timecop.return
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: raidis
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- bukowskis
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-03-26 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: redis-namespace
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: guard-rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rb-fsevent
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: timecop
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
description: See https://github.com/bukowskis/raidis
|
95
|
+
email:
|
96
|
+
executables: []
|
97
|
+
extensions: []
|
98
|
+
extra_rdoc_files: []
|
99
|
+
files:
|
100
|
+
- lib/raidis/availability.rb
|
101
|
+
- lib/raidis/configuration.rb
|
102
|
+
- lib/raidis/redis_wrapper.rb
|
103
|
+
- lib/raidis.rb
|
104
|
+
- spec/lib/raidis_spec.rb
|
105
|
+
- spec/lib/redis_wrapper_spec.rb
|
106
|
+
- spec/spec_helper.rb
|
107
|
+
- README.md
|
108
|
+
homepage: https://github.com/bukowskis/raidis
|
109
|
+
licenses: []
|
110
|
+
post_install_message:
|
111
|
+
rdoc_options: []
|
112
|
+
require_paths:
|
113
|
+
- lib
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
115
|
+
none: false
|
116
|
+
requirements:
|
117
|
+
- - ! '>='
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
requirements: []
|
127
|
+
rubyforge_project:
|
128
|
+
rubygems_version: 1.8.23
|
129
|
+
signing_key:
|
130
|
+
specification_version: 3
|
131
|
+
summary: Yet another failover wrapper around Redis.
|
132
|
+
test_files: []
|