redis_ha 0.0.5 → 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 +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
|