kt 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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