ssdb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,27 @@
1
+ class SSDB::Batch < Array
2
+
3
+ # Constructor
4
+ def initialize
5
+ @futures = []
6
+ super
7
+ end
8
+
9
+ # Call command
10
+ # @param [Hash] opts the command options
11
+ def call(opts)
12
+ push(opts)
13
+
14
+ future = SSDB::Future.new(opts[:cmd])
15
+ @futures.push(future)
16
+ future
17
+ end
18
+
19
+ # @param [Array] values
20
+ def values=(values)
21
+ values.each_with_index do |value, index|
22
+ future = @futures[index]
23
+ future.value = value if future
24
+ end
25
+ end
26
+
27
+ end
@@ -0,0 +1,173 @@
1
+ require "socket"
2
+ require "uri"
3
+
4
+ class SSDB
5
+ class Client
6
+ NL = "\n".freeze
7
+ OK = "ok".freeze
8
+ NOT_FOUND = "not_found".freeze
9
+
10
+ attr_reader :url, :timeout
11
+ attr_accessor :reconnect
12
+
13
+ # @param [Hash] opts
14
+ # @option opts [String|URI] :url the URL to connect to,required
15
+ # @option opts [Numeric] :timeout socket timeout, defaults to 10s
16
+ def initialize(opts = {})
17
+ @timeout = opts[:timeout] || 10.0
18
+ @sock = nil
19
+ @url = parse_url(opts[:url] || ENV["SSDB_URL"] || "ssdb://127.0.0.1:8888/")
20
+ @reconnect = opts[:reconnect] != false
21
+ end
22
+
23
+ # @return [String] URL string
24
+ def id
25
+ url.to_s
26
+ end
27
+
28
+ # @return [Integer] port
29
+ def port
30
+ @port ||= url.port || 8888
31
+ end
32
+
33
+ # @return [Boolean] true if connected
34
+ def connected?
35
+ !!@sock
36
+ end
37
+
38
+ # Disconnects the client
39
+ def disconnect
40
+ @sock.close if connected?
41
+ rescue
42
+ ensure
43
+ @sock = nil
44
+ end
45
+
46
+ # Calls a single command
47
+ # @param [Hash] opts options
48
+ # @option opts [Array] :cmd command parts
49
+ # @option opts [Boolean] :multi true if multi-response is expected
50
+ # @option opts [Proc] :proc a proc to apply to the result
51
+ # @option opts [Array] :args arguments to pass to the :proc
52
+ def call(opts)
53
+ perform([opts])[0]
54
+ end
55
+
56
+ # Performs multiple commands
57
+ # @param [Array<Hash>] commands array of command options
58
+ # @see SSDB::Client#call for command format
59
+ def perform(commands)
60
+ message = ""
61
+
62
+ commands.each do |hash|
63
+ hash[:cmd].each do |c|
64
+ message << c.bytesize.to_s << NL << c << NL
65
+ end
66
+ message << NL
67
+ end
68
+
69
+ results = []
70
+ ensure_connected do
71
+ io(:write, message)
72
+
73
+ commands.each do |hash|
74
+ part = read_part(hash[:multi])
75
+ if hash[:proc]
76
+ args = [part]
77
+ args.concat(hash[:args]) if hash[:args]
78
+ part = hash[:proc].call(*args)
79
+ end
80
+ results << part
81
+ end
82
+ end
83
+
84
+ results
85
+ end
86
+
87
+ protected
88
+
89
+ # @return [TcpSocket] socket connection
90
+ def socket
91
+ @sock ||= connect
92
+ end
93
+
94
+ # Safely perform IO operation
95
+ def io(op, *args)
96
+ socket.__send__(op, *args)
97
+ rescue Errno::EAGAIN
98
+ raise SSDB::TimeoutError, "Connection timed out"
99
+ rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL => e
100
+ raise SSDB::ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
101
+ end
102
+
103
+ def ensure_connected
104
+ attempts = 0
105
+ begin
106
+ yield
107
+ rescue SSDB::ConnectionError
108
+ disconnect
109
+ retry if (attempts += 1) < 2
110
+ raise
111
+ rescue Exception
112
+ disconnect
113
+ raise
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ # "Inspired" by http://www.mikeperham.com/2009/03/15/socket-timeouts-in-ruby/
120
+ def connect
121
+ addr = Socket.getaddrinfo(url.host, nil)
122
+ sock = Socket.new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0)
123
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, sock_timeout
124
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, sock_timeout
125
+ sock.connect(Socket.pack_sockaddr_in(port, addr[0][3]))
126
+ sock
127
+ end
128
+
129
+ # Converts numeric `timeout` into a packed socket option value
130
+ def sock_timeout
131
+ @sock_timeout ||= begin
132
+ secs = Integer(timeout)
133
+ usecs = Integer((timeout - secs) * 1_000_000)
134
+ [secs, usecs].pack("l_2")
135
+ end
136
+ end
137
+
138
+ def read_len
139
+ len = io(:gets).chomp
140
+ len unless len.empty?
141
+ end
142
+
143
+ def read_part(multi)
144
+ read_len || return
145
+ status = io(:gets).chomp
146
+
147
+ case status
148
+ when OK
149
+ part = []
150
+ part << io(:gets).chomp while read_len
151
+ part.size > 1 || multi ? part : part[0]
152
+ when NOT_FOUND
153
+ multi ? [] : nil
154
+ else
155
+ raise SSDB::CommandError, "Server responded with '#{status}'"
156
+ end
157
+ end
158
+
159
+ # Parses `url`
160
+ def parse_url(url)
161
+ url = URI(url) if url.is_a?(String)
162
+
163
+ # Validate URL
164
+ unless url.host
165
+ raise ArgumentError, "Invalid :url option, unable to determine 'host'."
166
+ end
167
+
168
+ url
169
+ end
170
+
171
+ end
172
+
173
+ end
@@ -0,0 +1,30 @@
1
+ class SSDB::Future < ::BasicObject
2
+
3
+ def initialize(command)
4
+ @command = command
5
+ end
6
+
7
+ def inspect
8
+ "<SSDB::Future #{@command.inspect}>"
9
+ end
10
+
11
+ def value=(value)
12
+ @value = value
13
+ end
14
+
15
+ def value
16
+ unless defined?(@value)
17
+ ::Kernel.raise ::SSDB::FutureNotReady, "Value of #{@command.inspect} is not ready"
18
+ end
19
+ @value
20
+ end
21
+
22
+ def instance_of?(klass)
23
+ klass == ::SSDB::Future
24
+ end
25
+
26
+ def class
27
+ ::SSDB::Future
28
+ end
29
+
30
+ end
@@ -0,0 +1,3 @@
1
+ class SSDB
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,24 @@
1
+ require 'bundler/setup'
2
+ require 'rspec'
3
+ require 'ssdb'
4
+
5
+ # Fixture prefix
6
+ FPX = "ssdb:rb:spec"
7
+
8
+ RSpec.configure do |c|
9
+
10
+ c.after do
11
+ db = SSDB.current
12
+
13
+ # Remove keys
14
+ db.keys(FPX, FPX + "\xFF").each do |key|
15
+ db.del(key)
16
+ end
17
+
18
+ # Remove zsets
19
+ db.zlist(FPX, FPX + "\xFF").each do |key|
20
+ db.multi_zdel key, db.zkeys(key, 0, 1_000_000)
21
+ end
22
+ end
23
+
24
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe SSDB::Batch do
4
+
5
+ let :futures do
6
+ subject.instance_variable_get(:@futures)
7
+ end
8
+
9
+ let :calling do
10
+ -> { subject.call cmd: ["incr", "key", 2] }
11
+ end
12
+
13
+ it { should be_a(Array) }
14
+
15
+ describe "#call" do
16
+
17
+ it 'should store commands' do
18
+ calling.should change { subject.count }.by(1)
19
+ subject.last.should include(:cmd)
20
+ end
21
+
22
+ it 'should remember futures' do
23
+ calling.should change { futures.count }.by(1)
24
+ futures.last.should be_instance_of(SSDB::Future)
25
+ end
26
+
27
+ end
28
+
29
+ describe "applying values" do
30
+ before { 3.times { calling.call } }
31
+
32
+ it 'should set feature' do
33
+ subject.values = [2, 4, 6]
34
+ futures.map(&:value).should == [2, 4, 6]
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ describe SSDB::Client do
4
+
5
+ its(:url) { should be_instance_of(URI::Generic) }
6
+ its(:timeout) { should == 10.0 }
7
+ its(:id) { should == "ssdb://127.0.0.1:8888/" }
8
+ its(:port) { should == 8888 }
9
+ its(:reconnect) { should be(true) }
10
+
11
+ it "should not be connected by default" do
12
+ subject.should_not be_connected
13
+ end
14
+
15
+ it "can connect" do
16
+ -> { subject.send(:socket) }.should change { subject.connected? }.to(true)
17
+ end
18
+
19
+ it "can disconnect" do
20
+ -> { subject.disconnect }.should_not change { subject.connected? }
21
+ subject.send(:socket)
22
+ -> { subject.disconnect }.should change { subject.connected? }.to(false)
23
+ end
24
+
25
+ it "should perform commands" do
26
+ res = subject.perform [{ cmd: ["set", "#{FPX}:key", "VAL1"] }]
27
+ res.should == ["1"]
28
+ end
29
+
30
+ it "should perform commands in bulks" do
31
+ res = subject.perform [
32
+ { cmd: ["set", "#{FPX}:key", "VAL2"] },
33
+ { cmd: ["get", "#{FPX}:key"] }
34
+ ]
35
+ res.should == ["1", "VAL2"]
36
+ end
37
+
38
+ it "should perform complex command chains" do
39
+ res = subject.perform [
40
+ { cmd: ["zset", "#{FPX}:zset", "VAL1", "1"] },
41
+ { cmd: ["zset", "#{FPX}:zset", "VAL2", "2"] },
42
+ { cmd: ["zscan", "#{FPX}:zset", "*", "0", "5", "-1"], multi: true },
43
+ { cmd: ["zrscan", "#{FPX}:zset", "*", "5", "0", "-1"], multi: true, proc: ->r { r[1] = r[1].to_f; r } }
44
+ ]
45
+ res.should == ["1", "1", ["VAL1", "1", "VAL2", "2"], ["VAL2", 2, "VAL1", "1"]]
46
+ end
47
+
48
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ describe SSDB::Future do
4
+
5
+ subject do
6
+ described_class.new ["set", "key", "val"]
7
+ end
8
+
9
+ it { should be_instance_of(described_class) }
10
+
11
+ it "should be introspectable" do
12
+ subject.inspect.should == %(<SSDB::Future ["set", "key", "val"]>)
13
+ end
14
+
15
+ it "should raise error when not ready" do
16
+ -> { subject.value }.should raise_error(SSDB::FutureNotReady)
17
+ end
18
+
19
+ it "should return value when ready" do
20
+ subject.value = "ok"
21
+ subject.value.should == "ok"
22
+
23
+ subject.value = true
24
+ subject.value.should == true
25
+
26
+ subject.value = nil
27
+ subject.value.should == nil
28
+ end
29
+
30
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe SSDB do
4
+
5
+ describe "class" do
6
+ subject { described_class }
7
+
8
+ its(:current) { should be_instance_of(described_class) }
9
+ it { should respond_to(:current=) }
10
+ end
11
+
12
+ it 'should execute batches' do
13
+ res = subject.batch do
14
+ subject.set "#{FPX}:key", "100"
15
+ subject.get "#{FPX}:key"
16
+ subject.incr "#{FPX}:key", 10
17
+ subject.decr "#{FPX}:key", 30
18
+ end
19
+ res.should == [true, "100", 110, 80]
20
+ end
21
+
22
+ it 'should execute batches with futures' do
23
+ s = n = nil
24
+ subject.batch do
25
+ subject.set "#{FPX}:key", "100"
26
+ s = subject.get "#{FPX}:key"
27
+ n = subject.incr "#{FPX}:key", 10
28
+
29
+ -> { s.value }.should raise_error(SSDB::FutureNotReady)
30
+ end
31
+
32
+ s.should be_instance_of(SSDB::Future)
33
+ s.value.should == "100"
34
+ n.value.should == 110
35
+ end
36
+
37
+ end
@@ -0,0 +1,103 @@
1
+ require 'spec_helper'
2
+
3
+ describe SSDB do
4
+ describe "plain values" do
5
+
6
+ before do
7
+ subject.set "#{FPX}:key1", 1
8
+ subject.set "#{FPX}:key2", "2"
9
+ subject.set "#{FPX}:key3", "C"
10
+ subject.set "#{FPX}:key4", "d"
11
+ end
12
+
13
+ it "should check existence" do
14
+ subject.exists("#{FPX}:key1").should be(true)
15
+ subject.exists?("#{FPX}:key2").should be(true)
16
+ subject.exists?("#{FPX}:keyX").should be(false)
17
+ end
18
+
19
+ it "should delete" do
20
+ -> {
21
+ subject.del("#{FPX}:key1").should be_nil
22
+ }.should change { subject.exists?("#{FPX}:key1") }.to(false)
23
+
24
+ subject.del("#{FPX}:keyX").should be_nil
25
+ end
26
+
27
+ it "should list" do
28
+ subject.keys("#{FPX}:key1", "#{FPX}:key3").should == ["#{FPX}:key2", "#{FPX}:key3"]
29
+ subject.keys("#{FPX}:key0", "#{FPX}:keyz", limit: 2).should == ["#{FPX}:key1", "#{FPX}:key2"]
30
+ subject.keys("#{FPX}:keyy", "#{FPX}:keyz").should == []
31
+ subject.keys("#{FPX}:keyz", "#{FPX}:key0").should == []
32
+ end
33
+
34
+ it "should scan" do
35
+ subject.scan("#{FPX}:key1", "#{FPX}:key3").should == [["#{FPX}:key2", "2"], ["#{FPX}:key3", "C"]]
36
+ subject.scan("#{FPX}:key0", "#{FPX}:keyz", limit: 2).should == [["#{FPX}:key1", "1"], ["#{FPX}:key2", "2"]]
37
+ subject.scan("#{FPX}:keyy", "#{FPX}:keyz").should == []
38
+ subject.scan("#{FPX}:keyz", "#{FPX}:key0").should == []
39
+ end
40
+
41
+ it "should rscan" do
42
+ subject.rscan("#{FPX}:key3", "#{FPX}:key1").should == [["#{FPX}:key2", "2"], ["#{FPX}:key1", "1"]]
43
+ subject.rscan("#{FPX}:keyz", "#{FPX}:key0", limit: 2).should == [["#{FPX}:key4", "d"], ["#{FPX}:key3", "C"]]
44
+ subject.rscan("#{FPX}:keyz", "#{FPX}:keyy").should == []
45
+ subject.rscan("#{FPX}:key0", "#{FPX}:keyz").should == []
46
+ end
47
+
48
+ it "should set/get values" do
49
+ subject.set("#{FPX}:key", "a").should be(true)
50
+ subject.set("#{FPX}:key", "a").should be(true)
51
+ subject.set("#{FPX}:key", "b").should be(true)
52
+
53
+ subject.get("#{FPX}:key").should == "b"
54
+ subject.get("#{FPX}:keyX").should be_nil
55
+ end
56
+
57
+ it "should increment/decrement values" do
58
+ subject.incr("#{FPX}:key", 7).should == 7
59
+ subject.decr("#{FPX}:key", 2).should == 5
60
+ subject.incr("#{FPX}:key", 4).should == 9
61
+ subject.incr("#{FPX}:key", 3.9).should == 12
62
+ subject.decr("#{FPX}:key", 2.8).should == 10
63
+
64
+ subject.set "#{FPX}:keyN", "100"
65
+ subject.incr("#{FPX}:keyN", 2).should == 102
66
+
67
+ subject.set "#{FPX}:keyN", "100"
68
+ subject.decr("#{FPX}:keyN", 2).should == 98
69
+
70
+ subject.set "#{FPX}:keyN", "5.9"
71
+ subject.incr("#{FPX}:keyN", 3.1).should == 8
72
+
73
+ subject.set "#{FPX}:keyS", "a"
74
+ subject.incr("#{FPX}:keyS", 4).should == 4
75
+ subject.get("#{FPX}:keyS").should == "4"
76
+
77
+ subject.set "#{FPX}:keyS", "a"
78
+ subject.decr("#{FPX}:keyS", 4).should == -4
79
+ subject.get("#{FPX}:keyS").should == "-4"
80
+ end
81
+
82
+ it 'should multi-get/set' do
83
+ subject.multi_set("#{FPX}:key4" => "x", "#{FPX}:key8" => "y", "#{FPX}:key9" => "z").should == 6
84
+ subject.multi_get(["#{FPX}:key4", "#{FPX}:key6", "#{FPX}:key9", "#{FPX}:key8"]).
85
+ should == ["x", nil, "z", "y"]
86
+ end
87
+
88
+ it 'should check existence of multiple keys' do
89
+ subject.multi_exists(["#{FPX}:key2", "#{FPX}:key3"]).should == [true, true]
90
+ subject.multi_exists(["#{FPX}:key2", "#{FPX}:key9", "#{FPX}:key3"]).should == [true, false, true]
91
+ end
92
+
93
+ it 'should multi-delete' do
94
+ keys = ["#{FPX}:key2", "#{FPX}:key3", "#{FPX}:key9"]
95
+ -> {
96
+ subject.multi_del(keys).should == 0
97
+ }.should change {
98
+ subject.multi_exists(keys)
99
+ }.from([true, true, false]).to([false, false, false])
100
+ end
101
+
102
+ end
103
+ end