em-tycoon 0.9.0

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