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