famoseagle-carrot 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +24 -0
- data/VERSION.yml +4 -0
- data/lib/amqp/buffer.rb +395 -0
- data/lib/amqp/exchange.rb +47 -0
- data/lib/amqp/frame.rb +130 -0
- data/lib/amqp/header.rb +27 -0
- data/lib/amqp/protocol.rb +209 -0
- data/lib/amqp/queue.rb +70 -0
- data/lib/amqp/server.rb +213 -0
- data/lib/amqp/spec.rb +832 -0
- data/lib/carrot.rb +64 -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
@@ -0,0 +1,209 @@
|
|
1
|
+
module 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,70 @@
|
|
1
|
+
module AMQP
|
2
|
+
class Queue
|
3
|
+
attr_reader :name, :server
|
4
|
+
attr_accessor :delivery_tag
|
5
|
+
|
6
|
+
def initialize(server, name, opts = {})
|
7
|
+
@server = server
|
8
|
+
@opts = opts
|
9
|
+
@name = name
|
10
|
+
server.send_frame(
|
11
|
+
Protocol::Queue::Declare.new({ :queue => name, :nowait => true }.merge(opts))
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
def delete(opts = {})
|
16
|
+
server.send_frame(
|
17
|
+
Protocol::Queue::Delete.new({ :queue => name, :nowait => true }.merge(opts))
|
18
|
+
)
|
19
|
+
pp server.next_method
|
20
|
+
end
|
21
|
+
|
22
|
+
def pop(opts = {})
|
23
|
+
self.delivery_tag = nil
|
24
|
+
server.send_frame(
|
25
|
+
Protocol::Basic::Get.new({ :queue => name, :consumer_tag => name, :no_ack => !opts.delete(:ack), :nowait => true }.merge(opts))
|
26
|
+
)
|
27
|
+
method = server.next_method
|
28
|
+
return if method.is_a?(Protocol::Basic::GetEmpty)
|
29
|
+
|
30
|
+
self.delivery_tag = method.delivery_tag
|
31
|
+
|
32
|
+
header = server.next_payload
|
33
|
+
msg = server.next_payload
|
34
|
+
raise 'unexpected length' if msg.length < header.size
|
35
|
+
|
36
|
+
msg
|
37
|
+
end
|
38
|
+
|
39
|
+
def ack
|
40
|
+
server.send_frame(
|
41
|
+
Protocol::Basic::Ack.new(:delivery_tag => delivery_tag)
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def publish(data, opts = {})
|
46
|
+
exchange.publish(data, opts)
|
47
|
+
end
|
48
|
+
|
49
|
+
def message_count
|
50
|
+
status.first
|
51
|
+
end
|
52
|
+
|
53
|
+
def consumer_count
|
54
|
+
status.last
|
55
|
+
end
|
56
|
+
|
57
|
+
def status(opts = {}, &blk)
|
58
|
+
server.send_frame(
|
59
|
+
Protocol::Queue::Declare.new({ :queue => name, :passive => true }.merge(opts))
|
60
|
+
)
|
61
|
+
method = server.next_method
|
62
|
+
[method.message_count, method.consumer_count]
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
def exchange
|
67
|
+
@exchange ||= Exchange.new(server, :direct, '', :key => name)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/amqp/server.rb
ADDED
@@ -0,0 +1,213 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'thread'
|
3
|
+
require 'timeout'
|
4
|
+
|
5
|
+
module AMQP
|
6
|
+
class Server
|
7
|
+
CONNECT_TIMEOUT = 1.0
|
8
|
+
RETRY_DELAY = 10.0
|
9
|
+
DEFAULT_PORT = 5672
|
10
|
+
|
11
|
+
attr_reader :host, :port, :status
|
12
|
+
attr_accessor :retry_at, :channel, :ticket
|
13
|
+
|
14
|
+
class ConnectionError < StandardError; end
|
15
|
+
class ServerError < StandardError; end
|
16
|
+
class ClientError < StandardError; end
|
17
|
+
class ServerDown < StandardError; end
|
18
|
+
class ProtocolError < StandardError; end
|
19
|
+
|
20
|
+
def initialize(opts = {})
|
21
|
+
@host = opts[:host] || 'localhost'
|
22
|
+
@port = opts[:port] || DEFAULT_PORT
|
23
|
+
@user = opts[:user] || 'guest'
|
24
|
+
@pass = opts[:pass] || 'guest'
|
25
|
+
@vhost = opts[:vhost] || '/'
|
26
|
+
@insist = opts[:insist]
|
27
|
+
@status = 'NOT CONNECTED'
|
28
|
+
|
29
|
+
@multithread = opts[:multithread]
|
30
|
+
start_session
|
31
|
+
end
|
32
|
+
|
33
|
+
def start_session
|
34
|
+
@channel = 0
|
35
|
+
write(HEADER)
|
36
|
+
write([1, 1, VERSION_MAJOR, VERSION_MINOR].pack('C4'))
|
37
|
+
raise ProtocolError, 'bad start connection' unless next_method.is_a?(Protocol::Connection::Start)
|
38
|
+
|
39
|
+
send_frame(
|
40
|
+
Protocol::Connection::StartOk.new(
|
41
|
+
{:platform => 'Ruby', :product => 'Carrot', :information => 'http://github.com/famosagle/carrot', :version => VERSION},
|
42
|
+
'AMQPLAIN',
|
43
|
+
{:LOGIN => @user, :PASSWORD => @pass},
|
44
|
+
'en_US'
|
45
|
+
)
|
46
|
+
)
|
47
|
+
|
48
|
+
if next_method.is_a?(Protocol::Connection::Tune)
|
49
|
+
send_frame(
|
50
|
+
Protocol::Connection::TuneOk.new( :channel_max => 0, :frame_max => 131072, :heartbeat => 0)
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
send_frame(
|
55
|
+
Protocol::Connection::Open.new(:virtual_host => @vhost, :capabilities => '', :insist => @insist)
|
56
|
+
)
|
57
|
+
raise ProtocolError, 'bad open connection' unless next_method.is_a?(Protocol::Connection::OpenOk)
|
58
|
+
|
59
|
+
@channel = 1
|
60
|
+
send_frame(Protocol::Channel::Open.new)
|
61
|
+
raise ProtocolError, "cannot open channel #{channel}" unless next_method.is_a?(Protocol::Channel::OpenOk)
|
62
|
+
|
63
|
+
send_frame(
|
64
|
+
Protocol::Access::Request.new(:realm => '/data', :read => true, :write => true, :active => true, :passive => true)
|
65
|
+
)
|
66
|
+
method = next_method
|
67
|
+
raise ProtocolError, 'access denied' unless method.is_a?(Protocol::Access::RequestOk)
|
68
|
+
self.ticket = method.ticket
|
69
|
+
end
|
70
|
+
|
71
|
+
def send_frame(*args)
|
72
|
+
args.each do |data|
|
73
|
+
data.ticket = ticket if ticket and data.respond_to?(:ticket=)
|
74
|
+
data = data.to_frame(channel) unless data.is_a?(Frame)
|
75
|
+
data.channel = channel
|
76
|
+
|
77
|
+
log :send, data
|
78
|
+
write(data.to_s)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def next_frame
|
83
|
+
frame = Frame.get(self)
|
84
|
+
log :received, frame
|
85
|
+
frame
|
86
|
+
end
|
87
|
+
|
88
|
+
def next_method
|
89
|
+
next_payload
|
90
|
+
end
|
91
|
+
|
92
|
+
def next_payload
|
93
|
+
next_frame.payload
|
94
|
+
end
|
95
|
+
|
96
|
+
def close
|
97
|
+
send_frame(
|
98
|
+
Protocol::Channel::Close.new(:reply_code => 200, :reply_text => 'bye', :method_id => 0, :class_id => 0)
|
99
|
+
)
|
100
|
+
puts "Error closing channel #{channel}" unless next_method.is_a?(Protocol::Channel::CloseOk)
|
101
|
+
|
102
|
+
self.channel = 0
|
103
|
+
send_frame(
|
104
|
+
Protocol::Connection::Close.new(:reply_code => 200, :reply_text => 'Goodbye', :class_id => 0, :method_id => 0)
|
105
|
+
)
|
106
|
+
puts "Error closing connection" unless next_method.is_a?(Protocol::Connection::CloseOk)
|
107
|
+
|
108
|
+
close_socket
|
109
|
+
end
|
110
|
+
|
111
|
+
def read(*args)
|
112
|
+
with_socket_management do |socket|
|
113
|
+
socket.read(*args)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def write(*args)
|
118
|
+
with_socket_management do |socket|
|
119
|
+
socket.write(*args)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def with_socket_management(&block)
|
126
|
+
retried = false
|
127
|
+
begin
|
128
|
+
mutex.lock if multithread?
|
129
|
+
yield socket
|
130
|
+
|
131
|
+
rescue ClientError, ServerError, SocketError, SystemCallError, IOError => error
|
132
|
+
if not retried
|
133
|
+
# Close the socket and retry once.
|
134
|
+
close_socket
|
135
|
+
#start_session
|
136
|
+
retried = true
|
137
|
+
retry
|
138
|
+
else
|
139
|
+
# Mark the server dead and raise an error.
|
140
|
+
close(error.message)
|
141
|
+
|
142
|
+
# Reraise as a ConnectionError
|
143
|
+
new_error = ConnectionError.new("#{error.class}: #{error.message}")
|
144
|
+
new_error.set_backtrace(error.backtrace)
|
145
|
+
raise new_error
|
146
|
+
end
|
147
|
+
ensure
|
148
|
+
mutex.unlock if multithread?
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def socket
|
153
|
+
return @socket if @socket and not @socket.closed?
|
154
|
+
raise ServerDown, "will retry at #{retry_at}" unless retry?
|
155
|
+
|
156
|
+
begin
|
157
|
+
# Attempt to connect.
|
158
|
+
mutex.lock if multithread?
|
159
|
+
@socket = timeout(CONNECT_TIMEOUT) do
|
160
|
+
TCPSocket.new(host, port)
|
161
|
+
end
|
162
|
+
|
163
|
+
if Socket.constants.include? 'TCP_NODELAY'
|
164
|
+
@socket.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
|
165
|
+
end
|
166
|
+
@retry_at = nil
|
167
|
+
@status = 'CONNECTED'
|
168
|
+
rescue SocketError, SystemCallError, IOError, Timeout::Error => e
|
169
|
+
close_socket
|
170
|
+
raise ServerDown, e.message
|
171
|
+
ensure
|
172
|
+
mutex.unlock if multithread?
|
173
|
+
end
|
174
|
+
|
175
|
+
@socket
|
176
|
+
end
|
177
|
+
|
178
|
+
def multithread?
|
179
|
+
@multithread
|
180
|
+
end
|
181
|
+
|
182
|
+
def retry?
|
183
|
+
@retry_at.nil? or @retry_at < Time.now
|
184
|
+
end
|
185
|
+
|
186
|
+
def unexpected_eof!
|
187
|
+
raise ConnectionError, 'unexpected end of file'
|
188
|
+
end
|
189
|
+
|
190
|
+
def close_socket(reason=nil)
|
191
|
+
# Close the socket. The server is not considered dead.
|
192
|
+
mutex.lock if multithread?
|
193
|
+
@socket.close if @socket and not @socket.closed?
|
194
|
+
@socket = nil
|
195
|
+
@retry_at = nil
|
196
|
+
@status = "NOT CONNECTED"
|
197
|
+
ensure
|
198
|
+
mutex.unlock if multithread?
|
199
|
+
end
|
200
|
+
|
201
|
+
def mutex
|
202
|
+
@mutex ||= Mutex.new
|
203
|
+
end
|
204
|
+
|
205
|
+
def log(*args)
|
206
|
+
return unless Carrot.logging?
|
207
|
+
require 'pp'
|
208
|
+
pp args
|
209
|
+
puts
|
210
|
+
end
|
211
|
+
|
212
|
+
end
|
213
|
+
end
|