kt 0.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +1 -0
- data/.travis.yml +20 -0
- data/Gemfile +4 -0
- data/LICENSE +13 -0
- data/README.md +71 -0
- data/Rakefile +1 -0
- data/kt.gemspec +29 -0
- data/lib/kt.rb +362 -0
- data/lib/kt/errors.rb +10 -0
- data/lib/kt/kv.rb +10 -0
- data/lib/kt/version.rb +3 -0
- data/spec/kt_spec.rb +221 -0
- data/spec/spec_helper.rb +64 -0
- metadata +144 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e51d9769475943af5267c8d3e9cf7497bc4830dd
|
4
|
+
data.tar.gz: d9d0385cec3ed60994fe58af7796b946ae8ac4c7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 86663c41ca8c8a04c7db442d41a2fead8446a1d016f803ca379d62f9b110c20a4b209c113c1c907c40e47d665de812183f3ebf25616f70694ce1765107735eca
|
7
|
+
data.tar.gz: 26c05b1d767681bcdd70603d006b8ec9874560f5b9bdf34a2b315bdba875e1552af3043af2c09dda784000d620446759996c1a833a7815d2d64481e14a71c4c0
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.travis.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- "2.3.0"
|
4
|
+
- "2.2"
|
5
|
+
before_install:
|
6
|
+
- sudo apt-get update -qq
|
7
|
+
- sudo apt-get install zlib1g-dev curl liblzo2-dev liblua5.1-0-dev -qq
|
8
|
+
- gem install bundler
|
9
|
+
install:
|
10
|
+
# Cabinet
|
11
|
+
- pushd /tmp
|
12
|
+
- git clone https://github.com/alticelabs/kyoto.git
|
13
|
+
- pushd kyoto
|
14
|
+
- sudo make install --quiet
|
15
|
+
- popd
|
16
|
+
- popd
|
17
|
+
- sudo ldconfig
|
18
|
+
script:
|
19
|
+
- bundle install
|
20
|
+
- rspec spec
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright [2016] Kuende
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# kt-ruby
|
2
|
+
|
3
|
+
[](https://travis-ci.org/kuende/kt-ruby)
|
4
|
+
|
5
|
+
Ruby client for [Kyoto Tycoon](http://fallabs.com/kyototycoon/). It uses a connection pool to maintain multiple connections.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
|
10
|
+
Add this to your application's `Gemfile`:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'kt'
|
14
|
+
```
|
15
|
+
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
require "kt"
|
21
|
+
|
22
|
+
kt = KT.new(host: "127.0.0.1", port: 1978, poolsize: 5, timeout: 5.0)
|
23
|
+
|
24
|
+
# Setting
|
25
|
+
kt.set("japan", "tokyo") # set a key
|
26
|
+
kt.set("japan", "tokyo", expire: 60) # set a key with expire of 60 seconds
|
27
|
+
kt.set_bulk({"china" => "beijing", "france" => "paris", "uk" => "london"})
|
28
|
+
|
29
|
+
kt.get("japan") # => "tokyo"
|
30
|
+
kt.get_bulk(["japan", "france"]) # => {"japan" => "tokyo", "france" => "paris"}
|
31
|
+
kt.get("foo") # => nil
|
32
|
+
kt.get!("foo") # => raises KT::RecordNotFound
|
33
|
+
|
34
|
+
kt.remove("japan") # => true
|
35
|
+
kt.remove("japan") # => false, key japan is not found anymore
|
36
|
+
kt.remove!("japan") # => raises KT::RecordNotFound becouse key japan is not found
|
37
|
+
kt.remove_bulk(["japan", "china"]) # => 1 (number keys deleted)
|
38
|
+
|
39
|
+
kt.clear # deletes all records in the database
|
40
|
+
kt.vacuum # triggers forced garbage collection of expired records
|
41
|
+
|
42
|
+
kt.set_bulk({"user:1" => "1", "user:2" => "2", "user:4" => "4"})
|
43
|
+
kt.match_prefix("user:") # => ["user:1", "user:2", "user:3", "user:4", "user:5"]
|
44
|
+
|
45
|
+
# Compare and swap
|
46
|
+
kt.set("user:1", "1")
|
47
|
+
kt.cas("user:1", "1", "2") # => true
|
48
|
+
kt.cas("user:1", "1", "3") # => false, previous value is "2"
|
49
|
+
kt.cas("user:1", nil, "3") # => false, record already exists with value "2"
|
50
|
+
kt.cas("user:2", nil, "1") # => true, no record exists so it was set
|
51
|
+
kt.cas("user:1", "2", nil) # => true, record is removed becouse it was present
|
52
|
+
kt.cas("user:1", "2", nil) # => false, it fails becouse no record with this key exists
|
53
|
+
|
54
|
+
# cas! raises where cas returns false
|
55
|
+
kt.cas!("user:1", "1", "2") # => KT::CASFailed, no record exists with this value
|
56
|
+
|
57
|
+
kt.count # => 2 keys in database
|
58
|
+
```
|
59
|
+
|
60
|
+
### TODO
|
61
|
+
|
62
|
+
- [ ] implement expiration for most commands
|
63
|
+
- [ ] work with multiple servers
|
64
|
+
|
65
|
+
## Contributing
|
66
|
+
|
67
|
+
1. Fork it ( https://github.com/kuende/kt-ruby/fork )
|
68
|
+
2. Create your feature branch (git checkout -b my-new-feature)
|
69
|
+
3. Commit your changes (git commit -am 'Add some feature')
|
70
|
+
4. Push to the branch (git push origin my-new-feature)
|
71
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/kt.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'kt/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "kt"
|
8
|
+
spec.version = KT::VERSION
|
9
|
+
spec.authors = ["Teodor Pripoae"]
|
10
|
+
spec.email = ["toni@kuende.com"]
|
11
|
+
spec.description = %q{Kyoto Tycoon client}
|
12
|
+
spec.summary = %q{Kyoto Tycoon client for ruby. For more information see the Readme on github}
|
13
|
+
spec.homepage = "https://github.com/kuende/kt-ruby"
|
14
|
+
spec.license = "Apache-2.0"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = []
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "excon", "~> 0.51.0"
|
22
|
+
spec.add_dependency "connection_pool", "~> 2.2"
|
23
|
+
|
24
|
+
# testing
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
26
|
+
spec.add_development_dependency "rspec", "~> 3.4"
|
27
|
+
spec.add_development_dependency "rspec-eventually", "~> 0.1"
|
28
|
+
spec.add_development_dependency "pry", "~> 0.10"
|
29
|
+
end
|
data/lib/kt.rb
ADDED
@@ -0,0 +1,362 @@
|
|
1
|
+
require "excon"
|
2
|
+
require "connection_pool"
|
3
|
+
require "base64"
|
4
|
+
require "kt/errors"
|
5
|
+
require "kt/kv"
|
6
|
+
require "kt/version"
|
7
|
+
|
8
|
+
class KT
|
9
|
+
IDENTITY_ENCODING = "text/tab-separated-values"
|
10
|
+
BASE64_ENCODING = "text/tab-separated-values; colenc=B"
|
11
|
+
|
12
|
+
IDENTITY_HEADERS = {"Content-Type" => IDENTITY_ENCODING}
|
13
|
+
BASE64_HEADERS = {"Content-Type" => BASE64_ENCODING}
|
14
|
+
EMPTY_HEADERS = {}
|
15
|
+
|
16
|
+
def initialize(options)
|
17
|
+
@host = options.fetch(:host, "127.0.0.1")
|
18
|
+
@port = options.fetch(:port, 1978)
|
19
|
+
@poolsize = options.fetch(:poolsize, 5)
|
20
|
+
@timeout = options.fetch(:timeout, 5.0)
|
21
|
+
|
22
|
+
@pool = ConnectionPool.new(size: @poolsize, timeout: @timeout) do
|
23
|
+
Excon.new("http://#{@host}:#{@port}")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# count returns the number of records in the database
|
28
|
+
def count
|
29
|
+
status, m = do_rpc("/rpc/status")
|
30
|
+
|
31
|
+
if status != 200
|
32
|
+
raise_error(m)
|
33
|
+
end
|
34
|
+
|
35
|
+
find_rec(m, "count").value.to_i
|
36
|
+
end
|
37
|
+
|
38
|
+
# clear removes all records in the database
|
39
|
+
def clear
|
40
|
+
status, m = do_rpc("/rpc/clear")
|
41
|
+
|
42
|
+
if status != 200
|
43
|
+
raise_error(m)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# vacuum triggers garbage collection of expired records
|
48
|
+
def vacuum
|
49
|
+
status, m = do_rpc("/rpc/vacuum")
|
50
|
+
|
51
|
+
if status != 200
|
52
|
+
raise_error(m)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# get retrieves the data stored at key.
|
57
|
+
# It returns nil if no such data is found
|
58
|
+
def get(key)
|
59
|
+
status, body = do_rest("GET", key, nil)
|
60
|
+
|
61
|
+
case status
|
62
|
+
when 200
|
63
|
+
body
|
64
|
+
when 404
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# get! retrieves the data stored at key.
|
70
|
+
# KT::RecordNotFound is raised if not such data is found
|
71
|
+
def get!(key)
|
72
|
+
value = get(key)
|
73
|
+
if value != nil
|
74
|
+
value
|
75
|
+
else
|
76
|
+
raise KT::RecordNotFound.new("Key: #{key} not found")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# get_bulk retrieves the keys in the list
|
81
|
+
# It returns a hash of key => value.
|
82
|
+
# If a key was not found in the database, the value in return hash will be nil
|
83
|
+
def get_bulk(keys)
|
84
|
+
req = keys.map do |key|
|
85
|
+
KT::KV.new("_#{key}", "")
|
86
|
+
end
|
87
|
+
|
88
|
+
status, res_body = do_rpc("/rpc/get_bulk", req)
|
89
|
+
|
90
|
+
if status != 200
|
91
|
+
raise_error(res_body)
|
92
|
+
end
|
93
|
+
|
94
|
+
res = {}
|
95
|
+
|
96
|
+
res_body.each do |kv|
|
97
|
+
if kv.key.start_with?('_')
|
98
|
+
res[kv.key[1, kv.key.size - 1]] = kv.value
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
return res
|
103
|
+
end
|
104
|
+
|
105
|
+
# set stores the data at key
|
106
|
+
def set(key, value, expire: nil)
|
107
|
+
req = [
|
108
|
+
KT::KV.new("key", key),
|
109
|
+
KT::KV.new("value", value),
|
110
|
+
]
|
111
|
+
|
112
|
+
if expire
|
113
|
+
req << KT::KV.new("xt", expire.to_s)
|
114
|
+
end
|
115
|
+
|
116
|
+
status, body = do_rpc("/rpc/set", req)
|
117
|
+
|
118
|
+
if status != 200
|
119
|
+
raise_error(body)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# set_bulk sets multiple keys to multiple values
|
124
|
+
def set_bulk(values)
|
125
|
+
req = values.map do |key, value|
|
126
|
+
KT::KV.new("_#{key}", value)
|
127
|
+
end
|
128
|
+
|
129
|
+
status, body = do_rpc("/rpc/set_bulk", req)
|
130
|
+
|
131
|
+
if status != 200
|
132
|
+
raise_error(body)
|
133
|
+
end
|
134
|
+
|
135
|
+
find_rec(body, "num").value.to_i
|
136
|
+
end
|
137
|
+
|
138
|
+
# remove deletes the data at key in the database.
|
139
|
+
def remove(key)
|
140
|
+
status, body = do_rest("DELETE", key, nil)
|
141
|
+
|
142
|
+
if status == 404
|
143
|
+
return false
|
144
|
+
end
|
145
|
+
|
146
|
+
if status != 204
|
147
|
+
raise KT::Error.new(body)
|
148
|
+
end
|
149
|
+
|
150
|
+
return true
|
151
|
+
end
|
152
|
+
|
153
|
+
# remove! deletes the data at key in the database
|
154
|
+
# it raises KT::RecordNotFound if key was not found
|
155
|
+
def remove!(key)
|
156
|
+
unless remove(key)
|
157
|
+
raise KT::RecordNotFound.new("key #{key} was not found")
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# remove_bulk deletes multiple keys.
|
162
|
+
# it returnes the number of keys deleted
|
163
|
+
def remove_bulk(keys)
|
164
|
+
req = keys.map do |key|
|
165
|
+
KV.new("_#{key}", "")
|
166
|
+
end
|
167
|
+
|
168
|
+
status, body = do_rpc("/rpc/remove_bulk", req)
|
169
|
+
|
170
|
+
if status != 200
|
171
|
+
raise_error(body)
|
172
|
+
end
|
173
|
+
|
174
|
+
find_rec(body, "num").value.to_i
|
175
|
+
end
|
176
|
+
|
177
|
+
# match_prefix performs the match_prefix operation against the server
|
178
|
+
# It returns a sorted list of keys.
|
179
|
+
# max_records defines the number of results to be returned
|
180
|
+
# if negative, it means unlimited
|
181
|
+
def match_prefix(prefix, max_records = -1)
|
182
|
+
req = [
|
183
|
+
KT::KV.new("prefix", prefix),
|
184
|
+
KT::KV.new("max", max_records.to_s)
|
185
|
+
]
|
186
|
+
|
187
|
+
status, body = do_rpc("/rpc/match_prefix", req)
|
188
|
+
|
189
|
+
if status != 200
|
190
|
+
raise_error(body)
|
191
|
+
end
|
192
|
+
|
193
|
+
res = []
|
194
|
+
|
195
|
+
body.each do |kv|
|
196
|
+
if kv.key.start_with?('_')
|
197
|
+
res << kv.key[1, kv.key.size - 1]
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
return res
|
202
|
+
end
|
203
|
+
|
204
|
+
# cas executes a compare and swap operation
|
205
|
+
# if both old and new provided it sets to new value if previous value is old value
|
206
|
+
# if no old value provided it will set to new value if key is not present in db
|
207
|
+
# if no new value provided it will remove the record if it exists
|
208
|
+
# it returns true if it succeded or false otherwise
|
209
|
+
def cas(key, oval = nil, nval = nil)
|
210
|
+
req = [KT::KV.new("key", key)]
|
211
|
+
if oval != nil
|
212
|
+
req << KT::KV.new("oval", oval)
|
213
|
+
end
|
214
|
+
if nval != nil
|
215
|
+
req << KT::KV.new("nval", nval)
|
216
|
+
end
|
217
|
+
|
218
|
+
status, body = do_rpc("/rpc/cas", req)
|
219
|
+
|
220
|
+
if status == 450
|
221
|
+
return false
|
222
|
+
end
|
223
|
+
|
224
|
+
if status != 200
|
225
|
+
raise_error(body)
|
226
|
+
end
|
227
|
+
|
228
|
+
return true
|
229
|
+
end
|
230
|
+
|
231
|
+
# cas! works the same as cas but it raises error on failure
|
232
|
+
def cas!(key, oval = nil, nval = nil)
|
233
|
+
if !cas(key, oval, nval)
|
234
|
+
raise KT::CASFailed.new("Failed compare and swap for #{key}")
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
private
|
239
|
+
|
240
|
+
def do_rpc(path, values=nil)
|
241
|
+
body, encoding = encode_values(values)
|
242
|
+
headers = {"Content-Type" => encoding}
|
243
|
+
|
244
|
+
@pool.with do |conn|
|
245
|
+
res = conn.post(:path => path, :headers => headers, :body => body)
|
246
|
+
# return res.status_code, decode_values(res.body, res.headers.get("Content-Type").join("; "))
|
247
|
+
return res.status, decode_values(res.body, res.headers["Content-Type"])
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def do_rest(method, key, value="")
|
252
|
+
@pool.with do |conn|
|
253
|
+
res = conn.request(:method => method, :path => url_encode(key), :headers => EMPTY_HEADERS, :body => value)
|
254
|
+
return res.status, res.body
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def find_rec(kv_list, key)
|
259
|
+
kv_list.each do |kv|
|
260
|
+
if kv.key == key
|
261
|
+
return kv
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
KV.new("", "")
|
266
|
+
end
|
267
|
+
|
268
|
+
def raise_error(body)
|
269
|
+
kv = find_rec(body, "ERROR")
|
270
|
+
if kv == ""
|
271
|
+
raise KT::Error.new("unknown error")
|
272
|
+
end
|
273
|
+
|
274
|
+
raise KT::Error.new("#{kv.value}")
|
275
|
+
end
|
276
|
+
|
277
|
+
def decode_values(body, content_type)
|
278
|
+
# Ideally, we should parse the mime media type here,
|
279
|
+
# but this is an expensive operation because mime is just
|
280
|
+
# that awful.
|
281
|
+
#
|
282
|
+
# KT responses are pretty simple and we can rely
|
283
|
+
# on it putting the parameter of colenc=[BU] at
|
284
|
+
# the end of the string. Just look for B, U or s
|
285
|
+
# (last character of tab-separated-values)
|
286
|
+
# to figure out which field encoding is used.
|
287
|
+
|
288
|
+
case content_type.chars.last
|
289
|
+
when 'B'
|
290
|
+
# base64 decode
|
291
|
+
method = :base64_decode
|
292
|
+
when 'U'
|
293
|
+
# url decode
|
294
|
+
method = :url_decode
|
295
|
+
when 's'
|
296
|
+
# identity decode
|
297
|
+
method = :identity_decode
|
298
|
+
else
|
299
|
+
raise "kt responded with unknown content-type: #{content_type}"
|
300
|
+
end
|
301
|
+
|
302
|
+
# Because of the encoding, we can tell how many records there
|
303
|
+
# are by scanning through the input and counting the \n's
|
304
|
+
kv = body.each_line.map do |line|
|
305
|
+
key, value = line.chomp.split("\t")
|
306
|
+
KT::KV.new(send(method, key), send(method, value))
|
307
|
+
end.to_a
|
308
|
+
|
309
|
+
kv.to_a
|
310
|
+
end
|
311
|
+
|
312
|
+
def encode_values(kv_list)
|
313
|
+
if kv_list.nil?
|
314
|
+
return "", IDENTITY_ENCODING
|
315
|
+
end
|
316
|
+
|
317
|
+
has_binary = kv_list.any? do |kv|
|
318
|
+
has_binary?(kv.key) || has_binary?(kv.value)
|
319
|
+
end
|
320
|
+
|
321
|
+
str = StringIO.new
|
322
|
+
|
323
|
+
kv_list.each do |kv|
|
324
|
+
if has_binary
|
325
|
+
str << Base64.strict_encode64(kv.key)
|
326
|
+
str << "\t"
|
327
|
+
str << Base64.strict_encode64(kv.value)
|
328
|
+
else
|
329
|
+
str << kv.key
|
330
|
+
str << "\t"
|
331
|
+
str << kv.value
|
332
|
+
end
|
333
|
+
str << "\n"
|
334
|
+
end
|
335
|
+
|
336
|
+
encoding = has_binary ? BASE64_ENCODING : IDENTITY_ENCODING
|
337
|
+
|
338
|
+
return str.string, encoding
|
339
|
+
end
|
340
|
+
|
341
|
+
def identity_decode(value)
|
342
|
+
value.force_encoding("utf-8")
|
343
|
+
end
|
344
|
+
|
345
|
+
def base64_decode(value)
|
346
|
+
Base64.strict_decode64(value).force_encoding("utf-8")
|
347
|
+
end
|
348
|
+
|
349
|
+
def url_decode(value)
|
350
|
+
URI.unescape(value).force_encoding("utf-8")
|
351
|
+
end
|
352
|
+
|
353
|
+
def url_encode(key)
|
354
|
+
"/" + URI.escape(key).gsub("/", "%2F")
|
355
|
+
end
|
356
|
+
|
357
|
+
def has_binary?(value)
|
358
|
+
value.bytes.any? do |c|
|
359
|
+
c < 0x20 || c > 0x7e
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
data/lib/kt/errors.rb
ADDED
data/lib/kt/kv.rb
ADDED
data/lib/kt/version.rb
ADDED
data/spec/kt_spec.rb
ADDED
@@ -0,0 +1,221 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
describe KT do
|
6
|
+
before :all do
|
7
|
+
@kt = KT.new(host: HOST, port: PORT, poolsize: 5, timeout: 5.0)
|
8
|
+
end
|
9
|
+
|
10
|
+
after :all do
|
11
|
+
@kt.clear
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "count" do
|
15
|
+
it "returns 0 by default" do
|
16
|
+
expect(@kt.count).to eql(0)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "returns 2 after some keys were inserted" do
|
20
|
+
@kt.set("japan", "tokyo")
|
21
|
+
@kt.set("china", "beijing")
|
22
|
+
|
23
|
+
expect(@kt.count).to eql(2)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "get/set/remove" do
|
28
|
+
it "sets a few keys then it gets them" do
|
29
|
+
["a", "b", "c"].each do |k|
|
30
|
+
@kt.set(k, k + "aaa")
|
31
|
+
expect(@kt.get(k)).to eql(k + "aaa")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it "removes a key" do
|
36
|
+
@kt.set("to/be/removed", "42")
|
37
|
+
@kt.remove("to/be/removed")
|
38
|
+
expect(@kt.get("to/be/removed")).to eql(nil)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "get returns nil if not found" do
|
42
|
+
expect(@kt.get("not/existing")).to eql(nil)
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "get!" do
|
46
|
+
it "returns a string if existing" do
|
47
|
+
@kt.set("foo", "bar")
|
48
|
+
expect(@kt.get("foo")).to eql("bar")
|
49
|
+
end
|
50
|
+
|
51
|
+
it "raises error if not found" do
|
52
|
+
expect {
|
53
|
+
@kt.get!("not/existing")
|
54
|
+
}.to raise_error(KT::RecordNotFound)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "remove" do
|
59
|
+
it "returns true if key was deleted" do
|
60
|
+
@kt.set("foo", "bar")
|
61
|
+
expect(@kt.remove("foo")).to eql(true)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "returns false if key was not found" do
|
65
|
+
expect(@kt.remove("not/existing")).to eql(false)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "remove!" do
|
70
|
+
it "returns nothing if key was deleted" do
|
71
|
+
@kt.set("foo", "bar")
|
72
|
+
@kt.remove("foo")
|
73
|
+
expect(@kt.get("foo")).to eql(nil)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "raises error if not found" do
|
77
|
+
expect {
|
78
|
+
@kt.remove!("not/existing")
|
79
|
+
}.to raise_error(KT::RecordNotFound)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe "set" do
|
84
|
+
it "accepts expire" do
|
85
|
+
@kt.set("expirekey", "expiredvalue", expire: 1)
|
86
|
+
expect(@kt.get("expirekey")).to eql("expiredvalue")
|
87
|
+
@kt.vacuum # trigger garbage collection
|
88
|
+
|
89
|
+
expect {
|
90
|
+
@kt.get("expirekey")
|
91
|
+
}.to eventually(eq(nil)).within(5)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe "bulk" do
|
97
|
+
it "returns nil hash for not found keys" do
|
98
|
+
expect(@kt.get_bulk(["foo1", "foo2", "foo3"])).to eql({})
|
99
|
+
end
|
100
|
+
|
101
|
+
it "returns hash with key value" do
|
102
|
+
expected = {
|
103
|
+
"cache/news/1" => "1",
|
104
|
+
"cache/news/2" => "2",
|
105
|
+
"cache/news/3" => "3",
|
106
|
+
"cache/news/4" => "4",
|
107
|
+
"cache/news/5" => "5",
|
108
|
+
"cache/news/6" => "6"
|
109
|
+
}
|
110
|
+
expected.each do |k, v|
|
111
|
+
@kt.set(k, v)
|
112
|
+
end
|
113
|
+
|
114
|
+
expect(@kt.get_bulk(expected.keys)).to eql(expected)
|
115
|
+
end
|
116
|
+
|
117
|
+
it "returns hash with found elements" do
|
118
|
+
@kt.set("foo4", "4")
|
119
|
+
@kt.set("foo5", "5")
|
120
|
+
|
121
|
+
expect(@kt.get_bulk(["foo4", "foo5", "foo6"])).to eql({"foo4" => "4", "foo5" => "5"})
|
122
|
+
end
|
123
|
+
|
124
|
+
it "set_bulk sets multiple keys" do
|
125
|
+
@kt.set_bulk({"foo7" => "7", "foo8" => "8", "foo9" => "9"})
|
126
|
+
expect(@kt.get_bulk(["foo7", "foo8", "foo9"])).to eql({"foo7" => "7", "foo8" => "8", "foo9" => "9"})
|
127
|
+
end
|
128
|
+
|
129
|
+
it "remove_bulk deletes bulk items" do
|
130
|
+
@kt.set_bulk({"foo7" => "7", "foo8" => "8", "foo9" => "9"})
|
131
|
+
@kt.remove_bulk(["foo7", "foo8", "foo9"])
|
132
|
+
expect(@kt.get_bulk(["foo7", "foo8", "foo9"])).to eql({})
|
133
|
+
end
|
134
|
+
|
135
|
+
it "returns the number of keys deleted" do
|
136
|
+
@kt.set_bulk({"foo7" => "7", "foo8" => "8", "foo9" => "9"})
|
137
|
+
expect(@kt.remove_bulk(["foo7", "foo8", "foo9", "foo1000"])).to eql(3)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe "match_prefix" do
|
142
|
+
it "returns nothing for not found prefix" do
|
143
|
+
expect(@kt.match_prefix("user:", 100)).to eql([])
|
144
|
+
end
|
145
|
+
|
146
|
+
it "returns correct results sorted" do
|
147
|
+
@kt.set_bulk({"user:1" => "1", "user:2" => "2", "user:4" => "4"})
|
148
|
+
@kt.set_bulk({"user:3" => "3", "user:5" => "5"})
|
149
|
+
@kt.set_bulk({"usera" => "aaa", "users:bbb" => "bbb"})
|
150
|
+
|
151
|
+
expect(@kt.match_prefix("user:")).to eql(["user:1", "user:2", "user:3", "user:4", "user:5"])
|
152
|
+
# It returns the results in random order
|
153
|
+
expect(@kt.match_prefix("user:", 2).size).to eql(2)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
describe "clear" do
|
158
|
+
it "clears the database" do
|
159
|
+
expect(@kt.count).to_not eql(0)
|
160
|
+
@kt.clear
|
161
|
+
expect(@kt.count).to eql(0)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe "cas" do
|
166
|
+
describe "with old and new" do
|
167
|
+
it "sets new value if old value is correct and returns true" do
|
168
|
+
@kt.set("cas:1", "1")
|
169
|
+
expect(@kt.cas("cas:1", "1", "2")).to eql(true)
|
170
|
+
expect(@kt.get("cas:1")).to eql("2")
|
171
|
+
end
|
172
|
+
|
173
|
+
it "returns false if old value is not equal" do
|
174
|
+
@kt.set("cas:2", "3")
|
175
|
+
expect(@kt.cas("cas:2", "1", "2")).to eql(false)
|
176
|
+
expect(@kt.get("cas:2")).to eql("3")
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
describe "without old value" do
|
181
|
+
it "sets the value if no record exists in db and returns true" do
|
182
|
+
expect(@kt.cas("cas:3", nil, "5")).to eql(true)
|
183
|
+
expect(@kt.get("cas:3")).to eql("5")
|
184
|
+
end
|
185
|
+
|
186
|
+
it "returns false if record exists in db" do
|
187
|
+
@kt.set("cas:4", "2")
|
188
|
+
expect(@kt.cas("cas:4", nil, "5")).to eql(false)
|
189
|
+
expect(@kt.get("cas:4")).to eql("2")
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
describe "without new value" do
|
194
|
+
it "removes record if it exists in db and returns true" do
|
195
|
+
@kt.set("cas:5", "1")
|
196
|
+
expect(@kt.cas("cas:5", "1", nil)).to eql(true)
|
197
|
+
expect(@kt.get("cas:5")).to eql(nil)
|
198
|
+
end
|
199
|
+
|
200
|
+
it "returns false if no record exists in db" do
|
201
|
+
expect(@kt.cas("cas:6", "1", nil)).to eql(false)
|
202
|
+
expect(@kt.get("cas:6")).to eql(nil)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
describe "binary" do
|
208
|
+
it "sets binary and gets it" do
|
209
|
+
@kt.set_bulk({"Café" => "foo"})
|
210
|
+
expect(@kt.get("Café")).to eql("foo")
|
211
|
+
|
212
|
+
@kt.set_bulk({"foo" => "Café"})
|
213
|
+
expect(@kt.get_bulk(["foo"])).to eql({"foo" => "Café"})
|
214
|
+
end
|
215
|
+
|
216
|
+
it "sets string using newlines and gets it" do
|
217
|
+
@kt.set_bulk({"foo" => "my\n\ttest"})
|
218
|
+
expect(@kt.get_bulk(["foo"])).to eql({"foo" => "my\n\ttest"})
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "bundler/setup"
|
3
|
+
|
4
|
+
require "kt"
|
5
|
+
require "rspec/eventually"
|
6
|
+
require "pry"
|
7
|
+
require "open3"
|
8
|
+
|
9
|
+
HOST = "127.0.0.1"
|
10
|
+
PORT = 1979
|
11
|
+
|
12
|
+
def start_server(host, port)
|
13
|
+
if server_connected?(host, port)
|
14
|
+
raise "Server already running on port #{port}"
|
15
|
+
end
|
16
|
+
|
17
|
+
args = ["ktserver", "-host", host, "-port", port.to_s]
|
18
|
+
stdin, stdout, stderr, wait_thr = Open3.popen3(*args)
|
19
|
+
|
20
|
+
50.times do
|
21
|
+
if server_connected?(host, port)
|
22
|
+
return wait_thr
|
23
|
+
end
|
24
|
+
sleep 0.05
|
25
|
+
end
|
26
|
+
|
27
|
+
raise "Server failed to start on port #{port}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop_server(wait_thr)
|
31
|
+
Process.kill("KILL", wait_thr.pid)
|
32
|
+
end
|
33
|
+
|
34
|
+
def server_connected?(host, port)
|
35
|
+
begin
|
36
|
+
socket = TCPSocket.new(host, port)
|
37
|
+
# puts "Server connected"
|
38
|
+
return true
|
39
|
+
rescue => e
|
40
|
+
# puts "Server failed: #{e}"
|
41
|
+
ensure
|
42
|
+
socket.close unless socket.nil?
|
43
|
+
end
|
44
|
+
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
RSpec.configure do |config|
|
49
|
+
config.before(:suite) do
|
50
|
+
@server_thr = start_server(HOST, PORT)
|
51
|
+
unless @server_thr
|
52
|
+
raise "Failed to connect to server"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
config.after(:suite) do
|
57
|
+
if @server_thr
|
58
|
+
stop_server(@server_thr)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
config.filter_run focus: true
|
63
|
+
config.run_all_when_everything_filtered = true
|
64
|
+
end
|
metadata
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kt
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Teodor Pripoae
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-07-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: excon
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.51.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.51.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: connection_pool
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.3'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.4'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.4'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec-eventually
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.1'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.1'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.10'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.10'
|
97
|
+
description: Kyoto Tycoon client
|
98
|
+
email:
|
99
|
+
- toni@kuende.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- ".rspec"
|
106
|
+
- ".travis.yml"
|
107
|
+
- Gemfile
|
108
|
+
- LICENSE
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- kt.gemspec
|
112
|
+
- lib/kt.rb
|
113
|
+
- lib/kt/errors.rb
|
114
|
+
- lib/kt/kv.rb
|
115
|
+
- lib/kt/version.rb
|
116
|
+
- spec/kt_spec.rb
|
117
|
+
- spec/spec_helper.rb
|
118
|
+
homepage: https://github.com/kuende/kt-ruby
|
119
|
+
licenses:
|
120
|
+
- Apache-2.0
|
121
|
+
metadata: {}
|
122
|
+
post_install_message:
|
123
|
+
rdoc_options: []
|
124
|
+
require_paths:
|
125
|
+
- lib
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
requirements: []
|
137
|
+
rubyforge_project:
|
138
|
+
rubygems_version: 2.5.1
|
139
|
+
signing_key:
|
140
|
+
specification_version: 4
|
141
|
+
summary: Kyoto Tycoon client for ruby. For more information see the Readme on github
|
142
|
+
test_files:
|
143
|
+
- spec/kt_spec.rb
|
144
|
+
- spec/spec_helper.rb
|