ssdb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +31 -0
- data/README.md +117 -0
- data/Rakefile +10 -0
- data/lib/ssdb.rb +431 -0
- data/lib/ssdb/batch.rb +27 -0
- data/lib/ssdb/client.rb +173 -0
- data/lib/ssdb/future.rb +30 -0
- data/lib/ssdb/version.rb +3 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/ssdb/batch_spec.rb +38 -0
- data/spec/ssdb/client_spec.rb +48 -0
- data/spec/ssdb/future_spec.rb +30 -0
- data/spec/ssdb_spec.rb +37 -0
- data/spec/types/values_spec.rb +103 -0
- data/spec/types/zsets_spec.rb +116 -0
- data/ssdb.gemspec +25 -0
- metadata +151 -0
data/lib/ssdb/batch.rb
ADDED
@@ -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
|
data/lib/ssdb/client.rb
ADDED
@@ -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
|
data/lib/ssdb/future.rb
ADDED
@@ -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
|
data/lib/ssdb/version.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -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
|
data/spec/ssdb_spec.rb
ADDED
@@ -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
|