em-tycoon 0.9.0
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.
- 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
|