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 ADDED
@@ -0,0 +1 @@
1
+ html
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm ruby-1.9.2
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source :gemcutter
2
+
3
+ group(:test) do
4
+ gem 'rspec'
5
+ gem 'mocha'
6
+ gem 'em-spec', :git => 'git://github.com/bcg/em-spec.git'
7
+ gem 'child-process-manager'
8
+ end
9
+ gemspec
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
@@ -0,0 +1,6 @@
1
+ require 'eventmachine'
2
+ require_relative 'em/tycoon'
3
+ require_relative 'em/tycoon/protocol/message'
4
+ ['error', 'get','set','remove', 'play_script'].each do |x|
5
+ require_relative "em/tycoon/protocol/binary/#{x}_message"
6
+ end
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
@@ -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
@@ -0,0 +1,2 @@
1
+ *
2
+ !.gitignore
@@ -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
@@ -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}
@@ -0,0 +1,10 @@
1
+ kt = __kyototycoon__
2
+ db = kt.db
3
+
4
+ function testscript(inmap, outmap)
5
+ kt.log("system", "testscript called")
6
+ for k,v in pairs(inmap) do
7
+ outmap[k] = v
8
+ end
9
+ return kt.RVSUCCESS
10
+ end
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