maxlapshin-carrot 0.6.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/README.markdown +34 -0
- data/VERSION.yml +4 -0
- data/lib/amqp/buffer.rb +401 -0
- data/lib/amqp/exchange.rb +51 -0
- data/lib/amqp/frame.rb +121 -0
- data/lib/amqp/header.rb +27 -0
- data/lib/amqp/protocol.rb +209 -0
- data/lib/amqp/queue.rb +94 -0
- data/lib/amqp/server.rb +184 -0
- data/lib/amqp/spec.rb +820 -0
- data/lib/carrot.rb +91 -0
- data/lib/examples/simple_pop.rb +13 -0
- data/test/carrot_test.rb +8 -0
- data/test/test_helper.rb +18 -0
- metadata +69 -0
data/lib/amqp/frame.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
module Carrot::AMQP
|
2
|
+
class Frame #:nodoc: all
|
3
|
+
def initialize payload = nil, channel = 0
|
4
|
+
@channel, @payload = channel, payload
|
5
|
+
end
|
6
|
+
attr_accessor :channel, :payload
|
7
|
+
|
8
|
+
def id
|
9
|
+
self.class::ID
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_binary
|
13
|
+
buf = Buffer.new
|
14
|
+
buf.write :octet, id
|
15
|
+
buf.write :short, channel
|
16
|
+
buf.write :longstr, payload
|
17
|
+
buf.write :octet, FOOTER
|
18
|
+
buf.rewind
|
19
|
+
buf
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
to_binary.to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
def == frame
|
27
|
+
[ :id, :channel, :payload ].inject(true) do |eql, field|
|
28
|
+
eql and __send__(field) == frame.__send__(field)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Invalid < StandardError; end
|
33
|
+
|
34
|
+
class Method
|
35
|
+
def initialize payload = nil, channel = 0
|
36
|
+
super
|
37
|
+
unless @payload.is_a? Protocol::Class::Method or @payload.nil?
|
38
|
+
@payload = Protocol.parse(@payload)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Header
|
44
|
+
def initialize payload = nil, channel = 0
|
45
|
+
super
|
46
|
+
unless @payload.is_a? Protocol::Header or @payload.nil?
|
47
|
+
@payload = Protocol::Header.new(@payload)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Body; end
|
53
|
+
|
54
|
+
def self.parse(buf)
|
55
|
+
buf = Buffer.new(buf) unless buf.is_a? Buffer
|
56
|
+
buf.extract do
|
57
|
+
id, channel, payload, footer = buf.read(:octet, :short, :longstr, :octet)
|
58
|
+
Frame.types[id].new(payload, channel) if footer == FOOTER
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
if $0 =~ /bacon/ or $0 == __FILE__
|
65
|
+
require 'rubygems'
|
66
|
+
require 'bacon'
|
67
|
+
include AMQP
|
68
|
+
|
69
|
+
describe Frame do
|
70
|
+
should 'handle basic frame types' do
|
71
|
+
Frame::Method.new.id.should == 1
|
72
|
+
Frame::Header.new.id.should == 2
|
73
|
+
Frame::Body.new.id.should == 3
|
74
|
+
end
|
75
|
+
|
76
|
+
should 'convert method frames to binary' do
|
77
|
+
meth = Protocol::Connection::Secure.new :challenge => 'secret'
|
78
|
+
|
79
|
+
frame = Frame::Method.new(meth)
|
80
|
+
frame.to_binary.should.be.kind_of? Buffer
|
81
|
+
frame.to_s.should == [ 1, 0, meth.to_s.length, meth.to_s, 206 ].pack('CnNa*C')
|
82
|
+
end
|
83
|
+
|
84
|
+
should 'convert binary to method frames' do
|
85
|
+
orig = Frame::Method.new Protocol::Connection::Secure.new(:challenge => 'secret')
|
86
|
+
|
87
|
+
copy = Frame.parse(orig.to_binary)
|
88
|
+
copy.should == orig
|
89
|
+
end
|
90
|
+
|
91
|
+
should 'ignore partial frames until ready' do
|
92
|
+
frame = Frame::Method.new Protocol::Connection::Secure.new(:challenge => 'secret')
|
93
|
+
data = frame.to_s
|
94
|
+
|
95
|
+
buf = Buffer.new
|
96
|
+
Frame.parse(buf).should == nil
|
97
|
+
|
98
|
+
buf << data[0..5]
|
99
|
+
Frame.parse(buf).should == nil
|
100
|
+
|
101
|
+
buf << data[6..-1]
|
102
|
+
Frame.parse(buf).should == frame
|
103
|
+
|
104
|
+
Frame.parse(buf).should == nil
|
105
|
+
end
|
106
|
+
|
107
|
+
should 'convert header frames to binary' do
|
108
|
+
head = Protocol::Header.new(Protocol::Basic, :priority => 1)
|
109
|
+
|
110
|
+
frame = Frame::Header.new(head)
|
111
|
+
frame.to_s.should == [ 2, 0, head.to_s.length, head.to_s, 206 ].pack('CnNa*C')
|
112
|
+
end
|
113
|
+
|
114
|
+
should 'convert binary to header frame' do
|
115
|
+
orig = Frame::Header.new Protocol::Header.new(Protocol::Basic, :priority => 1)
|
116
|
+
|
117
|
+
copy = Frame.parse(orig.to_binary)
|
118
|
+
copy.should == orig
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/lib/amqp/header.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Carrot::AMQP
|
2
|
+
class Header
|
3
|
+
def initialize(server, header_obj)
|
4
|
+
@server = server
|
5
|
+
@header = header_obj
|
6
|
+
end
|
7
|
+
|
8
|
+
# Acknowledges the receipt of this message with the server.
|
9
|
+
def ack
|
10
|
+
@server.send(Protocol::Basic::Ack.new(:delivery_tag => properties[:delivery_tag]))
|
11
|
+
end
|
12
|
+
|
13
|
+
# Reject this message (XXX currently unimplemented in rabbitmq)
|
14
|
+
# * :requeue => true | false (default false)
|
15
|
+
def reject(opts = {})
|
16
|
+
@server.send(Protocol::Basic::Reject.new(opts.merge(:delivery_tag => properties[:delivery_tag])))
|
17
|
+
end
|
18
|
+
|
19
|
+
def method_missing(meth, *args, &blk)
|
20
|
+
@header.send(meth, *args, &blk)
|
21
|
+
end
|
22
|
+
|
23
|
+
def inspect
|
24
|
+
@header.inspect
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
module Carrot::AMQP
|
2
|
+
module Protocol
|
3
|
+
#:stopdoc:
|
4
|
+
class Class::Method
|
5
|
+
def initialize *args
|
6
|
+
opts = args.pop if args.last.is_a? Hash
|
7
|
+
opts ||= {}
|
8
|
+
|
9
|
+
@debug = 1 # XXX hack, p(obj) == '' if no instance vars are set
|
10
|
+
|
11
|
+
if args.size == 1 and args.first.is_a? Buffer
|
12
|
+
buf = args.shift
|
13
|
+
else
|
14
|
+
buf = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
self.class.arguments.each do |type, name|
|
18
|
+
val = buf ? buf.read(type) :
|
19
|
+
args.shift || opts[name] || opts[name.to_s]
|
20
|
+
instance_variable_set("@#{name}", val)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def arguments
|
25
|
+
self.class.arguments.inject({}) do |hash, (type, name)|
|
26
|
+
hash.update name => instance_variable_get("@#{name}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_binary
|
31
|
+
buf = Buffer.new
|
32
|
+
buf.write :short, self.class.parent.id
|
33
|
+
buf.write :short, self.class.id
|
34
|
+
|
35
|
+
bits = []
|
36
|
+
|
37
|
+
self.class.arguments.each do |type, name|
|
38
|
+
val = instance_variable_get("@#{name}")
|
39
|
+
if type == :bit
|
40
|
+
bits << (val || false)
|
41
|
+
else
|
42
|
+
unless bits.empty?
|
43
|
+
buf.write :bit, bits
|
44
|
+
bits = []
|
45
|
+
end
|
46
|
+
buf.write type, val
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
buf.write :bit, bits unless bits.empty?
|
51
|
+
buf.rewind
|
52
|
+
|
53
|
+
buf
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s
|
57
|
+
to_binary.to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_frame channel = 0
|
61
|
+
Frame::Method.new(self, channel)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
#:startdoc:
|
66
|
+
#
|
67
|
+
# Contains a properties hash that holds some potentially interesting
|
68
|
+
# information.
|
69
|
+
# * :delivery_mode
|
70
|
+
# 1 equals transient.
|
71
|
+
# 2 equals persistent. Unconsumed persistent messages will survive
|
72
|
+
# a server restart when they are stored in a durable queue.
|
73
|
+
# * :redelivered
|
74
|
+
# True or False
|
75
|
+
# * :routing_key
|
76
|
+
# The routing string used for matching this message to this queue.
|
77
|
+
# * :priority
|
78
|
+
# An integer in the range of 0 to 9 inclusive.
|
79
|
+
# * :content_type
|
80
|
+
# Always "application/octet-stream" (byte stream)
|
81
|
+
# * :exchange
|
82
|
+
# The source exchange which published this message.
|
83
|
+
# * :message_count
|
84
|
+
# The number of unconsumed messages contained in the queue.
|
85
|
+
# * :delivery_tag
|
86
|
+
# A monotonically increasing integer. This number should not be trusted
|
87
|
+
# as a sequence number. There is no guarantee it won't get reset.
|
88
|
+
class Header
|
89
|
+
def initialize *args
|
90
|
+
opts = args.pop if args.last.is_a? Hash
|
91
|
+
opts ||= {}
|
92
|
+
|
93
|
+
first = args.shift
|
94
|
+
|
95
|
+
if first.is_a? ::Class and first.ancestors.include? Protocol::Class
|
96
|
+
@klass = first
|
97
|
+
@size = args.shift || 0
|
98
|
+
@weight = args.shift || 0
|
99
|
+
@properties = opts
|
100
|
+
|
101
|
+
elsif first.is_a? Buffer or first.is_a? String
|
102
|
+
buf = first
|
103
|
+
buf = Buffer.new(buf) unless buf.is_a? Buffer
|
104
|
+
|
105
|
+
@klass = Protocol.classes[buf.read(:short)]
|
106
|
+
@weight = buf.read(:short)
|
107
|
+
@size = buf.read(:longlong)
|
108
|
+
|
109
|
+
props = buf.read(:properties, *klass.properties.map{|type,_| type })
|
110
|
+
@properties = Hash[*klass.properties.map{|_,name| name }.zip(props).reject{|k,v| v.nil? }.flatten]
|
111
|
+
|
112
|
+
else
|
113
|
+
raise ArgumentError, 'Invalid argument'
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
attr_accessor :klass, :size, :weight, :properties
|
118
|
+
|
119
|
+
def to_binary
|
120
|
+
buf = Buffer.new
|
121
|
+
buf.write :short, klass.id
|
122
|
+
buf.write :short, weight # XXX rabbitmq only supports weight == 0
|
123
|
+
buf.write :longlong, size
|
124
|
+
buf.write :properties, (klass.properties.map do |type, name|
|
125
|
+
[ type, properties[name] || properties[name.to_s] ]
|
126
|
+
end)
|
127
|
+
buf.rewind
|
128
|
+
buf
|
129
|
+
end
|
130
|
+
|
131
|
+
def to_s
|
132
|
+
to_binary.to_s
|
133
|
+
end
|
134
|
+
|
135
|
+
def to_frame channel = 0
|
136
|
+
Frame::Header.new(self, channel)
|
137
|
+
end
|
138
|
+
|
139
|
+
def == header
|
140
|
+
[ :klass, :size, :weight, :properties ].inject(true) do |eql, field|
|
141
|
+
eql and __send__(field) == header.__send__(field)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def method_missing meth, *args, &blk
|
146
|
+
@properties.has_key?(meth) || @klass.properties.find{|_,name| name == meth } ? @properties[meth] :
|
147
|
+
super
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.parse buf
|
152
|
+
buf = Buffer.new(buf) unless buf.is_a? Buffer
|
153
|
+
class_id, method_id = buf.read(:short, :short)
|
154
|
+
classes[class_id].methods[method_id].new(buf)
|
155
|
+
end
|
156
|
+
#:stopdoc:
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
if $0 =~ /bacon/ or $0 == __FILE__
|
161
|
+
require 'bacon'
|
162
|
+
include AMQP
|
163
|
+
|
164
|
+
describe Protocol do
|
165
|
+
should 'instantiate methods with arguments' do
|
166
|
+
meth = Protocol::Connection::StartOk.new nil, 'PLAIN', nil, 'en_US'
|
167
|
+
meth.locale.should == 'en_US'
|
168
|
+
end
|
169
|
+
|
170
|
+
should 'instantiate methods with named parameters' do
|
171
|
+
meth = Protocol::Connection::StartOk.new :locale => 'en_US',
|
172
|
+
:mechanism => 'PLAIN'
|
173
|
+
meth.locale.should == 'en_US'
|
174
|
+
end
|
175
|
+
|
176
|
+
should 'convert methods to binary' do
|
177
|
+
meth = Protocol::Connection::Secure.new :challenge => 'secret'
|
178
|
+
meth.to_binary.should.be.kind_of? Buffer
|
179
|
+
|
180
|
+
meth.to_s.should == [ 10, 20, 6, 'secret' ].pack('nnNa*')
|
181
|
+
end
|
182
|
+
|
183
|
+
should 'convert binary to method' do
|
184
|
+
orig = Protocol::Connection::Secure.new :challenge => 'secret'
|
185
|
+
copy = Protocol.parse orig.to_binary
|
186
|
+
orig.should == copy
|
187
|
+
end
|
188
|
+
|
189
|
+
should 'convert headers to binary' do
|
190
|
+
head = Protocol::Header.new Protocol::Basic,
|
191
|
+
size = 5,
|
192
|
+
weight = 0,
|
193
|
+
:content_type => 'text/json',
|
194
|
+
:delivery_mode => 1,
|
195
|
+
:priority => 1
|
196
|
+
head.to_s.should == [ 60, weight, 0, size, 0b1001_1000_0000_0000, 9, 'text/json', 1, 1 ].pack('nnNNnCa*CC')
|
197
|
+
end
|
198
|
+
|
199
|
+
should 'convert binary to header' do
|
200
|
+
orig = Protocol::Header.new Protocol::Basic,
|
201
|
+
size = 5,
|
202
|
+
weight = 0,
|
203
|
+
:content_type => 'text/json',
|
204
|
+
:delivery_mode => 1,
|
205
|
+
:priority => 1
|
206
|
+
Protocol::Header.new(orig.to_binary).should == orig
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
data/lib/amqp/queue.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
module Carrot::AMQP
|
2
|
+
class Queue
|
3
|
+
attr_reader :name, :server, :carrot
|
4
|
+
attr_accessor :delivery_tag
|
5
|
+
|
6
|
+
def initialize(carrot, name, opts = {})
|
7
|
+
@server = carrot.server
|
8
|
+
@opts = opts
|
9
|
+
@name = name
|
10
|
+
@carrot = carrot
|
11
|
+
server.send_frame(
|
12
|
+
Protocol::Queue::Declare.new({ :queue => name, :nowait => true }.merge(opts))
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def pop(opts = {})
|
17
|
+
self.delivery_tag = nil
|
18
|
+
server.send_frame(
|
19
|
+
Protocol::Basic::Get.new({ :queue => name, :consumer_tag => name, :no_ack => !opts.delete(:ack), :nowait => true }.merge(opts))
|
20
|
+
)
|
21
|
+
method = server.next_method
|
22
|
+
return unless method.is_a?(Protocol::Basic::GetOk)
|
23
|
+
|
24
|
+
self.delivery_tag = method.delivery_tag
|
25
|
+
|
26
|
+
header = server.next_payload
|
27
|
+
msg = server.next_payload
|
28
|
+
raise 'unexpected length' if msg.length < header.size
|
29
|
+
|
30
|
+
return msg, header
|
31
|
+
end
|
32
|
+
|
33
|
+
def ack
|
34
|
+
server.send_frame(
|
35
|
+
Protocol::Basic::Ack.new(:delivery_tag => delivery_tag)
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
def publish(data, opts = {})
|
40
|
+
exchange.publish(data, opts)
|
41
|
+
end
|
42
|
+
|
43
|
+
def message_count
|
44
|
+
status.first
|
45
|
+
end
|
46
|
+
|
47
|
+
def consumer_count
|
48
|
+
status.last
|
49
|
+
end
|
50
|
+
|
51
|
+
def status(opts = {}, &blk)
|
52
|
+
server.send_frame(
|
53
|
+
Protocol::Queue::Declare.new({ :queue => name, :passive => true }.merge(opts))
|
54
|
+
)
|
55
|
+
method = server.next_method
|
56
|
+
[method.message_count, method.consumer_count]
|
57
|
+
end
|
58
|
+
|
59
|
+
def bind(exchange, opts = {})
|
60
|
+
exchange = exchange.respond_to?(:name) ? exchange.name : exchange
|
61
|
+
bindings[exchange] = opts
|
62
|
+
server.send_frame(
|
63
|
+
Protocol::Queue::Bind.new({ :queue => name, :exchange => exchange, :routing_key => opts.delete(:key), :nowait => true }.merge(opts))
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
def unbind(exchange, opts = {})
|
68
|
+
exchange = exchange.respond_to?(:name) ? exchange.name : exchange
|
69
|
+
bindings.delete(exchange)
|
70
|
+
|
71
|
+
server.send_frame(
|
72
|
+
Protocol::Queue::Unbind.new({
|
73
|
+
:queue => name, :exchange => exchange, :routing_key => opts.delete(:key), :nowait => true }.merge(opts)
|
74
|
+
)
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
def delete(opts = {})
|
79
|
+
server.send_frame(
|
80
|
+
Protocol::Queue::Delete.new({ :queue => name, :nowait => true }.merge(opts))
|
81
|
+
)
|
82
|
+
carrot.queues.delete(name)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
def exchange
|
87
|
+
@exchange ||= Exchange.new(carrot, :direct, '', :key => name)
|
88
|
+
end
|
89
|
+
|
90
|
+
def bindings
|
91
|
+
@bindings ||= {}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|