stompserver 0.9.7 → 0.9.8
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/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
|
+
|