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.
@@ -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
+