stompserver 0.9.7 → 0.9.8
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +8 -6
- data/Manifest.txt +25 -7
- data/README.txt +136 -19
- data/Rakefile +8 -8
- data/STATUS +5 -0
- data/bin/stompserver +43 -19
- data/client/README.txt +1 -0
- data/client/both.rb +25 -0
- data/client/consume.rb +14 -0
- data/client/send.rb +17 -0
- data/config/stompserver.conf +11 -0
- data/etc/passwd.example +3 -0
- data/lib/stomp_server.rb +132 -157
- data/lib/stomp_server/protocols/http.rb +128 -0
- data/lib/stomp_server/protocols/stomp.rb +186 -0
- data/lib/stomp_server/queue.rb +140 -0
- data/lib/stomp_server/queue/activerecord_queue.rb +104 -0
- data/lib/stomp_server/queue/ar_message.rb +5 -0
- data/lib/stomp_server/queue/dbm_queue.rb +68 -0
- data/lib/stomp_server/queue/file_queue.rb +47 -0
- data/lib/stomp_server/queue/memory_queue.rb +58 -0
- data/lib/stomp_server/queue_manager.rb +209 -0
- data/lib/stomp_server/stomp_auth.rb +22 -0
- data/lib/{stomp_frame.rb → stomp_server/stomp_frame.rb} +7 -7
- data/lib/stomp_server/stomp_id.rb +21 -0
- data/lib/stomp_server/stomp_user.rb +17 -0
- data/lib/stomp_server/test_server.rb +21 -0
- data/lib/{topic_manager.rb → stomp_server/topic_manager.rb} +14 -2
- data/test/tesly.rb +15 -0
- data/test/test_queue_manager.rb +39 -32
- data/test/test_stomp_frame.rb +3 -16
- data/test/test_topic_manager.rb +3 -4
- data/{test → test_todo}/test_stomp_server.rb +0 -27
- metadata +56 -23
- data/lib/frame_journal.rb +0 -135
- data/lib/queue_manager.rb +0 -81
- data/test/test_frame_journal.rb +0 -14
@@ -0,0 +1,128 @@
|
|
1
|
+
|
2
|
+
class Mongrel::HttpRequest
|
3
|
+
attr_reader :body, :params
|
4
|
+
|
5
|
+
def initialize(params, initial_body)
|
6
|
+
@params = params
|
7
|
+
@body = StringIO.new
|
8
|
+
@body.write params.http_body
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module StompServer
|
13
|
+
module StompServer::Protocols
|
14
|
+
|
15
|
+
class Http < EventMachine::Connection
|
16
|
+
|
17
|
+
def initialize *args
|
18
|
+
super
|
19
|
+
@buf = ''
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def post_init
|
24
|
+
@parser = Mongrel::HttpParser.new
|
25
|
+
@params = Mongrel::HttpParams.new
|
26
|
+
@nparsed = 0
|
27
|
+
@request = nil
|
28
|
+
@request_method = nil
|
29
|
+
@request_length = 0
|
30
|
+
@state = :headers
|
31
|
+
@headers_out = {'Content-Length' => 0, 'Content-Type' => 'text/plain; charset=UTF-8'}
|
32
|
+
end
|
33
|
+
|
34
|
+
def receive_data data
|
35
|
+
parse_request(data)
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_request data
|
39
|
+
@buf << data
|
40
|
+
case @state
|
41
|
+
when :headers
|
42
|
+
@nparsed = @parser.execute(@params, @buf, @nparsed)
|
43
|
+
if @parser.finished?
|
44
|
+
@request = Mongrel::HttpRequest.new(@params,@buf)
|
45
|
+
@request_method = @request.params[Mongrel::Const::REQUEST_METHOD]
|
46
|
+
content_length = @request.params[Mongrel::Const::CONTENT_LENGTH].to_i
|
47
|
+
@request_length = @nparsed + content_length
|
48
|
+
@remain = content_length - @request.params.http_body.length
|
49
|
+
if @remain <= 0
|
50
|
+
@buf = @buf[@request_length+1..-1] || ''
|
51
|
+
process_request
|
52
|
+
post_init
|
53
|
+
return
|
54
|
+
end
|
55
|
+
@request.body.write @request.params.http_body
|
56
|
+
@state = :body
|
57
|
+
end
|
58
|
+
when :body
|
59
|
+
@remain -= @request.body.write data[0...@remain]
|
60
|
+
if @remain <= 0
|
61
|
+
@buf = @buf[@request_length+1..-1] || ''
|
62
|
+
process_request
|
63
|
+
post_init
|
64
|
+
return
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def process_request
|
70
|
+
begin
|
71
|
+
@request.body.rewind
|
72
|
+
dest = @request.params[Mongrel::Const::REQUEST_PATH]
|
73
|
+
case @request_method
|
74
|
+
when 'PUT'
|
75
|
+
@frame = StompServer::StompFrame.new
|
76
|
+
@frame.command = 'SEND'
|
77
|
+
@frame.body = @request.body.read
|
78
|
+
@frame.headers['destination'] = dest
|
79
|
+
if @@queue_manager.enqueue(@frame)
|
80
|
+
create_response('200','Message Enqueued')
|
81
|
+
else
|
82
|
+
create_response('500','Error enqueueing message')
|
83
|
+
end
|
84
|
+
when 'GET'
|
85
|
+
if frame = @@queue_manager.dequeue(dest)
|
86
|
+
@headers_out['message-id'] = frame.headers['message-id']
|
87
|
+
create_response('200',frame.body)
|
88
|
+
else
|
89
|
+
create_response('404','No messages in queue')
|
90
|
+
end
|
91
|
+
else
|
92
|
+
create_response('500','Invalid Command')
|
93
|
+
end
|
94
|
+
rescue Exception => e
|
95
|
+
puts "err: #{e} #{e.backtrace.join("\n")}"
|
96
|
+
create_response('500',e)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def unbind
|
101
|
+
puts "Closing connection"
|
102
|
+
close_connection_after_writing
|
103
|
+
end
|
104
|
+
|
105
|
+
def create_response(code,response_text)
|
106
|
+
response = ''
|
107
|
+
@headers_out['Content-Length'] = response_text.size
|
108
|
+
|
109
|
+
case code
|
110
|
+
when '200'
|
111
|
+
response << "HTTP/1.1 200 OK\r\n"
|
112
|
+
when '500'
|
113
|
+
response << "HTTP/1.1 500 Server Error\r\n"
|
114
|
+
when '404'
|
115
|
+
response << "HTTP/1.1 404 Message Not Found\r\n"
|
116
|
+
end
|
117
|
+
@headers_out.each_pair do |key, value|
|
118
|
+
response << "#{key}:#{value}\r\n"
|
119
|
+
end
|
120
|
+
response << "\r\n"
|
121
|
+
response << response_text
|
122
|
+
send_data(response)
|
123
|
+
unbind if @request.params['HTTP_CONNECTION'] == 'close'
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
|
2
|
+
module StompServer
|
3
|
+
module StompServer::Protocols
|
4
|
+
VALID_COMMANDS = [:connect, :send, :subscribe, :unsubscribe, :begin, :commit, :abort, :ack, :disconnect]
|
5
|
+
|
6
|
+
class Stomp < EventMachine::Connection
|
7
|
+
|
8
|
+
def initialize *args
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def post_init
|
13
|
+
@sfr = StompServer::StompFrameRecognizer.new
|
14
|
+
@transactions = {}
|
15
|
+
@connected = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def receive_data(data)
|
19
|
+
stomp_receive_data(data)
|
20
|
+
end
|
21
|
+
|
22
|
+
def stomp_receive_data(data)
|
23
|
+
begin
|
24
|
+
puts "receive_data: #{data.inspect}" if $DEBUG
|
25
|
+
@sfr << data
|
26
|
+
process_frames
|
27
|
+
rescue Exception => e
|
28
|
+
puts "err: #{e} #{e.backtrace.join("\n")}"
|
29
|
+
send_error(e.to_s)
|
30
|
+
close_connection_after_writing
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def stomp_receive_frame(frame)
|
35
|
+
begin
|
36
|
+
puts "receive_frame: #{frame.inspect}" if $DEBUG
|
37
|
+
process_frame(frame)
|
38
|
+
rescue Exception => e
|
39
|
+
puts "err: #{e} #{e.backtrace.join("\n")}"
|
40
|
+
send_error(e.to_s)
|
41
|
+
close_connection_after_writing
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def process_frames
|
46
|
+
frame = nil
|
47
|
+
process_frame(frame) while frame = @sfr.frames.shift
|
48
|
+
end
|
49
|
+
|
50
|
+
def process_frame(frame)
|
51
|
+
cmd = frame.command.downcase.to_sym
|
52
|
+
raise "Unhandled frame: #{cmd}" unless VALID_COMMANDS.include?(cmd)
|
53
|
+
raise "Not connected" if !@connected && cmd != :connect
|
54
|
+
|
55
|
+
# I really like this code, but my needs are a little trickier
|
56
|
+
#
|
57
|
+
|
58
|
+
if trans = frame.headers['transaction']
|
59
|
+
handle_transaction(frame, trans, cmd)
|
60
|
+
else
|
61
|
+
cmd = :sendmsg if cmd == :send
|
62
|
+
send(cmd, frame)
|
63
|
+
end
|
64
|
+
|
65
|
+
send_receipt(frame.headers['receipt']) if frame.headers['receipt']
|
66
|
+
end
|
67
|
+
|
68
|
+
def handle_transaction(frame, trans, cmd)
|
69
|
+
if [:begin, :commit, :abort].include?(cmd)
|
70
|
+
send(cmd, frame, trans)
|
71
|
+
else
|
72
|
+
raise "transaction does not exist" unless @transactions.has_key?(trans)
|
73
|
+
@transactions[trans] << frame
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def connect(frame)
|
78
|
+
if @@auth_required
|
79
|
+
unless frame.headers['login'] and frame.headers['passcode'] and @@stompauth.authorized[frame.headers['login']] == frame.headers['passcode']
|
80
|
+
raise "Invalid Login"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
puts "Connecting" if $DEBUG
|
84
|
+
response = StompServer::StompFrame.new("CONNECTED", {'session' => 'wow'})
|
85
|
+
stomp_send_data(response)
|
86
|
+
@connected = true
|
87
|
+
end
|
88
|
+
|
89
|
+
def sendmsg(frame)
|
90
|
+
# set message id
|
91
|
+
if frame.dest.match(%r|^/queue|)
|
92
|
+
@@queue_manager.sendmsg(frame)
|
93
|
+
else
|
94
|
+
frame.headers['message-id'] = "msg-#stompcma-#{@@topic_manager.next_index}"
|
95
|
+
@@topic_manager.sendmsg(frame)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def subscribe(frame)
|
100
|
+
use_ack = false
|
101
|
+
use_ack = true if frame.headers['ack'] == 'client'
|
102
|
+
if frame.dest =~ %r|^/queue|
|
103
|
+
@@queue_manager.subscribe(frame.dest, self,use_ack)
|
104
|
+
else
|
105
|
+
@@topic_manager.subscribe(frame.dest, self)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def unsubscribe(frame)
|
110
|
+
if frame.dest =~ %r|^/queue|
|
111
|
+
@@queue_manager.unsubscribe(frame.dest,self)
|
112
|
+
else
|
113
|
+
@@topic_manager.unsubscribe(frame.dest,self)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def begin(frame, trans=nil)
|
118
|
+
raise "Missing transaction" unless trans
|
119
|
+
raise "transaction exists" if @transactions.has_key?(trans)
|
120
|
+
@transactions[trans] = []
|
121
|
+
end
|
122
|
+
|
123
|
+
def commit(frame, trans=nil)
|
124
|
+
raise "Missing transaction" unless trans
|
125
|
+
raise "transaction does not exist" unless @transactions.has_key?(trans)
|
126
|
+
|
127
|
+
(@transactions[trans]).each do |frame|
|
128
|
+
frame.headers.delete('transaction')
|
129
|
+
process_frame(frame)
|
130
|
+
end
|
131
|
+
@transactions.delete(trans)
|
132
|
+
end
|
133
|
+
|
134
|
+
def abort(frame, trans=nil)
|
135
|
+
raise "Missing transaction" unless trans
|
136
|
+
raise "transaction does not exist" unless @transactions.has_key?(trans)
|
137
|
+
@transactions.delete(trans)
|
138
|
+
end
|
139
|
+
|
140
|
+
def ack(frame)
|
141
|
+
@@queue_manager.ack(self, frame)
|
142
|
+
end
|
143
|
+
|
144
|
+
def disconnect(frame)
|
145
|
+
puts "Polite disconnect" if $DEBUG
|
146
|
+
close_connection_after_writing
|
147
|
+
end
|
148
|
+
|
149
|
+
def unbind
|
150
|
+
p "Unbind called" if $DEBUG
|
151
|
+
@connected = false
|
152
|
+
@@queue_manager.disconnect(self)
|
153
|
+
@@topic_manager.disconnect(self)
|
154
|
+
end
|
155
|
+
|
156
|
+
def connected?
|
157
|
+
@connected
|
158
|
+
end
|
159
|
+
|
160
|
+
def send_message(msg)
|
161
|
+
msg.command = "MESSAGE"
|
162
|
+
stomp_send_data(msg)
|
163
|
+
end
|
164
|
+
|
165
|
+
def send_receipt(id)
|
166
|
+
send_frame("RECEIPT", { 'receipt-id' => id})
|
167
|
+
end
|
168
|
+
|
169
|
+
def send_error(msg)
|
170
|
+
send_frame("ERROR",{'message' => 'See below'},msg)
|
171
|
+
end
|
172
|
+
|
173
|
+
def stomp_send_data(frame)
|
174
|
+
send_data(frame.to_s)
|
175
|
+
puts "Sending frame #{frame.to_s}" if $DEBUG
|
176
|
+
end
|
177
|
+
|
178
|
+
def send_frame(command, headers={}, body='')
|
179
|
+
headers['content-length'] = body.size.to_s
|
180
|
+
response = StompServer::StompFrame.new(command, headers, body)
|
181
|
+
stomp_send_data(response)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
|
2
|
+
module StompServer
|
3
|
+
class Queue
|
4
|
+
|
5
|
+
def initialize(directory='.stompserver', delete_empty=true)
|
6
|
+
@stompid = StompServer::StompId.new
|
7
|
+
@delete_empty = delete_empty
|
8
|
+
@directory = directory
|
9
|
+
Dir.mkdir(@directory) unless File.directory?(@directory)
|
10
|
+
if File.exists?("#{@directory}/qinfo")
|
11
|
+
qinfo = Hash.new
|
12
|
+
File.open("#{@directory}/qinfo", "rb") { |f| qinfo = Marshal.load(f.read)}
|
13
|
+
@queues = qinfo[:queues]
|
14
|
+
@frames = qinfo[:frames]
|
15
|
+
else
|
16
|
+
@queues = Hash.new
|
17
|
+
@frames = Hash.new
|
18
|
+
end
|
19
|
+
|
20
|
+
@queues.keys.each do |dest|
|
21
|
+
puts "Queue #{dest} size=#{@queues[dest][:size]} enqueued=#{@queues[dest][:enqueued]} dequeued=#{@queues[dest][:dequeued]}" if $DEBUG
|
22
|
+
end
|
23
|
+
|
24
|
+
puts "Queue initialized in #{@directory}"
|
25
|
+
|
26
|
+
# Cleanup dead queues and save the state of the queues every so often. Alternatively we could save the queue state every X number
|
27
|
+
# of frames that are put in the queue. Should probably also read it after saving it to confirm integrity.
|
28
|
+
# Removed, this badly corrupt the queue when stopping with messages
|
29
|
+
#EventMachine::add_periodic_timer 1800, proc {@queues.keys.each {|dest| close_queue(dest)};save_queue_state }
|
30
|
+
end
|
31
|
+
|
32
|
+
def stop
|
33
|
+
puts "Shutting down Queue"
|
34
|
+
|
35
|
+
@queues.keys.each {|dest| close_queue(dest)}
|
36
|
+
@queues.keys.each do |dest|
|
37
|
+
puts "Queue #{dest} size=#{@queues[dest][:size]} enqueued=#{@queues[dest][:enqueued]} dequeued=#{@queues[dest][:dequeued]}" if $DEBUG
|
38
|
+
end
|
39
|
+
save_queue_state
|
40
|
+
end
|
41
|
+
|
42
|
+
def save_queue_state
|
43
|
+
puts "Saving Queue State" if $DEBUG
|
44
|
+
qinfo = {:queues => @queues, :frames => @frames}
|
45
|
+
File.open("#{@directory}/qinfo", "wb") { |f| f.write Marshal.dump(qinfo)}
|
46
|
+
end
|
47
|
+
|
48
|
+
def monitor
|
49
|
+
stats = Hash.new
|
50
|
+
@queues.keys.each do |dest|
|
51
|
+
stats[dest] = {'size' => @queues[dest][:size], 'enqueued' => @queues[dest][:enqueued], 'dequeued' => @queues[dest][:dequeued]}
|
52
|
+
end
|
53
|
+
stats
|
54
|
+
end
|
55
|
+
|
56
|
+
def close_queue(dest)
|
57
|
+
if @queues[dest][:size] == 0 and @queues[dest][:frames].size == 0 and @delete_empty
|
58
|
+
_close_queue(dest)
|
59
|
+
@queues.delete(dest)
|
60
|
+
@frames.delete(dest)
|
61
|
+
puts "Queue #{dest} removed." if $DEBUG
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def open_queue(dest)
|
66
|
+
@queues[dest] = Hash.new
|
67
|
+
@frames[dest] = Hash.new
|
68
|
+
@queues[dest][:size] = 0
|
69
|
+
@queues[dest][:frames] = Array.new
|
70
|
+
@queues[dest][:msgid] = 1
|
71
|
+
@queues[dest][:enqueued] = 0
|
72
|
+
@queues[dest][:dequeued] = 0
|
73
|
+
@queues[dest][:exceptions] = 0
|
74
|
+
_open_queue(dest)
|
75
|
+
puts "Created queue #{dest}" if $DEBUG
|
76
|
+
end
|
77
|
+
|
78
|
+
def requeue(dest,frame)
|
79
|
+
open_queue(dest) unless @queues.has_key?(dest)
|
80
|
+
msgid = frame.headers['message-id']
|
81
|
+
if frame.headers['max-exceptions'] and @frames[dest][msgid][:exceptions] >= frame.headers['max-exceptions'].to_i
|
82
|
+
enqueue("/queue/deadletter",frame)
|
83
|
+
return
|
84
|
+
end
|
85
|
+
writeframe(dest,frame,msgid)
|
86
|
+
@queues[dest][:frames].unshift(msgid)
|
87
|
+
@frames[dest][msgid][:exceptions] += 1
|
88
|
+
@queues[dest][:dequeued] -= 1
|
89
|
+
@queues[dest][:exceptions] += 1
|
90
|
+
@queues[dest][:size] += 1
|
91
|
+
save_queue_state
|
92
|
+
return true
|
93
|
+
end
|
94
|
+
|
95
|
+
def enqueue(dest,frame)
|
96
|
+
open_queue(dest) unless @queues.has_key?(dest)
|
97
|
+
msgid = assign_id(frame, dest)
|
98
|
+
writeframe(dest,frame,msgid)
|
99
|
+
@queues[dest][:frames].push(msgid)
|
100
|
+
@frames[dest][msgid] = Hash.new
|
101
|
+
@frames[dest][msgid][:exceptions] =0
|
102
|
+
@frames[dest][msgid][:client_id] = frame.headers['client-id'] if frame.headers['client-id']
|
103
|
+
@frames[dest][msgid][:expires] = frame.headers['expires'] if frame.headers['expires']
|
104
|
+
@queues[dest][:msgid] += 1
|
105
|
+
@queues[dest][:enqueued] += 1
|
106
|
+
@queues[dest][:size] += 1
|
107
|
+
save_queue_state
|
108
|
+
return true
|
109
|
+
end
|
110
|
+
|
111
|
+
def dequeue(dest)
|
112
|
+
return false unless message_for?(dest)
|
113
|
+
msgid = @queues[dest][:frames].shift
|
114
|
+
frame = readframe(dest,msgid)
|
115
|
+
@queues[dest][:size] -= 1
|
116
|
+
@queues[dest][:dequeued] += 1
|
117
|
+
@queues[dest].delete(msgid)
|
118
|
+
close_queue(dest)
|
119
|
+
save_queue_state
|
120
|
+
return frame
|
121
|
+
end
|
122
|
+
|
123
|
+
def message_for?(dest)
|
124
|
+
return (@queues.has_key?(dest) and (!@queues[dest][:frames].empty?))
|
125
|
+
end
|
126
|
+
|
127
|
+
def writeframe(dest,frame,msgid)
|
128
|
+
_writeframe(dest,frame,msgid)
|
129
|
+
end
|
130
|
+
|
131
|
+
def readframe(dest,msgid)
|
132
|
+
_readframe(dest,msgid)
|
133
|
+
end
|
134
|
+
|
135
|
+
def assign_id(frame, dest)
|
136
|
+
frame.headers['message-id'] = @stompid[@queues[dest][:msgid]]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|