redis_ha 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ RedisHA
2
+ =======
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.
7
+
8
+
9
+ Installation
10
+ ------------
11
+
12
+ gem install redis_ha
13
+
14
+ or in your Gemfile:
15
+
16
+ gem 'redis_ha', '~> 0.1'
17
+
18
+
19
+ License
20
+ -------
21
+
22
+ Copyright (c) 2011 Paul Asmuth
23
+
24
+ Permission is hereby granted, free of charge, to any person obtaining
25
+ a copy of this software and associated documentation files (the
26
+ "Software"), to use, copy and modify copies of the Software, subject
27
+ to the following conditions:
28
+
29
+ The above copyright notice and this permission notice shall be
30
+ included in all copies or substantial portions of the Software.
31
+
32
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
33
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
34
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
35
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
36
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
37
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
38
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,11 @@
1
+ class RedisHA::Base
2
+
3
+ attr_accessor :pool, :key, :merge_strategy
4
+
5
+ def initialize(pool, key)
6
+ @pool = pool
7
+ @pool.ensure_connected
8
+ @key = key
9
+ end
10
+
11
+ end
@@ -0,0 +1,96 @@
1
+ class RedisHA::Connection < Thread
2
+
3
+ attr_accessor :status, :buffer
4
+
5
+ def initialize(redis_opts, opts = {})
6
+ self.abort_on_exception = true
7
+
8
+ @read_timeout = opts[:read_timeout]
9
+ @retry_timeout = opts[:retry_timeout]
10
+ @redis_opts = redis_opts
11
+
12
+ @queue = Queue.new
13
+ @buffer = Array.new
14
+ @lock = Mutex.new
15
+
16
+ super do
17
+ run
18
+ end
19
+ end
20
+
21
+ def next
22
+ @lock.synchronize do
23
+ @buffer.shift
24
+ end
25
+ end
26
+
27
+ def <<(msg)
28
+ @queue << msg
29
+ end
30
+
31
+ private
32
+
33
+ def run
34
+ while job = @queue.pop
35
+ semaphore, *msg = job
36
+
37
+ @lock.synchronize do
38
+ @buffer << send(*msg)
39
+ end
40
+
41
+ semaphore.decrement
42
+ end
43
+ end
44
+
45
+ def connect
46
+ with_timeout_and_check do
47
+ @redis = Redis.new(@redis_opts)
48
+ @redis.ping
49
+ end
50
+ end
51
+
52
+ def method_missing(*msg)
53
+ with_timeout_and_check do
54
+ @redis.send(*msg)
55
+ end
56
+ end
57
+
58
+ def with_timeout_and_check(&block)
59
+ return nil unless up_or_retry?
60
+ with_timeout(&block)
61
+ end
62
+
63
+ def with_timeout
64
+ ret = Timeout::timeout(@read_timeout) do
65
+ yield
66
+ end
67
+ rescue Redis::CannotConnectError
68
+ mark_as_down; nil
69
+ rescue Timeout::Error
70
+ mark_as_down; nil
71
+ else
72
+ mark_as_up; ret
73
+ end
74
+
75
+ def up_or_retry?
76
+ return true if @status == :up
77
+ return true unless @down_since
78
+
79
+ down_diff = Time.now.to_f - @down_since
80
+ return true if down_diff > @retry_timeout
81
+ false
82
+ end
83
+
84
+ def mark_as_down
85
+ @status = :down
86
+ @down_since = Time.now.to_f
87
+ end
88
+
89
+ def mark_as_up
90
+ return if @status == :up
91
+ @status = :up
92
+ @down_since = nil
93
+ end
94
+
95
+ end
96
+
@@ -0,0 +1,68 @@
1
+ class RedisHA::ConnectionPool
2
+
3
+ # timeout after which a redis connection is considered down. the
4
+ # default is 500ms
5
+ DEFAULT_READ_TIMEOUT = 0.5
6
+
7
+ # timeout after which a redis that was marked as down is retried
8
+ # the default is 5s
9
+ DEFAULT_RETRY_TIMEOUT = 5
10
+
11
+ attr_accessor :status, :connections, :read_timeout, :retry_timeout
12
+
13
+ def initialize
14
+ @read_timeout = DEFAULT_READ_TIMEOUT
15
+ @retry_timeout = DEFAULT_RETRY_TIMEOUT
16
+
17
+ @connections = []
18
+ @connected = false
19
+ end
20
+
21
+ def connect(*conns)
22
+ @connected = true
23
+
24
+ conns.each do |conn|
25
+ @connections << setup(conn)
26
+ end
27
+
28
+ async(:connect)
29
+ end
30
+
31
+ def ensure_connected
32
+ unless @connected
33
+ raise RedisHA::Error.new("you need to call Base.connect first")
34
+ end
35
+
36
+ unless @connections.map(&:status).include?(:up)
37
+ raise RedisHA::Error.new("no servers available")
38
+ end
39
+ end
40
+
41
+ def method_missing(*msg)
42
+ ensure_connected
43
+ async(*msg)
44
+ end
45
+
46
+ private
47
+
48
+ def async(*msg)
49
+ @semaphore = RedisHA::Semaphore.new(@connections.size)
50
+
51
+ @connections.each do |conn|
52
+ conn << [@semaphore, *msg]
53
+ end
54
+
55
+ @semaphore.wait
56
+
57
+ @connections.map(&:next).tap do
58
+ ensure_connected
59
+ end
60
+ end
61
+
62
+ def setup(redis_opts)
63
+ RedisHA::Connection.new(redis_opts,
64
+ :retry_timeout => @retry_timeout,
65
+ :read_timeout => @read_timeout)
66
+ end
67
+
68
+ end
@@ -0,0 +1,31 @@
1
+ class RedisHA::Counter < RedisHA::Base
2
+
3
+ # this lambda defines how the individual response hashes are merged
4
+ # the default is to select the maximum value
5
+ DEFAULT_MERGE_STRATEGY = ->(v) { v.map(&:to_i).max }
6
+
7
+ def incr(n = 1)
8
+ pool.incrby(@key, n)
9
+ true
10
+ end
11
+
12
+ def decr(n = 1)
13
+ pool.decrby(@key, n)
14
+ true
15
+ end
16
+
17
+ def set(n)
18
+ pool.set(@key, n)
19
+ true
20
+ end
21
+
22
+ def get
23
+ versions = pool.get(@key).compact
24
+ merge_strategy[versions]
25
+ end
26
+
27
+ def merge_strategy
28
+ @merge_strategy || DEFAULT_MERGE_STRATEGY
29
+ end
30
+
31
+ end
@@ -0,0 +1,31 @@
1
+ class RedisHA::HashMap < RedisHA::Base
2
+
3
+ # this lambda defines how the individual response hashes are merged
4
+ # the default is to merge in reverse-chronological order
5
+ DEFAULT_MERGE_STRATEGY = ->(v) { v
6
+ .sort{ |a,b| a[:_time] <=> b[:_time] }
7
+ .inject({}){ |t,c| t.merge!(c) } }
8
+
9
+
10
+ def set(data = {})
11
+ data.merge!(:_time => Time.now.to_i)
12
+ pool.set(@key, Marshal.dump(data))
13
+ true
14
+ end
15
+
16
+ def get
17
+ versions = pool.get(@key).map do |v|
18
+ next if v.nil? || v == ""
19
+ puts v.inspect
20
+ Marshal.load(v) rescue nil
21
+ end.compact
22
+ merge_strategy[versions].tap do |merged|
23
+ merged.delete(:_time)
24
+ end
25
+ end
26
+
27
+ def merge_strategy
28
+ @merge_strategy || DEFAULT_MERGE_STRATEGY
29
+ end
30
+
31
+ end
@@ -0,0 +1,20 @@
1
+ class RedisHA::Semaphore
2
+
3
+ POLL_INTERVAL = 0.00001
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
@@ -0,0 +1,26 @@
1
+ class RedisHA::Set < RedisHA::Base
2
+
3
+ # this lambda defines how the individual response hashes are merged
4
+ # the default is set union
5
+ DEFAULT_MERGE_STRATEGY = ->(v) { v.inject(&:+).uniq }
6
+
7
+ def add(*items)
8
+ pool.sadd(@key, *items)
9
+ true
10
+ end
11
+
12
+ def rem(*items)
13
+ pool.srem(@key, *items)
14
+ true
15
+ end
16
+
17
+ def get
18
+ versions = pool.smembers(@key).compact
19
+ merge_strategy[versions]
20
+ end
21
+
22
+ def merge_strategy
23
+ @merge_strategy || DEFAULT_MERGE_STRATEGY
24
+ end
25
+
26
+ end
data/lib/redis_ha.rb ADDED
@@ -0,0 +1,16 @@
1
+ require "rubygems"
2
+ require "redis"
3
+ require "timeout"
4
+
5
+ module RedisHA
6
+ class Error < StandardError
7
+ end
8
+ end
9
+
10
+ require "redis_ha/base"
11
+ require "redis_ha/semaphore"
12
+ require "redis_ha/connection"
13
+ require "redis_ha/connection_pool"
14
+ require "redis_ha/hash_map"
15
+ require "redis_ha/set"
16
+ require "redis_ha/counter"
data/lib/test.rb ADDED
@@ -0,0 +1,71 @@
1
+ require "rubygems"; require "redis"; require "pp"; require "ripl"
2
+
3
+ def bm(label)
4
+ t = Time.now.to_f
5
+ yield
6
+ d = (Time.now.to_f - t) * 1000
7
+ puts "#{label}: #{d.to_i}ms"
8
+ end
9
+
10
+ $: << ::File.expand_path("..", __FILE__)
11
+ require "redis_ha"
12
+
13
+ pool = RedisHA::ConnectionPool.new
14
+ pool.retry_timeout = 0.5
15
+ pool.read_timeout = 0.5
16
+ pool.connect(
17
+ {:host => "localhost", :port => 6379},
18
+ {:host => "localhost", :port => 6380},
19
+ {:host => "localhost", :port => 6381},
20
+ {:host => "localhost", :port => 6385})
21
+
22
+ map = RedisHA::HashMap.new(pool, "fnordmap")
23
+ set = RedisHA::Set.new(pool, "fnordset")
24
+ ctr = RedisHA::Counter.new(pool, "fnordctr")
25
+
26
+ Ripl.start :binding => binding
27
+ exit
28
+
29
+
30
+ bm "1000x HashMap.set w/ retries" do
31
+ 1000.times do |n|
32
+ map.set(:fu=>:bar, :fnord=>:bar)
33
+ end
34
+ end
35
+
36
+ pool = RedisHA::ConnectionPool.new
37
+ pool.retry_timeout = 50
38
+ pool.read_timeout = 0.5
39
+ pool.connect(
40
+ {:host => "localhost", :port => 6379},
41
+ {:host => "localhost", :port => 6380},
42
+ {:host => "localhost", :port => 6385})
43
+
44
+ map = RedisHA::HashMap.new(pool, "fnordmap")
45
+
46
+ bm "1000x HashMap.set w/o retries" do
47
+ 1000.times do |n|
48
+ map.set(:fu=>:bar, :fnord=>:bar)
49
+ end
50
+ end
51
+
52
+
53
+ bm "sequential connect" do
54
+ pool = RedisHA::ConnectionPool.new
55
+ pool.connect(:host => "localhost", :port => 6379)
56
+ pool.connect(:host => "localhost", :port => 6380)
57
+ pool.connect(:host => "localhost", :port => 6385)
58
+ pool.connect(:host => "localhost", :port => 6382)
59
+ pool.connect(:host => "localhost", :port => 6383)
60
+ end
61
+
62
+ bm "async connect" do
63
+ pool = RedisHA::ConnectionPool.new
64
+ pool.connect(
65
+ {:host => "localhost", :port => 6379},
66
+ {:host => "localhost", :port => 6380},
67
+ {:host => "localhost", :port => 6385},
68
+ {:host => "localhost", :port => 6382},
69
+ {:host => "localhost", :port => 6383}
70
+ )
71
+ end
data/redis_ha.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "redis_ha"
6
+ s.version = "0.0.1"
7
+ s.date = Date.today.to_s
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Paul Asmuth"]
10
+ s.email = ["paul@paulasmuth.com"]
11
+ s.homepage = "http://github.com/paulasmuth/redis_ha"
12
+ s.summary = %q{basic CRDTs and a HA connection pool for redis}
13
+ s.description = %q{Three basic CRDTs (set, hashmap and counter) for redis. Also includes a ConnectionPool that allows you to run concurrent redis commands on multiple connections w/o using eventmachine/em-hiredis.}
14
+ s.licenses = ["MIT"]
15
+
16
+ s.add_dependency "redis", ">= 2.2.2"
17
+
18
+ s.files = `git ls-files`.split("\n") - [".gitignore", ".rspec", ".travis.yml"]
19
+ s.test_files = `git ls-files -- spec/*`.split("\n")
20
+ s.require_paths = ["lib"]
21
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis_ha
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Paul Asmuth
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: &19924060 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 2.2.2
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *19924060
25
+ description: Three basic CRDTs (set, hashmap and counter) for redis. Also includes
26
+ a ConnectionPool that allows you to run concurrent redis commands on multiple connections
27
+ w/o using eventmachine/em-hiredis.
28
+ email:
29
+ - paul@paulasmuth.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - README.md
35
+ - lib/redis_ha.rb
36
+ - lib/redis_ha/base.rb
37
+ - lib/redis_ha/connection.rb
38
+ - 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/test.rb
44
+ - redis_ha.gemspec
45
+ homepage: http://github.com/paulasmuth/redis_ha
46
+ licenses:
47
+ - MIT
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ! '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubyforge_project:
66
+ rubygems_version: 1.8.17
67
+ signing_key:
68
+ specification_version: 3
69
+ summary: basic CRDTs and a HA connection pool for redis
70
+ test_files: []