em-tycoon 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/.rvmrc +1 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +37 -0
- data/LICENSE +22 -0
- data/README.rdoc +38 -0
- data/Rakefile +44 -0
- data/em-tycoon.gemspec +22 -0
- data/lib/em-tycoon.rb +6 -0
- data/lib/em/tycoon.rb +134 -0
- data/lib/em/tycoon/protocol/binary/error_message.rb +18 -0
- data/lib/em/tycoon/protocol/binary/get_message.rb +91 -0
- data/lib/em/tycoon/protocol/binary/play_script_message.rb +76 -0
- data/lib/em/tycoon/protocol/binary/remove_message.rb +31 -0
- data/lib/em/tycoon/protocol/binary/set_message.rb +42 -0
- data/lib/em/tycoon/protocol/message.rb +142 -0
- data/lib/em/tycoon/protocol/parser.rb +29 -0
- data/spec/kt_msg_spec.rb +137 -0
- data/spec/log/.gitignore +2 -0
- data/spec/online/binary_protocol_spec.rb +69 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/testscript.lua +10 -0
- metadata +92 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
html
|
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm ruby-1.9.2
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
GIT
|
2
|
+
remote: git://github.com/bcg/em-spec.git
|
3
|
+
revision: e44d67847794d317078cf357271863447d26c8c5
|
4
|
+
specs:
|
5
|
+
em-spec (0.2.2)
|
6
|
+
|
7
|
+
PATH
|
8
|
+
remote: .
|
9
|
+
specs:
|
10
|
+
em-tycoon (0.0.1)
|
11
|
+
eventmachine
|
12
|
+
|
13
|
+
GEM
|
14
|
+
remote: http://rubygems.org/
|
15
|
+
specs:
|
16
|
+
child-process-manager (0.0.3)
|
17
|
+
diff-lcs (1.1.2)
|
18
|
+
eventmachine (0.12.10)
|
19
|
+
mocha (0.9.12)
|
20
|
+
rspec (2.5.0)
|
21
|
+
rspec-core (~> 2.5.0)
|
22
|
+
rspec-expectations (~> 2.5.0)
|
23
|
+
rspec-mocks (~> 2.5.0)
|
24
|
+
rspec-core (2.5.2)
|
25
|
+
rspec-expectations (2.5.0)
|
26
|
+
diff-lcs (~> 1.1.2)
|
27
|
+
rspec-mocks (2.5.0)
|
28
|
+
|
29
|
+
PLATFORMS
|
30
|
+
ruby
|
31
|
+
|
32
|
+
DEPENDENCIES
|
33
|
+
child-process-manager
|
34
|
+
em-spec!
|
35
|
+
em-tycoon!
|
36
|
+
mocha
|
37
|
+
rspec
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2011 Chris Ingrassia
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
= em_tycoon
|
2
|
+
|
3
|
+
An async client for Kyoto Tycoon (http://fallabs.com/kyototycoon/) using EventMachine
|
4
|
+
|
5
|
+
== Overview
|
6
|
+
|
7
|
+
em_tycoon uses Kyoto Tycoon's binary protocol (see http://fallabs.com/kyototycoon/spex.html#protocol) for increased efficiency, it does not currently implement any of the commands available outside of that protocol. This means you are currently limited to:
|
8
|
+
|
9
|
+
* get_bulk
|
10
|
+
* set_bulk
|
11
|
+
* remove_bulk
|
12
|
+
* play_script
|
13
|
+
|
14
|
+
== Quick start
|
15
|
+
|
16
|
+
require 'em-tycoon'
|
17
|
+
|
18
|
+
EM.run do
|
19
|
+
tycoon = EM::Tycoon.connect(:host => 'localhost', :port => 1978)
|
20
|
+
# The second key will expire after 24 hours
|
21
|
+
tycoon.set("key1" => "value1", "key_with_xt" => {:value => "value2", :xt => (Time.now+86400)}) do |set_result|
|
22
|
+
unless set_result.nil?
|
23
|
+
puts "Set #{set_result} keys"
|
24
|
+
tycoon.get("key1","key_with_xt") do |get_result|
|
25
|
+
get_result.each_pair do |key,value|
|
26
|
+
puts "Got #{key} = #{value[:value]} with expiration time : #{value[:xt] || 'None'}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
else
|
30
|
+
puts "Error!"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
== Copyright
|
36
|
+
|
37
|
+
Copyright (c) 2011 Chris Ingrassia. See LICENSE for details.
|
38
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
|
4
|
+
Bundler.setup
|
5
|
+
|
6
|
+
require 'rspec'
|
7
|
+
require 'rspec/core/rake_task'
|
8
|
+
|
9
|
+
desc 'Build .gem from Gemspec'
|
10
|
+
task :build do
|
11
|
+
system('gem build em-tycoon.gemspec')
|
12
|
+
end
|
13
|
+
|
14
|
+
RSpec::Core::RakeTask.new do |t|
|
15
|
+
t.rspec_opts = "-c"
|
16
|
+
t.pattern = FileList['spec/*_spec.rb']
|
17
|
+
end
|
18
|
+
|
19
|
+
namespace :spec do
|
20
|
+
desc "Spawns (and then reaps) a ktserver process to run online tests against "
|
21
|
+
RSpec::Core::RakeTask.new(:online) do |t|
|
22
|
+
t.rspec_opts = "-c"
|
23
|
+
t.pattern = FileList['spec/online/**/*_spec.rb']
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
require 'rake/rdoctask'
|
28
|
+
|
29
|
+
desc 'Generates RDOC'
|
30
|
+
Rake::RDocTask.new do |rd|
|
31
|
+
rd.main = 'README.rdoc'
|
32
|
+
rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
|
33
|
+
|
34
|
+
rd.options += [
|
35
|
+
'-SHN',
|
36
|
+
'-f', 'darkfish', # This is the important bit
|
37
|
+
]
|
38
|
+
end
|
39
|
+
|
40
|
+
desc "Shells out to 'bundle install'"
|
41
|
+
task :bundle do
|
42
|
+
system('bundle install > /dev/null')
|
43
|
+
end
|
44
|
+
|
data/em-tycoon.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "em-tycoon"
|
6
|
+
s.version = "0.9.0"
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ["Chris Ingrassia"]
|
9
|
+
s.email = ["chris@noneofyo.biz"]
|
10
|
+
s.homepage = "http://github.com/andry1/em-tycoon"
|
11
|
+
s.summary = %q{EventMachine client for Kyoto Tycoon}
|
12
|
+
s.description = %q{An async client for Kyoto Tycoon's binary protocol using EventMachine}
|
13
|
+
|
14
|
+
s.rubyforge_project = "em-tycoon"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency(%q<eventmachine>, [">= 0"])
|
22
|
+
end
|
data/lib/em-tycoon.rb
ADDED
data/lib/em/tycoon.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require_relative 'tycoon/protocol/parser'
|
3
|
+
|
4
|
+
module EM
|
5
|
+
# EventMachine Kyoto Tycoon Driver
|
6
|
+
# Uses Kyoto Tycoon's binary protocol for increased efficiency (see "Binary Protocol" at http://fallabs.com/kyototycoon/spex.html#protocol)
|
7
|
+
# To get started:
|
8
|
+
#
|
9
|
+
# tycoon = EM::Tycoon.connect(:host => 'localhost', :port => 1978)
|
10
|
+
# tycoon.get("key1","key2","key3") do |results|
|
11
|
+
# results.each_pair { |k,v| puts "#{k} = #{v[:value]}" }
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
module Tycoon
|
15
|
+
DEFAULT_OPTS = {:host => '127.0.0.1', :port => 1978}
|
16
|
+
REQUEST_TIMEOUT = 2 # Timeout requests after 2 seconds
|
17
|
+
|
18
|
+
# Connect to a Kyoto Tycoon host, supported options are:
|
19
|
+
# * :host => host name or IP address of ktserver instance (default '127.0.0.1')
|
20
|
+
# * :port => port ktserver is listening on (default 1978)
|
21
|
+
def self.connect(options={})
|
22
|
+
options = DEFAULT_OPTS.merge(options)
|
23
|
+
EM.connect(options[:host], options[:port], Client)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Kyoto Tycoon binary protocol handler
|
27
|
+
class Client < EM::Connection
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def post_init
|
34
|
+
@jobs = []
|
35
|
+
end
|
36
|
+
|
37
|
+
def receive_data(data)
|
38
|
+
bytes_parsed = 0
|
39
|
+
while @jobs.any? && (bytes_parsed < data.bytesize)
|
40
|
+
bytes_parsed += @jobs.first.parse_chunk(data[bytes_parsed..-1])
|
41
|
+
@jobs.shift if @jobs.first.message.parsed?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def unbind
|
46
|
+
end
|
47
|
+
|
48
|
+
# Use KT binary protocol "set_bulk" command to set keys and values passed in the data argument,
|
49
|
+
# with the option to pass an optional expiration time by specifying it in the value, in the following format:
|
50
|
+
#
|
51
|
+
# {
|
52
|
+
# "my_key1" => "my value for my_key1",
|
53
|
+
# "my_key2" => "my value for my_key2",
|
54
|
+
# "my_key3_with_60s_xt" => {:value => "my value for my_key3_with_60s_xt", :xt => 60}
|
55
|
+
# }
|
56
|
+
#
|
57
|
+
# Expiration times can be specified either as an integer representing the number of seconds the key should persist
|
58
|
+
# after it is created, or as a Time object containing the absolute time at which the key should expire.
|
59
|
+
#
|
60
|
+
# On completion, the callback block will be called (if provided) with the number of records stored as returned
|
61
|
+
# by Kyoto Tycoon, or nil on error. If no callback is specified, the no-reply option will be passed to Kyoto Tycoon.
|
62
|
+
#
|
63
|
+
def set(data={},&cb)
|
64
|
+
msg = Protocol::Message.generate(:set, data, {:no_reply => !(block_given?)})
|
65
|
+
if block_given?
|
66
|
+
job = Protocol::Parser.new(REQUEST_TIMEOUT)
|
67
|
+
job.callback { |result|
|
68
|
+
cb.call(result) if block_given?
|
69
|
+
}
|
70
|
+
job.errback { |result|
|
71
|
+
cb.call(nil) if block_given?
|
72
|
+
}
|
73
|
+
@jobs << job
|
74
|
+
send_data(msg)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Use KT binary protocol "get_bulk" command to get values for the given keys
|
79
|
+
# The callback will be called upon completion and passed a hash with the returned values and expirations
|
80
|
+
# (see EM::Tycoon::Client#set), or nil on error
|
81
|
+
def get(*keys,&cb)
|
82
|
+
raise ArgumentError.new("No block given") unless block_given?
|
83
|
+
msg = Protocol::Message.generate(:get, keys)
|
84
|
+
job = Protocol::Parser.new(REQUEST_TIMEOUT)
|
85
|
+
job.callback { |result|
|
86
|
+
cb.call(result) if block_given?
|
87
|
+
}
|
88
|
+
job.errback { |result|
|
89
|
+
cb.call(nil) if block_given?
|
90
|
+
}
|
91
|
+
@jobs << job
|
92
|
+
send_data(msg)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Use KT binary protocol "remove_bulk" command to remove the given keys
|
96
|
+
# The callback will be called upon completion and passed the number of keys deleted, or nil on error
|
97
|
+
def remove(keys=[],&cb)
|
98
|
+
msg = Protocol::Message.generate(:remove, keys)
|
99
|
+
if block_given?
|
100
|
+
job = Protocol::Parser.new(REQUEST_TIMEOUT)
|
101
|
+
job.callback { |result|
|
102
|
+
cb.call(result) if block_given?
|
103
|
+
}
|
104
|
+
job.errback { |result|
|
105
|
+
cb.call(nil) if block_given?
|
106
|
+
}
|
107
|
+
@jobs << job
|
108
|
+
send_data(msg)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Use KT binary protocol "play_script" command to execute the script function passed in the script_name
|
113
|
+
# argument, with the arguments passed in args
|
114
|
+
# The callback will be called upon completion and passed a hash containing the key/value pairs returned
|
115
|
+
# by the script (see EM::Tycoon::Client#set), or nil on error
|
116
|
+
def play_script(script_name, args={}, &cb)
|
117
|
+
msg = Protocol::Message.generate(:play_script, [script_name,args])
|
118
|
+
if block_given?
|
119
|
+
job = Protocol::Parser.new(REQUEST_TIMEOUT)
|
120
|
+
job.callback { |result|
|
121
|
+
cb.call(result) if block_given?
|
122
|
+
}
|
123
|
+
job.errback { |result|
|
124
|
+
cb.call(nil) if block_given?
|
125
|
+
}
|
126
|
+
@jobs << job
|
127
|
+
send_data(msg)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module EM
|
2
|
+
module Tycoon
|
3
|
+
module Protocol
|
4
|
+
module Binary
|
5
|
+
class ErrorMessage < EM::Tycoon::Protocol::Message
|
6
|
+
def initialize(data={},opts={})
|
7
|
+
super(:error)
|
8
|
+
@bytes_expected = 1
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.from_bytes(data=nil)
|
12
|
+
return self.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module EM
|
2
|
+
module Tycoon
|
3
|
+
module Protocol
|
4
|
+
module Binary
|
5
|
+
class GetMessage < EM::Tycoon::Protocol::Message
|
6
|
+
HEADER_BYTES_PER_RECORD = (
|
7
|
+
2 + # (uint16_t): (iteration): the index of the target database.
|
8
|
+
4 + # (uint32_t): (iteration): the size of the key.
|
9
|
+
4 + # (uint32_t): (iteration): the size of the value.
|
10
|
+
8 # (int64_t): (iteration): the expiration time.
|
11
|
+
)
|
12
|
+
PARSE_PHASES=EM::Tycoon::Protocol::Message::PARSE_PHASES+[:header,:keys_and_values]
|
13
|
+
HEADER_UNPACK_FORMAT="nNNH16"
|
14
|
+
# Best I can figure it, Mikio is passing in 63 bits of 1s in set_bulk messages inside
|
15
|
+
# his own utilities to force KT to detect an overflow of the _real_ max XT time (which is 40 bits of 1s)
|
16
|
+
# and then returns that real max XT time in the get_bulk reply... I think
|
17
|
+
NO_EXPIRATION_TIME_RESPONSE=0x000000ffffffffff
|
18
|
+
def initialize(data={},opts={})
|
19
|
+
super(:get,data)
|
20
|
+
@key_fmt = String.new
|
21
|
+
@value_fmt = String.new
|
22
|
+
@xts = Array.new
|
23
|
+
@db_idxs = Array.new
|
24
|
+
@keys_parsed = 0
|
25
|
+
@keysize = 0
|
26
|
+
@valuesize = 0
|
27
|
+
@dbidx = 0
|
28
|
+
@xt = 0
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.generate(data,opts={})
|
32
|
+
data = [data.to_s] unless data.kind_of?(Array)
|
33
|
+
msg_array = [MAGIC[:get], 0, data.length]
|
34
|
+
data.each {|k|
|
35
|
+
msg_array += [0, k.bytesize, k]
|
36
|
+
}
|
37
|
+
return msg_array.pack("CNN#{'nNa*'*data.length}")
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_chunk(data)
|
41
|
+
return 0 unless data && data.bytesize > 0
|
42
|
+
msg_hsh = {}
|
43
|
+
bytes_parsed = 0
|
44
|
+
case parse_phase
|
45
|
+
when :magic,:item_count
|
46
|
+
@magic, @item_count = data.unpack("CN")
|
47
|
+
@parse_phase = :header
|
48
|
+
@bytes_expected += HEADER_BYTES_PER_RECORD
|
49
|
+
bytes_parsed = 5
|
50
|
+
@data = Hash.new
|
51
|
+
when :header
|
52
|
+
@dbidx,@keysize,@valuesize,@xt = data.unpack(HEADER_UNPACK_FORMAT)
|
53
|
+
@xt = @xt.to_i(16)
|
54
|
+
@bytes_expected += (@keysize+@valuesize)
|
55
|
+
bytes_parsed = HEADER_BYTES_PER_RECORD
|
56
|
+
@parse_phase = :keys_and_values
|
57
|
+
when :keys_and_values
|
58
|
+
k,v = data.unpack("a#{@keysize}a#{@valuesize}")
|
59
|
+
@data[k] = {
|
60
|
+
:value => v,
|
61
|
+
:dbidx => @dbidx,
|
62
|
+
:xt => (@xt == NO_EXPIRATION_TIME_RESPONSE) ? nil : Time.at(@xt)
|
63
|
+
}
|
64
|
+
bytes_parsed = (@keysize+@valuesize)
|
65
|
+
@keysize = 0
|
66
|
+
@valuesize = 0
|
67
|
+
@dbidx = 0
|
68
|
+
@xt = nil
|
69
|
+
if (@keys_parsed+=1) == item_count
|
70
|
+
@parse_phase = :done
|
71
|
+
else
|
72
|
+
@parse_phase = :header
|
73
|
+
@bytes_expected += HEADER_BYTES_PER_RECORD
|
74
|
+
end
|
75
|
+
end
|
76
|
+
return bytes_parsed
|
77
|
+
end
|
78
|
+
|
79
|
+
def keysize
|
80
|
+
@key_sizes.inject {|sum,x| sum += x}
|
81
|
+
end
|
82
|
+
|
83
|
+
def valuesize
|
84
|
+
@value_sizes.inject {|sum,x| sum += x}
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module EM
|
2
|
+
module Tycoon
|
3
|
+
module Protocol
|
4
|
+
module Binary
|
5
|
+
class PlayScriptMessage < EM::Tycoon::Protocol::Message
|
6
|
+
KV_PACK_FMT="NNa*a*"
|
7
|
+
HEADER_BYTES_PER_RECORD=8
|
8
|
+
|
9
|
+
def initialize(data={},opts={})
|
10
|
+
super(:play_script,data)
|
11
|
+
@keys_parsed = 0
|
12
|
+
@keysize = 0
|
13
|
+
@valuesize = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.generate(data,opts={})
|
17
|
+
raise ArgumentError.new("Unsupported data type : #{data.class.name}") unless data.kind_of?(Array)
|
18
|
+
raise ArgumentError.new("Expected array of [<String>,<Hash>]") unless (data.length == 2) && data.first.kind_of?(String) && data.last.kind_of?(Hash)
|
19
|
+
script_name = data.first
|
20
|
+
args = data.last
|
21
|
+
msg_array = [MAGIC[:play_script]]
|
22
|
+
optflags = 0
|
23
|
+
opts.each_pair do |optkey,optval|
|
24
|
+
optflags |= FLAGS[optkey] if (FLAGS.has_key?(optkey) and (optval == true))
|
25
|
+
end
|
26
|
+
msg_array << optflags
|
27
|
+
msg_array << script_name.bytesize
|
28
|
+
msg_array << args.keys.length
|
29
|
+
msg_array << script_name
|
30
|
+
args.each_pair do |key,value|
|
31
|
+
value = value
|
32
|
+
msg_array << key.bytesize
|
33
|
+
msg_array << value.bytesize
|
34
|
+
msg_array << key.to_s
|
35
|
+
msg_array << value.to_s
|
36
|
+
end
|
37
|
+
return msg_array.pack("CNNNa*#{KV_PACK_FMT*args.keys.length}")
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_chunk(data)
|
41
|
+
return 0 unless data && data.bytesize > 0
|
42
|
+
msg_hsh = {}
|
43
|
+
bytes_parsed = 0
|
44
|
+
case parse_phase
|
45
|
+
when :magic,:item_count
|
46
|
+
@magic, @item_count = data.unpack("CN")
|
47
|
+
@parse_phase = :header
|
48
|
+
@bytes_expected += HEADER_BYTES_PER_RECORD
|
49
|
+
bytes_parsed = 5
|
50
|
+
@data = Hash.new
|
51
|
+
when :header
|
52
|
+
@keysize,@valuesize = data.unpack("NN")
|
53
|
+
@bytes_expected += (@keysize+@valuesize)
|
54
|
+
bytes_parsed = HEADER_BYTES_PER_RECORD
|
55
|
+
@parse_phase = :keys_and_values
|
56
|
+
when :keys_and_values
|
57
|
+
k,v = data.unpack("a#{@keysize}a#{@valuesize}")
|
58
|
+
@data[k] = {:value => v}
|
59
|
+
bytes_parsed = (@keysize+@valuesize)
|
60
|
+
@keysize = 0
|
61
|
+
@valuesize = 0
|
62
|
+
if (@keys_parsed+=1) == item_count
|
63
|
+
@parse_phase = :done
|
64
|
+
else
|
65
|
+
@parse_phase = :header
|
66
|
+
@bytes_expected += HEADER_BYTES_PER_RECORD
|
67
|
+
end
|
68
|
+
end
|
69
|
+
return bytes_parsed
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module EM
|
2
|
+
module Tycoon
|
3
|
+
module Protocol
|
4
|
+
module Binary
|
5
|
+
class RemoveMessage < EM::Tycoon::Protocol::Message
|
6
|
+
|
7
|
+
def initialize(data=nil,opts={})
|
8
|
+
super(:remove,data)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.generate(data,opts={})
|
12
|
+
data = [data.to_s] unless data.kind_of?(Array)
|
13
|
+
msg_array = [MAGIC[:remove]]
|
14
|
+
optflags = 0
|
15
|
+
opts.each_pair do |optkey,optval|
|
16
|
+
optflags |= FLAGS[optkey] if (FLAGS.has_key?(optkey) and (optval == true))
|
17
|
+
end
|
18
|
+
msg_array << optflags
|
19
|
+
msg_array << data.length
|
20
|
+
data.each do |d|
|
21
|
+
msg_array << 0 # dbidx
|
22
|
+
msg_array << d.to_s.bytesize
|
23
|
+
msg_array << d
|
24
|
+
end
|
25
|
+
return msg_array.pack("CNN#{'nNa*'*data.length}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module EM
|
2
|
+
module Tycoon
|
3
|
+
module Protocol
|
4
|
+
module Binary
|
5
|
+
class SetMessage < EM::Tycoon::Protocol::Message
|
6
|
+
|
7
|
+
def initialize(data={},opts={})
|
8
|
+
super(:set,data)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.generate(data,opts={})
|
12
|
+
raise ArgumentError.new("Unsupported data type : #{data.class.name}") unless data.kind_of?(Hash)
|
13
|
+
msg_array = [MAGIC[:set]]
|
14
|
+
optflags = 0
|
15
|
+
opts.each_pair do |optkey,optval|
|
16
|
+
optflags |= FLAGS[optkey] if (FLAGS.has_key?(optkey) and (optval == true))
|
17
|
+
end
|
18
|
+
msg_array << optflags
|
19
|
+
msg_array << data.keys.length
|
20
|
+
data.each_pair do |key,value|
|
21
|
+
xt = NO_XT_HEX
|
22
|
+
if value.kind_of?(Hash)
|
23
|
+
if value.has_key?(:xt)
|
24
|
+
xt = ("%016X" % (value[:xt].kind_of?(Time) ? (value[:xt] - Time.now).to_i : value[:xt].to_i))
|
25
|
+
end
|
26
|
+
value = value[:value]
|
27
|
+
end
|
28
|
+
msg_array << 0 # dbidx
|
29
|
+
msg_array << key.bytesize
|
30
|
+
msg_array << value.bytesize
|
31
|
+
msg_array << xt
|
32
|
+
msg_array << key
|
33
|
+
msg_array << value
|
34
|
+
end
|
35
|
+
return msg_array.pack("CNN#{KV_PACK_FMT*data.keys.length}")
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module EM
|
2
|
+
module Tycoon
|
3
|
+
module Protocol
|
4
|
+
|
5
|
+
# Represents a Kyoto Tycoon binary protocol message
|
6
|
+
# See KT docs : http://fallabs.com/kyototycoon/spex.html#protocol
|
7
|
+
class Message
|
8
|
+
MAGIC={:set => 0xB8,
|
9
|
+
:get => 0xBA,
|
10
|
+
:remove => 0xB9,
|
11
|
+
:play_script => 0xB4,
|
12
|
+
:replication => 0xB1,
|
13
|
+
:error => 0xBF}
|
14
|
+
MSG_TYPES=MAGIC.invert
|
15
|
+
FLAGS = {:no_reply => 0x01}
|
16
|
+
DEFAULT_OPTS = {
|
17
|
+
:no_reply => false
|
18
|
+
}
|
19
|
+
NO_EXPIRATION_TIME = 0x7FFFFFFFFFFFFFFF
|
20
|
+
PARSE_PHASES=[:magic,:item_count]
|
21
|
+
NO_XT_HEX="7#{'F'*15}"
|
22
|
+
KV_PACK_FMT="nNNH*a*a*"
|
23
|
+
|
24
|
+
attr_reader :bytes_expected,:parsed,:buffer,:parse_phase
|
25
|
+
# The human-readable symbol version of the KT message type, as defined in the keys of Message::MAGIC
|
26
|
+
attr_accessor :type
|
27
|
+
# "Magic" number header from KT message, indicating message type
|
28
|
+
attr_reader :magic
|
29
|
+
# Number of items/KV pairs contained in KT message
|
30
|
+
attr_reader :item_count
|
31
|
+
# Total size, in bytes, of all key data contained in KT message
|
32
|
+
attr_reader :keysize
|
33
|
+
# Total size, in bytes, of all value data contained in KT message
|
34
|
+
attr_reader :valuesize
|
35
|
+
# The data payload of the KT message, which can either be empty, contain a hash of KV pairs, or
|
36
|
+
# a list of keys
|
37
|
+
attr_reader :data
|
38
|
+
|
39
|
+
# Create a new KT message object to parse a response from KT or to serialize one to send,
|
40
|
+
# type indicates the message type being created, as defined by the keys of Message::MAGIC
|
41
|
+
# (e.g. :set, :get, etc.), and the optional data parameter can be used to specify the initial
|
42
|
+
# contents of the message, specific to the message type
|
43
|
+
def initialize(type,data=nil)
|
44
|
+
self.class.check_msg_type(type)
|
45
|
+
self.type = type
|
46
|
+
@data = data
|
47
|
+
@bytes_per_record = 0
|
48
|
+
@bytes_expected = 5
|
49
|
+
@keysize = @valuesize = 0
|
50
|
+
@parsed = false
|
51
|
+
@buffer = String.new
|
52
|
+
@parse_phase = PARSE_PHASES.first
|
53
|
+
end
|
54
|
+
|
55
|
+
def type=(t)
|
56
|
+
self.class.check_msg_type(t)
|
57
|
+
@type = t.downcase.to_sym
|
58
|
+
@magic = MAGIC[@type]
|
59
|
+
return @type
|
60
|
+
end
|
61
|
+
|
62
|
+
# Parse an arbitrary blob of data, possibly containing an entire message, possibly part of it, possibly more than 1
|
63
|
+
# returns the number of bytes from the buffer that were actually parsed and updates the #bytes_expected
|
64
|
+
# property accordingly
|
65
|
+
def parse(data)
|
66
|
+
return 0 unless data && data.bytesize > 0
|
67
|
+
if data.bytesize < @bytes_expected
|
68
|
+
@buffer << data
|
69
|
+
@bytes_expected -= data.bytesize
|
70
|
+
return data.bytesize
|
71
|
+
else
|
72
|
+
@buffer << data[0..@bytes_expected]
|
73
|
+
bytes_parsed = parse_chunk(@buffer)
|
74
|
+
return 0 if bytes_parsed == 0 # This is an error
|
75
|
+
@bytes_expected -= bytes_parsed
|
76
|
+
@buffer = String.new
|
77
|
+
if @bytes_expected == 0
|
78
|
+
@parsed = true
|
79
|
+
elsif (data.bytesize-bytes_parsed) > 0
|
80
|
+
bytes_parsed += parse(data[bytes_parsed..-1])
|
81
|
+
end
|
82
|
+
return bytes_parsed
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Parse a Kyoto Tycoon binary protocol message part into this Message instance, returning the number
|
87
|
+
# of bytes parsed. Default implementation supports standard magic+hits or just magic (in case of error message)
|
88
|
+
# messages
|
89
|
+
def parse_chunk(data)
|
90
|
+
return 0 if data.nil?
|
91
|
+
return 0 unless data.bytesize == @bytes_expected
|
92
|
+
bytes_parsed = 0
|
93
|
+
if @bytes_expected > 1
|
94
|
+
@magic,@item_count = data.unpack("CN")
|
95
|
+
bytes_parsed = 5
|
96
|
+
else
|
97
|
+
@magic = data.unpack("C").first
|
98
|
+
bytes_parsed = 1
|
99
|
+
end
|
100
|
+
@data = @item_count
|
101
|
+
@parse_phase = :item_count
|
102
|
+
return bytes_parsed
|
103
|
+
end
|
104
|
+
|
105
|
+
def parsed?
|
106
|
+
@parsed
|
107
|
+
end
|
108
|
+
|
109
|
+
def [](key)
|
110
|
+
@data[key]
|
111
|
+
end
|
112
|
+
|
113
|
+
class << self
|
114
|
+
protected(:new)
|
115
|
+
|
116
|
+
def message_for(data)
|
117
|
+
msgtype = msg_type_for(data)
|
118
|
+
raise ArgumentError.new("Unknown magic byte 0x#{('%02X' % data[0])}") unless msgtype
|
119
|
+
classname = msgtype.to_s.gsub(/^[a-z]|_[a-z]/) {|c| c.upcase}.gsub('_','') + "Message"
|
120
|
+
Binary.const_get(classname).new(data)
|
121
|
+
end
|
122
|
+
|
123
|
+
def msg_type_for(data)
|
124
|
+
magic = data.unpack("C").first
|
125
|
+
MSG_TYPES[magic]
|
126
|
+
end
|
127
|
+
|
128
|
+
def generate(type, data, opts={})
|
129
|
+
check_msg_type(type)
|
130
|
+
opts=DEFAULT_OPTS.merge(opts)
|
131
|
+
classname = type.to_s.gsub(/^[a-z]|_[a-z]/) {|c| c.upcase}.gsub('_','') + "Message"
|
132
|
+
Binary.const_get(classname).generate(data,opts)
|
133
|
+
end
|
134
|
+
|
135
|
+
def check_msg_type(type)
|
136
|
+
raise ArgumentError.new("Unknown message type #{type.inspect}") unless MAGIC.has_key?(type.downcase.to_sym)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
module EM
|
4
|
+
module Tycoon
|
5
|
+
module Protocol
|
6
|
+
class Parser
|
7
|
+
include EM::Deferrable
|
8
|
+
attr_reader :result,:bytes_parsed,:message
|
9
|
+
attr_accessor :buffer
|
10
|
+
|
11
|
+
# Create a new Parser deferrable, using the specified initial data and optional timeout specified in seconds
|
12
|
+
def initialize(timeout=0)
|
13
|
+
@bytes_parsed = 0
|
14
|
+
timeout(timeout) if timeout > 0
|
15
|
+
@message = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def parse_chunk(data)
|
19
|
+
@message ||= Message.message_for(data)
|
20
|
+
@bytes_parsed += @message.parse(data)
|
21
|
+
@result = @message.data
|
22
|
+
succeed(@message.data) if @message.parsed?
|
23
|
+
return @bytes_parsed
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/spec/kt_msg_spec.rb
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'spec_helper.rb'
|
2
|
+
|
3
|
+
describe "Kyoto Tycoon Messages" do
|
4
|
+
before(:each) do
|
5
|
+
@single_set_hsh = {
|
6
|
+
"mykey" => "myvalue"
|
7
|
+
}
|
8
|
+
@multiple_set_hsh = {
|
9
|
+
"mykey1" => "myvalue1",
|
10
|
+
"my_longer_key2" => "myvalue2",
|
11
|
+
"mykey3" => "my_longer_value3"
|
12
|
+
}
|
13
|
+
@get_key = @single_set_hsh.keys.first
|
14
|
+
@multiple_get_keys = @multiple_set_hsh.keys
|
15
|
+
@single_set_keysize = @single_set_hsh.keys.first.bytesize
|
16
|
+
@single_set_valuesize = @single_set_hsh.values.first.bytesize
|
17
|
+
@multiple_set_packed = [0xB8, 0, 3]
|
18
|
+
@play_script_packed = [0xB4, 0, "myscript".bytesize, @multiple_set_hsh.keys.length, "myscript"]
|
19
|
+
@play_script_reply = [0xB4, 3]
|
20
|
+
@get_bulk_reply = [0xBA, 3]
|
21
|
+
@multiple_set_hsh.each_pair do |k,v|
|
22
|
+
[@play_script_packed,@play_script_reply].each do |a|
|
23
|
+
a << k.bytesize
|
24
|
+
a << v.bytesize
|
25
|
+
a << k
|
26
|
+
a << v
|
27
|
+
end
|
28
|
+
[@multiple_set_packed,@get_bulk_reply].each do |a|
|
29
|
+
a << 0
|
30
|
+
a << k.bytesize
|
31
|
+
a << v.bytesize
|
32
|
+
a << EM::Tycoon::Protocol::Message::NO_XT_HEX
|
33
|
+
a << k
|
34
|
+
a << v
|
35
|
+
end
|
36
|
+
end
|
37
|
+
# XTs of "none" coming back in response are not 0x7FF...
|
38
|
+
@get_bulk_reply.collect! {|x| (x == EM::Tycoon::Protocol::Message::NO_XT_HEX) ? "000000ffffffffff" : x}
|
39
|
+
@multiple_set_packed = @multiple_set_packed.pack("CNN#{EM::Tycoon::Protocol::Message::KV_PACK_FMT*@multiple_set_hsh.keys.length}")
|
40
|
+
@multiple_get_packed = [0xBA, 0, 3,
|
41
|
+
0, "mykey1".bytesize, "mykey1",
|
42
|
+
0, "my_longer_key2".bytesize, "my_longer_key2",
|
43
|
+
0, "mykey3".bytesize, "mykey3"].pack("CNN"+("nNa*"*3))
|
44
|
+
@single_set_packed = [0xB8, 0, 1, 0,
|
45
|
+
@single_set_keysize, @single_set_valuesize, "7#{'F'*15}",
|
46
|
+
@single_set_hsh.keys.first, @single_set_hsh.values.first].pack("CNNnNNH*a*a*")
|
47
|
+
@single_get_packed = [0xBA, 0, 1, 0, "mykey".bytesize, "mykey"].pack("CNNnNa*")
|
48
|
+
|
49
|
+
@single_remove_packed = [0xB9, 0, 1, 0, "mykey".bytesize, "mykey"].pack("CNNnNa*")
|
50
|
+
@multiple_remove_packed = [0xB9, 0, 3,
|
51
|
+
0, "mykey1".bytesize, "mykey1",
|
52
|
+
0, "my_longer_key2".bytesize, "my_longer_key2",
|
53
|
+
0, "mykey3".bytesize, "mykey3"].pack("CNNnNa*nNa*nNa*")
|
54
|
+
@get_bulk_reply = @get_bulk_reply.pack("CN#{EM::Tycoon::Protocol::Message::KV_PACK_FMT*@multiple_set_hsh.keys.length}")
|
55
|
+
@set_bulk_reply = [0xB8, 2].pack("CN")
|
56
|
+
@remove_bulk_reply = [0xB9, 4].pack("CN")
|
57
|
+
@play_script_packed = @play_script_packed.pack("CNNNa*"+EM::Tycoon::Protocol::Binary::PlayScriptMessage::KV_PACK_FMT*@multiple_set_hsh.keys.length)
|
58
|
+
@play_script_reply = @play_script_reply.pack("CN#{EM::Tycoon::Protocol::Binary::PlayScriptMessage::KV_PACK_FMT*@multiple_set_hsh.keys.length}")
|
59
|
+
end
|
60
|
+
|
61
|
+
it "Should generate a set_bulk message properly with one key/value pair" do
|
62
|
+
msg = EM::Tycoon::Protocol::Message.generate(:set, @single_set_hsh)
|
63
|
+
msg.should == @single_set_packed
|
64
|
+
end
|
65
|
+
|
66
|
+
it "Should generate a set_bulk message properly with multiple key/value pairs" do
|
67
|
+
msg = EM::Tycoon::Protocol::Message.generate(:set, @multiple_set_hsh)
|
68
|
+
msg.should == @multiple_set_packed
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
it "Should parse a set_bulk reply properly" do
|
73
|
+
msg = EM::Tycoon::Protocol::Message.message_for(@set_bulk_reply)
|
74
|
+
bytes_parsed = msg.parse(@set_bulk_reply)
|
75
|
+
bytes_parsed.should == @set_bulk_reply.bytesize
|
76
|
+
msg.item_count.should be_kind_of(Integer)
|
77
|
+
msg.item_count.should == 2
|
78
|
+
end
|
79
|
+
|
80
|
+
it "Should generate a get_bulk message properly with one key" do
|
81
|
+
msg = EM::Tycoon::Protocol::Message.generate(:get, "mykey")
|
82
|
+
msg.should == @single_get_packed
|
83
|
+
end
|
84
|
+
|
85
|
+
it "Should generate a get_bulk message properly with multiple keys" do
|
86
|
+
msg = EM::Tycoon::Protocol::Message.generate(:get, ["mykey1","my_longer_key2","mykey3"])
|
87
|
+
msg.should == @multiple_get_packed
|
88
|
+
end
|
89
|
+
|
90
|
+
it "Should parse a get_bulk reply properly" do
|
91
|
+
msg = EM::Tycoon::Protocol::Message.message_for(@get_bulk_reply)
|
92
|
+
bytes_parsed = msg.parse(@get_bulk_reply)
|
93
|
+
bytes_parsed.should == @get_bulk_reply.bytesize
|
94
|
+
msg.item_count.should be_kind_of(Integer)
|
95
|
+
msg.item_count.should == 3
|
96
|
+
@multiple_set_hsh.each_pair do |k,v|
|
97
|
+
msg[k].should be_kind_of(Hash)
|
98
|
+
msg[k][:value].should == v
|
99
|
+
msg[k][:xt].should == nil
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
it "Should generate a remove_bulk message properly with one key" do
|
104
|
+
msg = EM::Tycoon::Protocol::Message.generate(:remove, "mykey")
|
105
|
+
msg.should == @single_remove_packed
|
106
|
+
end
|
107
|
+
|
108
|
+
it "Should generate a remove_bulk message properly with multiple keys" do
|
109
|
+
msg = EM::Tycoon::Protocol::Message.generate(:remove, ["mykey1","my_longer_key2","mykey3"])
|
110
|
+
msg.should == @multiple_remove_packed
|
111
|
+
end
|
112
|
+
|
113
|
+
it "Should parse a remove_bulk reply properly" do
|
114
|
+
msg = EM::Tycoon::Protocol::Message.message_for(@remove_bulk_reply)
|
115
|
+
bytes_parsed = msg.parse(@remove_bulk_reply)
|
116
|
+
bytes_parsed.should == @remove_bulk_reply.bytesize
|
117
|
+
msg.item_count.should be_kind_of(Integer)
|
118
|
+
msg.item_count.should == 4
|
119
|
+
end
|
120
|
+
|
121
|
+
it "Should generate a play_script message properly" do
|
122
|
+
msg = EM::Tycoon::Protocol::Message.generate(:play_script, ["myscript", @multiple_set_hsh])
|
123
|
+
msg.should == @play_script_packed
|
124
|
+
end
|
125
|
+
|
126
|
+
it "Should parse a play_script reply propery" do
|
127
|
+
msg = EM::Tycoon::Protocol::Message.message_for(@play_script_reply)
|
128
|
+
bytes_parsed = msg.parse(@play_script_reply)
|
129
|
+
bytes_parsed.should == @play_script_reply.bytesize
|
130
|
+
msg.item_count.should be_kind_of(Integer)
|
131
|
+
msg.item_count.should == 3
|
132
|
+
@multiple_set_hsh.each_pair do |k,v|
|
133
|
+
msg[k].should be_kind_of(Hash)
|
134
|
+
msg[k][:value].should == v
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
data/spec/log/.gitignore
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require File.expand_path('../../spec_helper.rb', __FILE__)
|
2
|
+
|
3
|
+
describe "Binary Protocol" do
|
4
|
+
include EM::Spec
|
5
|
+
|
6
|
+
before(:all) do
|
7
|
+
ChildProcessManager.spawn({:cmd => "ktserver -host #{KT_OPTS[:host]} -port #{KT_OPTS[:port]} -scr #{File.dirname(__FILE__)}/../testscript.lua -log #{File.dirname(__FILE__)}/../log/ktserver.log -li ':#ktcapsiz=16m'",
|
8
|
+
:port => KT_OPTS[:port]})
|
9
|
+
done
|
10
|
+
end
|
11
|
+
|
12
|
+
after(:all) do
|
13
|
+
ChildProcessManager.reap_all
|
14
|
+
done
|
15
|
+
end
|
16
|
+
|
17
|
+
it "Should support get and set operations with single KV pairs" do
|
18
|
+
client = EM::Tycoon.connect(KT_OPTS)
|
19
|
+
client.should be
|
20
|
+
client.set("key1" => "value1") { |result|
|
21
|
+
result.should == 1
|
22
|
+
client.get("key1") {|getresult|
|
23
|
+
getresult["key1"][:value].should == "value1"
|
24
|
+
client.remove("key1") {|removeresult|
|
25
|
+
removeresult.should == 1
|
26
|
+
done
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
it "Should support get and set operations with multiple KV pairs" do
|
33
|
+
client = EM::Tycoon.connect(KT_OPTS)
|
34
|
+
client.should be
|
35
|
+
xt = Time.now+86400
|
36
|
+
kvs = {
|
37
|
+
"key1" => {:value => "value1", :xt => xt},
|
38
|
+
"key2" => {:value => "value2", :xt => xt}
|
39
|
+
}
|
40
|
+
client.set(kvs) { |result|
|
41
|
+
result.should == 2
|
42
|
+
client.get(kvs.keys) {|getresult|
|
43
|
+
kvs.each_pair do |k,v|
|
44
|
+
getresult[k][:value].should == v[:value]
|
45
|
+
getresult[k][:dbidx].should == 0
|
46
|
+
getresult[k][:xt].should <= v[:xt]
|
47
|
+
end
|
48
|
+
client.remove(kvs.keys) {|removeresult|
|
49
|
+
removeresult.should == 2
|
50
|
+
done
|
51
|
+
}
|
52
|
+
}
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
it "Should support the play_script command" do
|
57
|
+
client = EM::Tycoon.connect(KT_OPTS)
|
58
|
+
client.should be
|
59
|
+
args = {"arg1" => "value1", "arg2" => "value2", "arg3" => "value3"}
|
60
|
+
client.play_script("testscript", args) { |result|
|
61
|
+
result.should be_kind_of(Hash)
|
62
|
+
args.each_pair do |k,v|
|
63
|
+
result[k][:value].should == args[k]
|
64
|
+
end
|
65
|
+
done
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
begin
|
2
|
+
require File.expand_path('../../.bundle/environment', File.dirname(__FILE__))
|
3
|
+
rescue LoadError
|
4
|
+
require 'rubygems'
|
5
|
+
require 'bundler'
|
6
|
+
|
7
|
+
Bundler.setup(:default, :test)
|
8
|
+
end
|
9
|
+
require 'eventmachine'
|
10
|
+
require 'rspec'
|
11
|
+
require 'em-spec/rspec'
|
12
|
+
require 'child-process-manager'
|
13
|
+
|
14
|
+
RSpec.configure do |config|
|
15
|
+
config.mock_with :mocha
|
16
|
+
end
|
17
|
+
|
18
|
+
$LOAD_PATH << File.dirname(__FILE__) + '/../lib'
|
19
|
+
require 'em-tycoon'
|
20
|
+
|
21
|
+
KT_OPTS={:host => "localhost", :port => 1979}
|
data/spec/testscript.lua
ADDED
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: em-tycoon
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.9.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Chris Ingrassia
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-05-08 00:00:00 -04:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: eventmachine
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "0"
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
description: An async client for Kyoto Tycoon's binary protocol using EventMachine
|
28
|
+
email:
|
29
|
+
- chris@noneofyo.biz
|
30
|
+
executables: []
|
31
|
+
|
32
|
+
extensions: []
|
33
|
+
|
34
|
+
extra_rdoc_files: []
|
35
|
+
|
36
|
+
files:
|
37
|
+
- .gitignore
|
38
|
+
- .rvmrc
|
39
|
+
- Gemfile
|
40
|
+
- Gemfile.lock
|
41
|
+
- LICENSE
|
42
|
+
- README.rdoc
|
43
|
+
- Rakefile
|
44
|
+
- em-tycoon.gemspec
|
45
|
+
- lib/em-tycoon.rb
|
46
|
+
- lib/em/tycoon.rb
|
47
|
+
- lib/em/tycoon/protocol/binary/error_message.rb
|
48
|
+
- lib/em/tycoon/protocol/binary/get_message.rb
|
49
|
+
- lib/em/tycoon/protocol/binary/play_script_message.rb
|
50
|
+
- lib/em/tycoon/protocol/binary/remove_message.rb
|
51
|
+
- lib/em/tycoon/protocol/binary/set_message.rb
|
52
|
+
- lib/em/tycoon/protocol/message.rb
|
53
|
+
- lib/em/tycoon/protocol/parser.rb
|
54
|
+
- spec/kt_msg_spec.rb
|
55
|
+
- spec/log/.gitignore
|
56
|
+
- spec/online/binary_protocol_spec.rb
|
57
|
+
- spec/spec_helper.rb
|
58
|
+
- spec/testscript.lua
|
59
|
+
has_rdoc: true
|
60
|
+
homepage: http://github.com/andry1/em-tycoon
|
61
|
+
licenses: []
|
62
|
+
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: "0"
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: "0"
|
80
|
+
requirements: []
|
81
|
+
|
82
|
+
rubyforge_project: em-tycoon
|
83
|
+
rubygems_version: 1.5.2
|
84
|
+
signing_key:
|
85
|
+
specification_version: 3
|
86
|
+
summary: EventMachine client for Kyoto Tycoon
|
87
|
+
test_files:
|
88
|
+
- spec/kt_msg_spec.rb
|
89
|
+
- spec/log/.gitignore
|
90
|
+
- spec/online/binary_protocol_spec.rb
|
91
|
+
- spec/spec_helper.rb
|
92
|
+
- spec/testscript.lua
|