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 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