ssdb 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/.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
|