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 CHANGED
@@ -1,31 +1,97 @@
1
1
  RedisHA
2
2
  =======
3
3
 
4
- Three basic CRDTs (set, hashmap and counter) for redis. Also includes
5
- a ConnectionPool that allows you to run concurrent redis commands on
6
- multiple connections w/o using eventmachine/em-hiredis.
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
- here be dragons
70
+ RedisHA::Set (ADD/REM/GET)
14
71
 
72
+ ```ruby
73
+ >> set = RedisHA::Set.new(pool, "my-set")
15
74
 
16
- ADD/REM/GET on a RedisHA::Set
75
+ >> set.add(:fnord, :bar)
76
+ => true
17
77
 
18
- here be dragons
78
+ >> set.rem(:bar)
79
+ => true
19
80
 
81
+ >> set.get
82
+ => [:fnord]
83
+ ```
20
84
 
21
- INCR/DECR/SET/GET on a RedisHA::Counter
85
+ Timeouts
86
+ --------
22
87
 
23
- here be dragons
88
+ here be dragons
24
89
 
25
90
 
26
- SET/GET on a RedisHA::HashMap
91
+ Caveats
92
+ --------
27
93
 
28
- here be dragons
94
+ -> delete / decrement is not safe
29
95
 
30
96
 
31
97
 
@@ -1,92 +1,98 @@
1
- class RedisHA::Connection < Thread
1
+ class RedisHA::Connection < Socket
2
+ attr_accessor :addr, :status, :read_buffer, :write_buffer
2
3
 
3
- POLL_INTERVAL = 0.01
4
+ def initialize(redis, pool)
5
+ @write_buffer = ""
6
+ @read_buffer = ""
4
7
 
5
- attr_accessor :status, :buffer
8
+ super(AF_INET, SOCK_STREAM, 0)
6
9
 
7
- def initialize(redis_opts, opts = {})
8
- self.abort_on_exception = true
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
- super do
20
- run
21
- end
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 next
25
- @buffer_lock.synchronize do
26
- @buffer.shift
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 <<(msg)
31
- @buffer_lock.synchronize do
32
- @queue << msg
33
- end
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
- private
43
+ def <<(buf)
44
+ @write_buffer << buf
45
+ end
37
46
 
38
- def run
39
- while job = pop
40
- semaphore, *msg = job
47
+ def rewind
48
+ @read_buffer = ""
49
+ @write_buffer = ""
50
+ @ready = false
51
+ end
41
52
 
42
- @buffer_lock.synchronize do
43
- @buffer << send(*msg)
44
- end
53
+ def wait_read?
54
+ return false if @ready
55
+ @write_buffer.size == 0
56
+ end
45
57
 
46
- semaphore.decrement
47
- end
58
+ def wait_write?
59
+ return false if @ready
60
+ @write_buffer.size != 0
48
61
  end
49
62
 
50
- def pop
51
- loop do
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 connect
61
- with_timeout_and_check do
62
- @redis = Redis.new(@redis_opts)
63
- @redis.ping
64
- end
67
+ def ready?
68
+ @ready == true
65
69
  end
66
70
 
67
- def method_missing(*msg)
68
- with_timeout_and_check do
69
- @redis.send(*msg)
70
- end
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 with_timeout_and_check(&block)
74
- return nil unless up_or_retry?
75
- with_timeout(&block)
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 with_timeout
79
- ret = Timeout::timeout(@read_timeout) do
80
- yield
89
+ def check
90
+ if RedisHA::Protocol.peek?(@read_buffer)
91
+ @ready = true
81
92
  end
82
- rescue Exception => e
83
- @status = :down
84
- @down_since = Time.now.to_f
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 << setup(conn)
22
+ @connections << RedisHA::Connection.new(conn, self)
23
+ @connections.last.yield_connect
26
24
  end
25
+ end
27
26
 
28
- async(:connect)
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
- def ensure_connected
32
- unless @connected
33
- raise RedisHA::Error.new("you need to call Base.connect first")
34
- end
33
+ private
35
34
 
36
- unless @connections.map(&:status).include?(:up)
37
- raise RedisHA::Error.new("no servers available")
35
+ def execute(cmd)
36
+ @connections.each do |c|
37
+ c.rewind
38
+ c << cmd
38
39
  end
39
- end
40
40
 
41
- def method_missing(*msg)
42
- ensure_connected
43
- async(*msg)
44
- end
41
+ await
45
42
 
46
- private
43
+ @connections.map do |conn|
44
+ res = RedisHA::Protocol.parse(conn.read_buffer)
47
45
 
48
- def async(*msg)
49
- @semaphore = RedisHA::Semaphore.new(@connections.size)
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
- @connections.each do |conn|
52
- conn << [@semaphore, *msg]
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
- @semaphore.wait
63
+ req << @read_timeout
64
+ ready = IO.select(*req)
56
65
 
57
- @connections.map(&:next).tap do
58
- ensure_connected
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 setup(redis_opts)
63
- RedisHA::Connection.new(redis_opts,
64
- :retry_timeout => @retry_timeout,
65
- :read_timeout => @read_timeout)
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
@@ -4,7 +4,6 @@ class RedisHA::Base
4
4
 
5
5
  def initialize(pool, key)
6
6
  @pool = pool
7
- @pool.ensure_connected
8
7
  @key = key
9
8
  end
10
9
 
@@ -21,7 +21,7 @@ class RedisHA::Counter < RedisHA::Base
21
21
 
22
22
  def get
23
23
  versions = pool.get(@key).compact
24
- merge_strategy[versions]
24
+ merge_strategy[versions].to_i
25
25
  end
26
26
 
27
27
  def merge_strategy
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/base"
11
- require "redis_ha/semaphore"
6
+ require "redis_ha/protocol"
12
7
  require "redis_ha/connection"
13
8
  require "redis_ha/connection_pool"
14
- require "redis_ha/hash_map"
15
- require "redis_ha/set"
16
- require "redis_ha/counter"
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"; require "redis"; require "pp"; require "ripl"
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.to_i}ms"
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 = 0.5
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
@@ -3,7 +3,7 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "redis_ha"
6
- s.version = "0.0.5"
6
+ s.version = "0.1.0"
7
7
  s.date = Date.today.to_s
8
8
  s.platform = Gem::Platform::RUBY
9
9
  s.authors = ["Paul Asmuth"]
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.5
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-11-16 00:00:00.000000000 Z
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: &9331680 !ruby/object:Gem::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: *9331680
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/counter.rb
40
- - lib/redis_ha/hash_map.rb
41
- - lib/redis_ha/semaphore.rb
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.17
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
@@ -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