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.
@@ -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
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+
19
+ Gemfile.lock
20
+
21
+ *.swp
22
+ *.swo
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in kt.gemspec
4
+ gemspec
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.
@@ -0,0 +1,71 @@
1
+ # kt-ruby
2
+
3
+ [![Build Status](https://travis-ci.org/kuende/kt-ruby.svg)](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
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -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
@@ -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
@@ -0,0 +1,10 @@
1
+ class KT
2
+ class RecordNotFound < StandardError
3
+ end
4
+
5
+ class Error < StandardError
6
+ end
7
+
8
+ class CASFailed < StandardError
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ class KT
2
+ class KV
3
+ attr_accessor :key, :value
4
+
5
+ def initialize(key, value)
6
+ @key = key
7
+ @value = value
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ class KT
2
+ VERSION = "0.1.1"
3
+ end
@@ -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
@@ -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