genki-kyototycoon 0.6.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,74 @@
1
+ # -- coding: utf-8
2
+
3
+ =begin
4
+ $ cat foo.rb
5
+ require "rubygems"
6
+ require "kyototycoon"
7
+
8
+ KyotoTycoon::Stream.run($stdin) do |line|
9
+ ... do some stuff ..
10
+ end
11
+
12
+ $ ktremotemgr slave -uw | ruby foo.rb
13
+ =end
14
+
15
+ class KyotoTycoon
16
+ module Stream
17
+ def self.run(io=$stdin, &block)
18
+ io.each_line{|line|
19
+ line = Line.new(*line.strip.split("\t", 5))
20
+ block.call(line)
21
+ }
22
+ end
23
+
24
+ class Line < Struct.new(:ts, :sid, :db, :cmd, :raw_args)
25
+ def args
26
+ @args ||= begin
27
+ return [] if raw_args.nil?
28
+ k,v = *raw_args.split("\t").map{|v| v.unpack('m').first}
29
+ return [k] unless v
30
+ xt = 0
31
+ v.unpack('C5').each{|num|
32
+ xt = (xt << 8) + num
33
+ }
34
+ v = v[5, v.length]
35
+ [k, v, xt]
36
+ end
37
+ end
38
+
39
+ def key
40
+ @key ||= begin
41
+ args.first || nil
42
+ end
43
+ end
44
+
45
+ def value
46
+ @value ||= begin
47
+ args[1] || nil
48
+ end
49
+ end
50
+
51
+ def xt
52
+ @xt ||= begin
53
+ args[2] || nil
54
+ end
55
+ end
56
+
57
+ def xt_time
58
+ @xt_time ||= begin
59
+ if args[2]
60
+ # if not set xt:
61
+ # Time.at(1099511627775) # => 36812-02-20 09:36:15 +0900
62
+ Time.at(args[2].to_i)
63
+ else
64
+ Time.at(0)
65
+ end
66
+ end
67
+ end
68
+
69
+ def time
70
+ @time ||= Time.at(*[ts[0,10], ts[10, ts.length]].map(&:to_i))
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,57 @@
1
+ # -- coding: utf-8
2
+
3
+ class KyotoTycoon
4
+ module Tsvrpc
5
+ class Skinny
6
+ def initialize(host, port)
7
+ @host = host
8
+ @port = port
9
+ @tpl = ""
10
+ @tpl << "POST %s HTTP/1.1\r\n"
11
+ @tpl << "Content-Length: %d\r\n"
12
+ @tpl << "Content-Type: text/tab-separated-values; colenc=%s\r\n"
13
+ @tpl << "\r\n%s"
14
+ at_exit { finish }
15
+ end
16
+
17
+ def request(path, params, colenc)
18
+ start
19
+ query = KyotoTycoon::Tsvrpc.build_query(params, colenc)
20
+ request = @tpl % [path, query.bytesize, colenc, query]
21
+ @sock.write(request)
22
+ first_line = @sock.gets
23
+ status = first_line[9, 3]
24
+ bodylen = 0
25
+ body = ""
26
+ colenc = nil
27
+ loop do
28
+ line = @sock.gets
29
+ if line['Content-Type'] && line['colenc=']
30
+ colenc = line.match(/colenc=([A-Z])/).to_a[1]
31
+ next
32
+ end
33
+
34
+ if line['Content-Length']
35
+ bodylen = line.match(/[0-9]+/)[0].to_i
36
+ next
37
+ end
38
+
39
+ if line == "\r\n"
40
+ break
41
+ end
42
+ end
43
+ body = @sock.read(bodylen)
44
+ [status.to_i, body, colenc]
45
+ end
46
+
47
+ def start
48
+ @sock = nil if @sock && @sock.closed?
49
+ @sock ||= ::TCPSocket.new(@host, @port)
50
+ end
51
+
52
+ def finish
53
+ @sock.close if @sock && !@sock.closed?
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,45 @@
1
+ # -- coding: utf-8
2
+
3
+
4
+ class KyotoTycoon
5
+ module Tsvrpc
6
+ def self.parse(body, colenc)
7
+ decoder = case colenc
8
+ when "U"
9
+ lambda{|body| CGI.unescape(body)}
10
+ when "B"
11
+ lambda{|body| Base64.decode64(body)}
12
+ when nil
13
+ lambda{|body| body}
14
+ else
15
+ raise "Unknown colenc(response) '#{colenc}'"
16
+ end
17
+ body.split("\n").inject({}){|r, line|
18
+ k,v = *line.split("\t", 2).map{|v| decoder.call(v)}
19
+ r[k] = v
20
+ r
21
+ }
22
+ end
23
+
24
+ def self.build_query(params, colenc='U')
25
+ query = ""
26
+ if params
27
+ encoder = case colenc.to_s.upcase.to_sym
28
+ when :U
29
+ lambda{|body| CGI.escape(body.to_s)}
30
+ when :B
31
+ lambda{|body| [body.to_s].pack('m').gsub("\n","")}
32
+ else
33
+ raise "Unknown colenc '#{colenc}'"
34
+ end
35
+ query = params.inject([]){|r, tmp|
36
+ unless tmp.last.nil?
37
+ r << tmp.map{|v| encoder.call(v)}.join("\t")
38
+ end
39
+ r
40
+ }.join("\r\n")
41
+ end
42
+ query
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,305 @@
1
+ # -- coding: utf-8
2
+
3
+ require "logger"
4
+ require "cgi"
5
+ require "socket"
6
+ require "base64"
7
+ require "timeout"
8
+ require "kyototycoon/cursor.rb"
9
+ require "kyototycoon/serializer.rb"
10
+ require "kyototycoon/serializer/default.rb"
11
+ require "kyototycoon/serializer/msgpack.rb"
12
+ require "kyototycoon/tsvrpc.rb"
13
+ require "kyototycoon/tsvrpc/skinny.rb"
14
+ require "kyototycoon/stream.rb"
15
+
16
+ class KyotoTycoon
17
+ VERSION = '0.6.0'
18
+
19
+ attr_accessor :colenc, :connect_timeout, :servers
20
+ attr_reader :serializer, :logger, :db
21
+
22
+ DEFAULT_HOST = '0.0.0.0'
23
+ DEFAULT_PORT = 1978
24
+
25
+ def self.configure(name, host=DEFAULT_HOST, port=DEFAULT_PORT, &block)
26
+ @configure ||= {}
27
+ if @configure[name]
28
+ raise "'#{name}' is registered"
29
+ end
30
+ @configure[name] = lambda{
31
+ kt = KyotoTycoon.new(host, port)
32
+ block.call(kt)
33
+ kt
34
+ }
35
+ end
36
+
37
+ def self.configures
38
+ @configure
39
+ end
40
+
41
+ def self.configure_reset!
42
+ @configure = {}
43
+ end
44
+
45
+ def self.create(name)
46
+ if @configure[name].nil?
47
+ raise "undefined configure: '#{name}'"
48
+ end
49
+ @configure[name].call
50
+ end
51
+
52
+ def initialize(host=DEFAULT_HOST, port=DEFAULT_PORT)
53
+ @servers = [[host, port]]
54
+ @checked_servers = nil
55
+ @serializer = KyotoTycoon::Serializer::Default
56
+ @logger = Logger.new(nil)
57
+ @colenc = :B
58
+ @connect_timeout = 0.5
59
+ @cursor = 1
60
+ end
61
+
62
+ def serializer= (adaptor=:default)
63
+ klass = KyotoTycoon::Serializer.get(adaptor)
64
+ @serializer = klass
65
+ end
66
+
67
+ def db= (db)
68
+ @db = db
69
+ end
70
+
71
+ def logger= (logger)
72
+ if logger.class != Logger
73
+ logger = Logger.new(logger)
74
+ end
75
+ @logger = logger
76
+ end
77
+
78
+ def get(key)
79
+ res = request('/rpc/get', {:key => key})
80
+ @serializer.decode(Tsvrpc.parse(res[:body], res[:colenc])['value'])
81
+ end
82
+ alias_method :[], :get
83
+
84
+ def remove(*keys)
85
+ remove_bulk(keys.flatten)
86
+ end
87
+ alias_method :delete, :remove
88
+
89
+ def set(key, value, xt=nil)
90
+ res = request('/rpc/set', {:key => key, :value => @serializer.encode(value), :xt => xt})
91
+ Tsvrpc.parse(res[:body], res[:colenc])
92
+ end
93
+ alias_method :[]=, :set
94
+
95
+ def add(key, value, xt=nil)
96
+ res = request('/rpc/add', {:key => key, :value => @serializer.encode(value), :xt => xt})
97
+ Tsvrpc.parse(res[:body], res[:colenc])
98
+ end
99
+
100
+ def replace(key, value, xt=nil)
101
+ res = request('/rpc/replace', {:key => key, :value => @serializer.encode(value), :xt => xt})
102
+ Tsvrpc.parse(res[:body], res[:colenc])
103
+ end
104
+
105
+ def append(key, value, xt=nil)
106
+ request('/rpc/append', {:key => key, :value => @serializer.encode(value), :xt => xt})
107
+ end
108
+
109
+ def cas(key, oldval, newval, xt=nil)
110
+ res = request('/rpc/cas', {:key => key, :oval=> @serializer.encode(oldval), :nval => @serializer.encode(newval), :xt => xt})
111
+ case res[:status].to_i
112
+ when 200
113
+ true
114
+ when 450
115
+ false
116
+ end
117
+ end
118
+
119
+ def increment(key, num=1, xt=nil)
120
+ res = request('/rpc/increment', {:key => key, :num => num, :xt => xt})
121
+ Tsvrpc.parse(res[:body], res[:colenc])['num'].to_i
122
+ end
123
+ alias_method :incr, :increment
124
+
125
+ def decrement(key, num=1, xt=nil)
126
+ increment(key, num * -1, xt)
127
+ end
128
+ alias_method :decr, :decrement
129
+
130
+ def increment_double(key, num, xt=nil)
131
+ res = request('/rpc/increment_double', {:key => key, :num => num, :xt => xt})
132
+ Tsvrpc.parse(res[:body], res[:colenc])['num'].to_f
133
+ end
134
+
135
+ def set_bulk(records)
136
+ # records={'a' => 'aa', 'b' => 'bb'}
137
+ values = {}
138
+ records.each{|k,v|
139
+ values[k.to_s.match(/^_/) ? k.to_s : "_#{k}"] = @serializer.encode(v)
140
+ }
141
+ res = request('/rpc/set_bulk', values)
142
+ Tsvrpc.parse(res[:body], res[:colenc])
143
+ end
144
+
145
+ def get_bulk(keys)
146
+ params = keys.inject({}){|params, k|
147
+ params[k.to_s.match(/^_/) ? k.to_s : "_#{k}"] = ''
148
+ params
149
+ }
150
+ res = request('/rpc/get_bulk', params)
151
+ bulk = Tsvrpc.parse(res[:body], res[:colenc])
152
+ bulk.delete_if{|k,v| k.match(/^[^_]/)}.inject({}){|r, (k,v)|
153
+ r[k[1..-1]] = @serializer.decode(v)
154
+ r
155
+ }
156
+ end
157
+
158
+ def remove_bulk(keys)
159
+ params = keys.inject({}){|params, k|
160
+ params[k.to_s.match(/^_/) ? k.to_s : "_#{k}"] = ''
161
+ params
162
+ }
163
+ res = request('/rpc/remove_bulk', params)
164
+ Tsvrpc.parse(res[:body], res[:colenc])
165
+ end
166
+
167
+ def cursor(cur_id=nil)
168
+ Cursor.new(self, cur_id || @cursor += 1)
169
+ end
170
+
171
+ def clear
172
+ request('/rpc/clear')
173
+ end
174
+
175
+ def vacuum
176
+ request('/rpc/vacuum')
177
+ end
178
+
179
+ def sync(params={})
180
+ request('/rpc/synchronize', params)
181
+ end
182
+ alias_method :syncronize, :sync
183
+
184
+ def echo(value)
185
+ res = request('/rpc/echo', value)
186
+ Tsvrpc.parse(res[:body], res[:colenc])
187
+ end
188
+
189
+ def report
190
+ res = request('/rpc/report')
191
+ Tsvrpc.parse(res[:body], res[:colenc])
192
+ end
193
+
194
+ def status
195
+ res = request('/rpc/status')
196
+ Tsvrpc.parse(res[:body], res[:colenc])
197
+ end
198
+
199
+ def match_prefix(prefix)
200
+ res = request('/rpc/match_prefix', {:prefix => prefix})
201
+ keys = []
202
+ Tsvrpc.parse(res[:body], res[:colenc]).each{|k,v|
203
+ if k != 'num'
204
+ keys << k[1, k.length]
205
+ end
206
+ }
207
+ keys
208
+ end
209
+
210
+ def match_regex(re)
211
+ if re.class == Regexp
212
+ re = re.source
213
+ end
214
+ res = request('/rpc/match_regex', {:regex => re})
215
+ keys = []
216
+ Tsvrpc.parse(res[:body], res[:colenc]).each{|k,v|
217
+ if k != 'num'
218
+ keys << k[1, k.length]
219
+ end
220
+ }
221
+ keys
222
+ end
223
+
224
+ def keys
225
+ match_prefix("")
226
+ end
227
+
228
+ def request(path, params=nil)
229
+ if @db
230
+ params ||= {}
231
+ params[:DB] = @db
232
+ end
233
+
234
+ status,body,colenc = client.request(path, params, @colenc)
235
+ if ![200, 450].include?(status.to_i)
236
+ raise body
237
+ end
238
+ res = {:status => status, :body => body, :colenc => colenc}
239
+ @logger.info("#{path}: #{res[:status]} with query parameters #{params.inspect}")
240
+ res
241
+ end
242
+
243
+ def client
244
+ host, port = *choice_server
245
+ @client ||= begin
246
+ Tsvrpc::Skinny.new(host, port)
247
+ end
248
+ end
249
+
250
+ def start
251
+ client.start
252
+ end
253
+
254
+ def finish
255
+ client.finish
256
+ end
257
+
258
+ private
259
+
260
+ def ping(host, port)
261
+ begin
262
+ rpc = Tsvrpc::Skinny.new(host, port)
263
+ timeout(@connect_timeout){
264
+ @logger.debug("connect check #{host}:#{port}")
265
+ res = rpc.request('/rpc/echo', {'0' => '0'}, :U)
266
+ @logger.debug(res)
267
+ }
268
+ true
269
+ rescue Timeout::Error => ex
270
+ # Ruby 1.8.7 compatible
271
+ @logger.warn("connect failed at #{host}:#{port}")
272
+ false
273
+ rescue SystemCallError
274
+ @logger.warn("connect failed at #{host}:#{port}")
275
+ false
276
+ rescue => ex
277
+ @logger.warn("connect failed at #{host}:#{port}")
278
+ false
279
+ ensure
280
+ # for 1.8.7
281
+ rpc.finish
282
+ end
283
+ end
284
+
285
+ def choice_server
286
+ if @checked_servers
287
+ return @checked_servers
288
+ end
289
+
290
+ @servers.each{|s|
291
+ host,port = *s
292
+ if ping(host, port)
293
+ @checked_servers = [host, port]
294
+ break
295
+ end
296
+ }
297
+ if @checked_servers.nil?
298
+ msg = "alived server not exists"
299
+ @logger.fatal(msg)
300
+ raise msg
301
+ end
302
+ @checked_servers
303
+ end
304
+
305
+ end
@@ -0,0 +1,72 @@
1
+ # -- coding: utf-8
2
+
3
+
4
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper.rb')
5
+
6
+ describe KyotoTycoon do
7
+ it 'should handle multi servers' do
8
+ kt = KyotoTycoon.new('www.example.com', 11111)
9
+ kt.connect_timeout = 0.1
10
+ kt.servers << ['example.net', 1978]
11
+ kt.servers << ['0.0.0.0', 19999]
12
+ kt['foo'] = 'bar'
13
+ kt[:foo].should == 'bar'
14
+ end
15
+
16
+ it 'should configure/create method works' do
17
+ logger = Logger.new(STDOUT)
18
+ KyotoTycoon.configure(:test) do |kt|
19
+ kt.logger = logger
20
+ kt.serializer = :msgpack
21
+ kt.db = 'foobar'
22
+ end
23
+ KyotoTycoon.configure(:test2, 'host', 1999) do |kt|
24
+ kt.logger = logger
25
+ kt.serializer = :msgpack
26
+ kt.db = 'foobar'
27
+ end
28
+ %w!test test2!.each{|name|
29
+ kt = KyotoTycoon.create(name.to_sym)
30
+ kt.logger.should == logger
31
+ kt.serializer.should == KyotoTycoon::Serializer::Msgpack
32
+ kt.db.should == 'foobar'
33
+ }
34
+ lambda { KyotoTycoon.configure(:test2) }.should raise_error(StandardError)
35
+ lambda { KyotoTycoon.create(:not_exists) }.should raise_error(StandardError)
36
+
37
+ KyotoTycoon.configures.length.should == 2
38
+ KyotoTycoon.configure_reset!
39
+ KyotoTycoon.configures.length.should == 0
40
+ end
41
+
42
+ it 'should handle `ktremotemgr slave`' do
43
+ io = File.open("#{File.dirname(__FILE__)}/ktslave.txt", "r")
44
+ current = 0
45
+ KyotoTycoon::Stream.run(io){|line|
46
+ case current
47
+ when 0 # clear command
48
+ line.cmd.should == 'clear'
49
+ line.xt_time.should == Time.at(0)
50
+ line.value.should be_nil
51
+ line.key.should be_nil
52
+ line.value.should be_nil
53
+ when 1 # set foo bar
54
+ line.cmd.should == 'set'
55
+ line.xt_time.should > Time.now
56
+ line.key.should == 'foo'
57
+ line.value.should == 'bar'
58
+ when 2 # set fooxt bar with xt(2010-12-23 22:09:49 +0900)
59
+ line.cmd.should == 'set'
60
+ line.key.should == 'fooxt'
61
+ line.value.should == 'bar'
62
+ line.xt_time.should > Time.at(1234567890)
63
+ line.xt_time.should < Time.at(1334567890)
64
+ when 3 # remove foo
65
+ line.cmd.should == 'remove'
66
+ line.key.should == 'foo'
67
+ line.value.should be_nil
68
+ end
69
+ current += 1
70
+ }
71
+ end
72
+ end
@@ -0,0 +1,89 @@
1
+ # -- coding: utf-8
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper.rb')
4
+
5
+ describe KyotoTycoon do
6
+ before(:each) do
7
+ @kt = KyotoTycoon.new('localhost', 19999)
8
+ 100.times{|n|
9
+ @kt["#{"%02d" % n}foo"] = "foo#{n}"
10
+ }
11
+ end
12
+
13
+ it 'should handle cursor object' do
14
+ cur = @kt.cursor
15
+ cur.jump("33")
16
+ cur.key.should == "33foo"
17
+ cur.value.should == "foo33"
18
+ cur.current.should == [ "33foo","foo33" ]
19
+ cur.value = "new"
20
+ @kt["33foo"].should == "new"
21
+
22
+ cur.step
23
+ cur.key.should == "34foo"
24
+ cur.value.should == "foo34"
25
+ cur.remove
26
+ @kt["34foo"].should be_nil
27
+
28
+ cur.jump('55')
29
+ cur.seize.should == ["55foo","foo55"]
30
+ @kt["55foo"].should be_nil
31
+ end
32
+
33
+ it 'should handle cursor steps' do
34
+ # If you got failed in this section, it's a KyotoCabinet's bug
35
+ # It has been fixed at KyotoCabinet 1.2.70
36
+ # c.f.
37
+ # https://gist.github.com/1117611
38
+ # https://twitter.com/#!/fallabs/status/98079688550916097
39
+ cur = @kt.cursor
40
+ cur.jump
41
+ cur.key.should == "00foo"
42
+
43
+ cur.jump_back
44
+ cur.key.should == "99foo"
45
+ cur.jump
46
+ cur.key.should == "00foo"
47
+
48
+ cur.jump("50")
49
+ cur.step
50
+ cur.key.should == "51foo"
51
+ cur.step_back
52
+ cur.key.should == "50foo"
53
+ cur.step
54
+ cur.key.should == "51foo"
55
+ end
56
+
57
+ it "should handle multiple cursors" do
58
+ cur = @kt.cursor
59
+ cur2 = @kt.cursor
60
+ cur.jump("33")
61
+ cur.cur.should_not == cur2.cur
62
+ cur.key.should_not == cur2.key
63
+ end
64
+
65
+ it "should keep current position after called #each" do
66
+ cur = @kt.cursor
67
+ cur.jump("49")
68
+ cur.each{|k,v| } # no-op
69
+ cur.key.should == "49foo"
70
+ end
71
+
72
+ it "should handle #each" do
73
+ cur = @kt.cursor
74
+ cur.find{|k,v| k == "non-exists"}.should be_nil
75
+ cur.find_all{|k,v| k.match(/3[234]foo/)}.should == [
76
+ %w!32foo foo32!,
77
+ %w!33foo foo33!,
78
+ %w!34foo foo34!,
79
+ ]
80
+ cur.jump("33")
81
+ cur.find_all{|k,v| k.match(/3[234]foo/)}.should == [
82
+ %w!33foo foo33!,
83
+ %w!34foo foo34!,
84
+ ]
85
+
86
+ cur.jump
87
+ cur.find{|k,v| k == "15foo"}.should == ["15foo", "foo15"]
88
+ end
89
+ end
data/spec/ktslave.txt ADDED
@@ -0,0 +1,4 @@
1
+ 1293108572805000000 1 0 clear
2
+ 1293108575130000000 1 0 set Zm9v //////9iYXI=
3
+ 1293108589068000000 1 0 set Zm9veHQ= AE0TSh1iYXI=
4
+ 1293108597399000000 1 0 remove Zm9v