redis_ha 0.0.5 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +77 -11
- data/lib/redis_ha/connection.rb +71 -66
- data/lib/redis_ha/connection_pool.rb +54 -28
- data/lib/redis_ha/{base.rb → crdt/base.rb} +0 -1
- data/lib/redis_ha/{counter.rb → crdt/counter.rb} +1 -1
- data/lib/redis_ha/{hash_map.rb → crdt/hash_map.rb} +0 -0
- data/lib/redis_ha/{set.rb → crdt/set.rb} +0 -0
- data/lib/redis_ha/protocol.rb +38 -0
- data/lib/redis_ha.rb +7 -10
- data/lib/test.rb +22 -7
- data/redis_ha.gemspec +1 -1
- metadata +15 -10
- data/lib/redis_ha/semaphore.rb +0 -20
data/README.md
CHANGED
@@ -1,31 +1,97 @@
|
|
1
1
|
RedisHA
|
2
2
|
=======
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
A redis client that runs commands on multiple servers in parallel
|
5
|
+
without blocking if one of them is down.
|
6
|
+
|
7
|
+
I used this to implement a highly available session store on top of
|
8
|
+
redis; it writes and reads the data to multiple instances and merges
|
9
|
+
the responses after every read. This approach is negligibly slower
|
10
|
+
than writing to a single server since RedisHA uses asynchronous I/O
|
11
|
+
and is much more robust than complex server-side redis failover solutions
|
12
|
+
(sentinel, pacemaker, etcetera).
|
13
|
+
|
14
|
+
The gem includes three basic CRDTs (set, hashmap and counter).
|
15
|
+
|
16
|
+
[1] _DeCandia, Hastorun et al_ (2007). [Dynamo: Amazon’s Highly Available Key-value Store](http://www.read.seas.harvard.edu/~kohler/class/cs239-w08/decandia07dynamo.pd) (SOSP 2007)
|
17
|
+
|
7
18
|
|
8
19
|
Usage
|
9
20
|
-----
|
10
21
|
|
11
|
-
Create a RedisHA::ConnectionPool
|
22
|
+
Create a RedisHA::ConnectionPool (connect does not block):
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
pool = RedisHA::ConnectionPool.new
|
26
|
+
pool.connect(
|
27
|
+
{:host => "localhost", :port => 6379},
|
28
|
+
{:host => "localhost", :port => 6380},
|
29
|
+
{:host => "localhost", :port => 6381}
|
30
|
+
)
|
31
|
+
```
|
32
|
+
|
33
|
+
Execute a command in parallel:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
>> pool.ping
|
37
|
+
=> ["PONG", "PONG", "PONG"]
|
38
|
+
|
39
|
+
>> pool.setnx "fnord", 1
|
40
|
+
=> [1,1,1]
|
41
|
+
```
|
42
|
+
|
43
|
+
RedisHA::Counter (INCR/DECR/SET/GET)
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
>> ctr = RedisHA::Counter.new(pool, "my-counter")
|
47
|
+
|
48
|
+
>> ctr.set 3
|
49
|
+
=> true
|
50
|
+
|
51
|
+
>> ctr.incr
|
52
|
+
=> true
|
53
|
+
|
54
|
+
>> ctr.get
|
55
|
+
=> 4
|
56
|
+
```
|
57
|
+
|
58
|
+
RedisHA::HashMap (SET/GET)
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
>> map = RedisHA::HashMap.new(pool, "my-hashmap")
|
62
|
+
|
63
|
+
>> map.set(:fnord => 1, :fubar => 2)
|
64
|
+
=> true
|
65
|
+
|
66
|
+
=> map.get
|
67
|
+
=> {:fnord=>1, :fubar=>2}
|
68
|
+
```
|
12
69
|
|
13
|
-
|
70
|
+
RedisHA::Set (ADD/REM/GET)
|
14
71
|
|
72
|
+
```ruby
|
73
|
+
>> set = RedisHA::Set.new(pool, "my-set")
|
15
74
|
|
16
|
-
|
75
|
+
>> set.add(:fnord, :bar)
|
76
|
+
=> true
|
17
77
|
|
18
|
-
|
78
|
+
>> set.rem(:bar)
|
79
|
+
=> true
|
19
80
|
|
81
|
+
>> set.get
|
82
|
+
=> [:fnord]
|
83
|
+
```
|
20
84
|
|
21
|
-
|
85
|
+
Timeouts
|
86
|
+
--------
|
22
87
|
|
23
|
-
|
88
|
+
here be dragons
|
24
89
|
|
25
90
|
|
26
|
-
|
91
|
+
Caveats
|
92
|
+
--------
|
27
93
|
|
28
|
-
|
94
|
+
-> delete / decrement is not safe
|
29
95
|
|
30
96
|
|
31
97
|
|
data/lib/redis_ha/connection.rb
CHANGED
@@ -1,92 +1,98 @@
|
|
1
|
-
class RedisHA::Connection <
|
1
|
+
class RedisHA::Connection < Socket
|
2
|
+
attr_accessor :addr, :status, :read_buffer, :write_buffer
|
2
3
|
|
3
|
-
|
4
|
+
def initialize(redis, pool)
|
5
|
+
@write_buffer = ""
|
6
|
+
@read_buffer = ""
|
4
7
|
|
5
|
-
|
8
|
+
super(AF_INET, SOCK_STREAM, 0)
|
6
9
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
@read_timeout = opts[:read_timeout]
|
11
|
-
@retry_timeout = opts[:retry_timeout]
|
12
|
-
@redis_opts = redis_opts
|
13
|
-
|
14
|
-
@queue = Array.new
|
15
|
-
@queue_lock = Mutex.new
|
16
|
-
@buffer = Array.new
|
17
|
-
@buffer_lock = Mutex.new
|
10
|
+
@pool = pool
|
11
|
+
setup(redis)
|
12
|
+
end
|
18
13
|
|
19
|
-
|
20
|
-
|
21
|
-
|
14
|
+
def yield_connect
|
15
|
+
connect_nonblock(@__addr)
|
16
|
+
rescue Errno::EINPROGRESS, Errno::ECONNABORTED, Errno::EINVAL
|
17
|
+
nil
|
18
|
+
rescue Errno::ECONNREFUSED
|
19
|
+
finish(:fail)
|
22
20
|
end
|
23
21
|
|
24
|
-
def
|
25
|
-
|
26
|
-
@
|
22
|
+
def yield_read
|
23
|
+
loop do
|
24
|
+
@read_buffer << read_nonblock(1)[0]
|
27
25
|
end
|
26
|
+
rescue Errno::EAGAIN
|
27
|
+
check || raise(Errno::EAGAIN)
|
28
|
+
rescue Errno::ENOTCONN
|
29
|
+
yield_connect
|
30
|
+
rescue Errno::ECONNREFUSED
|
31
|
+
finish(:fail)
|
28
32
|
end
|
29
33
|
|
30
|
-
def
|
31
|
-
@
|
32
|
-
|
33
|
-
|
34
|
+
def yield_write
|
35
|
+
len = write_nonblock(@write_buffer)
|
36
|
+
@write_buffer = @write_buffer[len..-1] || ""
|
37
|
+
rescue Errno::EPIPE
|
38
|
+
yield_connect
|
39
|
+
rescue Errno::ECONNREFUSED
|
40
|
+
finish(:fail)
|
34
41
|
end
|
35
42
|
|
36
|
-
|
43
|
+
def <<(buf)
|
44
|
+
@write_buffer << buf
|
45
|
+
end
|
37
46
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
47
|
+
def rewind
|
48
|
+
@read_buffer = ""
|
49
|
+
@write_buffer = ""
|
50
|
+
@ready = false
|
51
|
+
end
|
41
52
|
|
42
|
-
|
43
|
-
|
44
|
-
|
53
|
+
def wait_read?
|
54
|
+
return false if @ready
|
55
|
+
@write_buffer.size == 0
|
56
|
+
end
|
45
57
|
|
46
|
-
|
47
|
-
|
58
|
+
def wait_write?
|
59
|
+
return false if @ready
|
60
|
+
@write_buffer.size != 0
|
48
61
|
end
|
49
62
|
|
50
|
-
def
|
51
|
-
|
52
|
-
sleep(POLL_INTERVAL) while @queue.size < 1
|
53
|
-
@queue_lock.synchronize do
|
54
|
-
job = @queue.shift
|
55
|
-
return job if job
|
56
|
-
end
|
57
|
-
end
|
63
|
+
def execution_expired
|
64
|
+
finish(:fail)
|
58
65
|
end
|
59
66
|
|
60
|
-
def
|
61
|
-
|
62
|
-
@redis = Redis.new(@redis_opts)
|
63
|
-
@redis.ping
|
64
|
-
end
|
67
|
+
def ready?
|
68
|
+
@ready == true
|
65
69
|
end
|
66
70
|
|
67
|
-
def
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
+
def setup(redis)
|
72
|
+
addr = [redis.fetch(:port), redis.fetch(:host)]
|
73
|
+
addr[1] = (TCPSocket.gethostbyname(addr[1])[4])
|
74
|
+
@__addr = Socket.pack_sockaddr_in(*addr)
|
71
75
|
end
|
72
76
|
|
73
|
-
def
|
74
|
-
|
75
|
-
|
77
|
+
def finish(stat)
|
78
|
+
@ready = true
|
79
|
+
|
80
|
+
if stat == :success
|
81
|
+
@down_since = nil if @status != :up
|
82
|
+
@status = :up
|
83
|
+
else
|
84
|
+
@status = :down
|
85
|
+
@down_since = Time.now.to_f
|
86
|
+
end
|
76
87
|
end
|
77
88
|
|
78
|
-
def
|
79
|
-
|
80
|
-
|
89
|
+
def check
|
90
|
+
if RedisHA::Protocol.peek?(@read_buffer)
|
91
|
+
@ready = true
|
81
92
|
end
|
82
|
-
|
83
|
-
|
84
|
-
@
|
85
|
-
return nil
|
86
|
-
else
|
87
|
-
@down_since = nil if @status != :up
|
88
|
-
@status = :up
|
89
|
-
return ret
|
93
|
+
|
94
|
+
finish(:success) if @ready
|
95
|
+
@ready
|
90
96
|
end
|
91
97
|
|
92
98
|
def up_or_retry?
|
@@ -94,9 +100,8 @@ private
|
|
94
100
|
return true unless @down_since
|
95
101
|
|
96
102
|
down_diff = Time.now.to_f - @down_since
|
97
|
-
return true if down_diff > @retry_timeout
|
103
|
+
return true if down_diff > @pool.retry_timeout
|
98
104
|
false
|
99
105
|
end
|
100
106
|
|
101
107
|
end
|
102
|
-
|
@@ -15,54 +15,80 @@ class RedisHA::ConnectionPool
|
|
15
15
|
@retry_timeout = DEFAULT_RETRY_TIMEOUT
|
16
16
|
|
17
17
|
@connections = []
|
18
|
-
@connected = false
|
19
18
|
end
|
20
19
|
|
21
20
|
def connect(*conns)
|
22
|
-
@connected = true
|
23
|
-
|
24
21
|
conns.each do |conn|
|
25
|
-
@connections <<
|
22
|
+
@connections << RedisHA::Connection.new(conn, self)
|
23
|
+
@connections.last.yield_connect
|
26
24
|
end
|
25
|
+
end
|
27
26
|
|
28
|
-
|
27
|
+
def method_missing(*msg)
|
28
|
+
msg = msg.map(&:to_s)
|
29
|
+
req = RedisHA::Protocol.request(*msg)
|
30
|
+
execute(req)
|
29
31
|
end
|
30
32
|
|
31
|
-
|
32
|
-
unless @connected
|
33
|
-
raise RedisHA::Error.new("you need to call Base.connect first")
|
34
|
-
end
|
33
|
+
private
|
35
34
|
|
36
|
-
|
37
|
-
|
35
|
+
def execute(cmd)
|
36
|
+
@connections.each do |c|
|
37
|
+
c.rewind
|
38
|
+
c << cmd
|
38
39
|
end
|
39
|
-
end
|
40
40
|
|
41
|
-
|
42
|
-
ensure_connected
|
43
|
-
async(*msg)
|
44
|
-
end
|
41
|
+
await
|
45
42
|
|
46
|
-
|
43
|
+
@connections.map do |conn|
|
44
|
+
res = RedisHA::Protocol.parse(conn.read_buffer)
|
47
45
|
|
48
|
-
|
49
|
-
|
46
|
+
if res.is_a?(Exception)
|
47
|
+
@connections.each(&:rewind)
|
48
|
+
raise res
|
49
|
+
else
|
50
|
+
res
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
50
54
|
|
51
|
-
|
52
|
-
|
55
|
+
def select
|
56
|
+
req = [[],[],[]]
|
57
|
+
|
58
|
+
@connections.each do |c|
|
59
|
+
req[0] << c if c.wait_read?
|
60
|
+
req[1] << c if c.wait_write?
|
53
61
|
end
|
54
62
|
|
55
|
-
@
|
63
|
+
req << @read_timeout
|
64
|
+
ready = IO.select(*req)
|
56
65
|
|
57
|
-
|
58
|
-
|
66
|
+
unless ready
|
67
|
+
req[0].each(&:execution_expired)
|
68
|
+
req[1].each(&:execution_expired)
|
69
|
+
return
|
59
70
|
end
|
71
|
+
|
72
|
+
ready[0].each(&:yield_read)
|
73
|
+
ready[1].each(&:yield_write)
|
60
74
|
end
|
61
75
|
|
62
|
-
def
|
63
|
-
|
64
|
-
|
65
|
-
|
76
|
+
def await
|
77
|
+
loop do
|
78
|
+
begin
|
79
|
+
await = false
|
80
|
+
select
|
81
|
+
|
82
|
+
@connections.each do |conn|
|
83
|
+
next unless conn.up_or_retry?
|
84
|
+
await = true unless conn.ready?
|
85
|
+
end
|
86
|
+
|
87
|
+
break unless await
|
88
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
89
|
+
next
|
90
|
+
end
|
91
|
+
end
|
66
92
|
end
|
67
93
|
|
68
94
|
end
|
File without changes
|
File without changes
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class RedisHA::Protocol
|
2
|
+
|
3
|
+
def self.request(*args)
|
4
|
+
args.inject("*#{args.size}\r\n") do |s, arg|
|
5
|
+
s << "$#{arg.size}\r\n#{arg}\r\n"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.peek?(buf)
|
10
|
+
if ["+", ":", "-"].include?(buf[0])
|
11
|
+
buf[-2..-1] == "\r\n"
|
12
|
+
elsif buf[0] == "$"
|
13
|
+
offset = buf.index("\r\n").to_i
|
14
|
+
return false if offset == 0
|
15
|
+
length = buf[1..offset].to_i
|
16
|
+
return true if length == -1
|
17
|
+
buf.size >= (length + offset + 2)
|
18
|
+
elsif buf[0] == "*"
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.parse(buf)
|
24
|
+
case buf[0]
|
25
|
+
when "-" then RuntimeError.new(buf[1..-3])
|
26
|
+
when "+" then buf[1..-3]
|
27
|
+
when ":" then buf[1..-3].to_i
|
28
|
+
|
29
|
+
when "$"
|
30
|
+
buf.sub(/.*\r\n/,"")[0...-2] if buf[1..2] != "-1"
|
31
|
+
|
32
|
+
when "*"
|
33
|
+
RuntimeError.new("multi bulk replies are not supported")
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
data/lib/redis_ha.rb
CHANGED
@@ -1,16 +1,13 @@
|
|
1
1
|
require "rubygems"
|
2
2
|
require "redis"
|
3
|
-
require "timeout"
|
4
3
|
|
5
|
-
module RedisHA
|
6
|
-
class Error < StandardError
|
7
|
-
end
|
8
|
-
end
|
4
|
+
module RedisHA; end
|
9
5
|
|
10
|
-
require "redis_ha/
|
11
|
-
require "redis_ha/semaphore"
|
6
|
+
require "redis_ha/protocol"
|
12
7
|
require "redis_ha/connection"
|
13
8
|
require "redis_ha/connection_pool"
|
14
|
-
|
15
|
-
require "redis_ha/
|
16
|
-
require "redis_ha/
|
9
|
+
|
10
|
+
require "redis_ha/crdt/base"
|
11
|
+
require "redis_ha/crdt/hash_map"
|
12
|
+
require "redis_ha/crdt/set"
|
13
|
+
require "redis_ha/crdt/counter"
|
data/lib/test.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
|
-
require "rubygems"
|
1
|
+
require "rubygems"
|
2
|
+
require "redis"
|
3
|
+
require "pp"
|
4
|
+
require "ripl"
|
2
5
|
|
3
6
|
def bm(label)
|
4
7
|
t = Time.now.to_f
|
5
8
|
yield
|
6
9
|
d = (Time.now.to_f - t) * 1000
|
7
|
-
puts "#{label}: #{d.
|
10
|
+
puts "#{label}: #{d.round(2)}ms"
|
8
11
|
end
|
9
12
|
|
10
13
|
$: << ::File.expand_path("..", __FILE__)
|
@@ -12,12 +15,9 @@ require "redis_ha"
|
|
12
15
|
|
13
16
|
pool = RedisHA::ConnectionPool.new
|
14
17
|
pool.retry_timeout = 0.5
|
15
|
-
pool.read_timeout =
|
18
|
+
pool.read_timeout = 10.1
|
16
19
|
pool.connect(
|
17
|
-
{:host => "localhost", :port => 6379}
|
18
|
-
{:host => "localhost", :port => 6380},
|
19
|
-
{:host => "localhost", :port => 6381},
|
20
|
-
{:host => "localhost", :port => 6385})
|
20
|
+
{:host => "localhost", :port => 6379})
|
21
21
|
|
22
22
|
map = RedisHA::HashMap.new(pool, "fnordmap")
|
23
23
|
set = RedisHA::Set.new(pool, "fnordset")
|
@@ -26,6 +26,21 @@ ctr = RedisHA::Counter.new(pool, "fnordctr")
|
|
26
26
|
Ripl.start :binding => binding
|
27
27
|
exit
|
28
28
|
|
29
|
+
[100, 1000, 10000].each do |b|
|
30
|
+
bm "#{b}x ping" do
|
31
|
+
b.times do |n|
|
32
|
+
pool.ping
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
while sleep 1
|
38
|
+
bm "1000x ping" do
|
39
|
+
1000.times do |n|
|
40
|
+
pool.ping
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
29
44
|
|
30
45
|
bm "1000x HashMap.set w/ retries" do
|
31
46
|
1000.times do |n|
|
data/redis_ha.gemspec
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis_ha
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-12-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|
16
|
-
requirement:
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,7 +21,12 @@ dependencies:
|
|
21
21
|
version: 2.2.2
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements:
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.2.2
|
25
30
|
description: Three basic CRDTs (set, hashmap and counter) for redis. Also includes
|
26
31
|
a ConnectionPool that allows you to run concurrent redis commands on multiple connections
|
27
32
|
w/o using eventmachine/em-hiredis.
|
@@ -33,13 +38,13 @@ extra_rdoc_files: []
|
|
33
38
|
files:
|
34
39
|
- README.md
|
35
40
|
- lib/redis_ha.rb
|
36
|
-
- lib/redis_ha/base.rb
|
37
41
|
- lib/redis_ha/connection.rb
|
38
42
|
- lib/redis_ha/connection_pool.rb
|
39
|
-
- lib/redis_ha/
|
40
|
-
- lib/redis_ha/
|
41
|
-
- lib/redis_ha/
|
42
|
-
- lib/redis_ha/set.rb
|
43
|
+
- lib/redis_ha/crdt/base.rb
|
44
|
+
- lib/redis_ha/crdt/counter.rb
|
45
|
+
- lib/redis_ha/crdt/hash_map.rb
|
46
|
+
- lib/redis_ha/crdt/set.rb
|
47
|
+
- lib/redis_ha/protocol.rb
|
43
48
|
- lib/test.rb
|
44
49
|
- redis_ha.gemspec
|
45
50
|
homepage: http://github.com/paulasmuth/redis_ha
|
@@ -63,7 +68,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
68
|
version: '0'
|
64
69
|
requirements: []
|
65
70
|
rubyforge_project:
|
66
|
-
rubygems_version: 1.8.
|
71
|
+
rubygems_version: 1.8.24
|
67
72
|
signing_key:
|
68
73
|
specification_version: 3
|
69
74
|
summary: basic CRDTs and a HA connection pool for redis
|
data/lib/redis_ha/semaphore.rb
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
class RedisHA::Semaphore
|
2
|
-
|
3
|
-
POLL_INTERVAL = 0.001
|
4
|
-
|
5
|
-
def initialize(n)
|
6
|
-
@lock = Mutex.new
|
7
|
-
@n = n
|
8
|
-
end
|
9
|
-
|
10
|
-
def decrement
|
11
|
-
@lock.synchronize do
|
12
|
-
@n -= 1
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def wait
|
17
|
-
sleep(POLL_INTERVAL) while @n != 0
|
18
|
-
end
|
19
|
-
|
20
|
-
end
|