raidis 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ ```
@@ -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
+
@@ -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: []