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.
@@ -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: []