sa-carrot 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/LICENSE +20 -0
- data/README.markdown +34 -0
- data/Rakefile +40 -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 +97 -0
- data/lib/amqp/server.rb +185 -0
- data/lib/amqp/spec.rb +820 -0
- data/lib/carrot.rb +87 -0
- data/lib/examples/simple_pop.rb +13 -0
- data/protocol/amqp-0.8.json +617 -0
- data/protocol/amqp-0.8.xml +3908 -0
- data/protocol/codegen.rb +173 -0
- data/protocol/doc.txt +281 -0
- data/sa-carrot.gemspec +63 -0
- data/test/carrot_test.rb +15 -0
- data/test/test_helper.rb +18 -0
- metadata +78 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
module Carrot::AMQP
|
2
|
+
class Exchange
|
3
|
+
attr_reader :server, :type, :name, :opts, :key, :carrot
|
4
|
+
|
5
|
+
def initialize(carrot, type, name, opts = {})
|
6
|
+
@server, @type, @name, @opts = carrot.server, type, name, opts
|
7
|
+
@key = opts[:key]
|
8
|
+
@carrot = carrot
|
9
|
+
|
10
|
+
unless name == "amq.#{type}" or name == ''
|
11
|
+
server.send_frame(
|
12
|
+
Protocol::Exchange::Declare.new(
|
13
|
+
{ :exchange => name, :type => type, :nowait => true }.merge(opts)
|
14
|
+
)
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
attr_reader :name, :type, :key
|
19
|
+
|
20
|
+
def publish(data, opts = {})
|
21
|
+
out = []
|
22
|
+
|
23
|
+
out << Protocol::Basic::Publish.new(
|
24
|
+
{ :exchange => name, :routing_key => opts.delete(:key) || key }.merge(opts)
|
25
|
+
)
|
26
|
+
data = data.to_s
|
27
|
+
out << Protocol::Header.new(
|
28
|
+
Protocol::Basic,
|
29
|
+
data.length, {
|
30
|
+
:content_type => 'application/octet-stream',
|
31
|
+
:delivery_mode => (opts.delete(:persistent) ? 2 : 1),
|
32
|
+
:priority => 0
|
33
|
+
}.merge(opts)
|
34
|
+
)
|
35
|
+
out << Frame::Body.new(data)
|
36
|
+
|
37
|
+
server.send_frame(*out)
|
38
|
+
end
|
39
|
+
|
40
|
+
def delete(opts = {})
|
41
|
+
server.send_frame(
|
42
|
+
Protocol::Exchange::Delete.new({ :exchange => name, :nowait => true }.merge(opts))
|
43
|
+
)
|
44
|
+
carrot.exchanges.delete(name)
|
45
|
+
end
|
46
|
+
|
47
|
+
def reset
|
48
|
+
initialize(server, type, name, opts)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
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,97 @@
|
|
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
|
+
|
28
|
+
msg = ''
|
29
|
+
while msg.length < header.size
|
30
|
+
msg << server.next_payload
|
31
|
+
end
|
32
|
+
|
33
|
+
msg
|
34
|
+
end
|
35
|
+
|
36
|
+
def ack
|
37
|
+
server.send_frame(
|
38
|
+
Protocol::Basic::Ack.new(:delivery_tag => delivery_tag)
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def publish(data, opts = {})
|
43
|
+
exchange.publish(data, opts)
|
44
|
+
end
|
45
|
+
|
46
|
+
def message_count
|
47
|
+
status.first
|
48
|
+
end
|
49
|
+
|
50
|
+
def consumer_count
|
51
|
+
status.last
|
52
|
+
end
|
53
|
+
|
54
|
+
def status(opts = {}, &blk)
|
55
|
+
server.send_frame(
|
56
|
+
Protocol::Queue::Declare.new({ :queue => name, :passive => true }.merge(opts))
|
57
|
+
)
|
58
|
+
method = server.next_method
|
59
|
+
[method.message_count, method.consumer_count]
|
60
|
+
end
|
61
|
+
|
62
|
+
def bind(exchange, opts = {})
|
63
|
+
exchange = exchange.respond_to?(:name) ? exchange.name : exchange
|
64
|
+
bindings[exchange] = opts
|
65
|
+
server.send_frame(
|
66
|
+
Protocol::Queue::Bind.new({ :queue => name, :exchange => exchange, :routing_key => opts.delete(:key), :nowait => true }.merge(opts))
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
def unbind(exchange, opts = {})
|
71
|
+
exchange = exchange.respond_to?(:name) ? exchange.name : exchange
|
72
|
+
bindings.delete(exchange)
|
73
|
+
|
74
|
+
server.send_frame(
|
75
|
+
Protocol::Queue::Unbind.new({
|
76
|
+
:queue => name, :exchange => exchange, :routing_key => opts.delete(:key), :nowait => true }.merge(opts)
|
77
|
+
)
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
def delete(opts = {})
|
82
|
+
server.send_frame(
|
83
|
+
Protocol::Queue::Delete.new({ :queue => name, :nowait => true }.merge(opts))
|
84
|
+
)
|
85
|
+
carrot.queues.delete(name)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
def exchange
|
90
|
+
@exchange ||= Exchange.new(carrot, :direct, '', :key => name)
|
91
|
+
end
|
92
|
+
|
93
|
+
def bindings
|
94
|
+
@bindings ||= {}
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|