stompserver_ng 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/History.txt +159 -0
  2. data/Manifest.txt +71 -0
  3. data/README.txt +172 -0
  4. data/Rakefile +38 -0
  5. data/STATUS +5 -0
  6. data/bin/stompserver_ng +63 -0
  7. data/client/README.txt +1 -0
  8. data/client/both.rb +25 -0
  9. data/client/consume.rb +14 -0
  10. data/client/send.rb +17 -0
  11. data/config/stompserver_ng.conf +11 -0
  12. data/etc/19xcompat/notes.txt +223 -0
  13. data/etc/arutils/README-activerecord.txt +78 -0
  14. data/etc/arutils/cre_mysql.rb +34 -0
  15. data/etc/arutils/cre_postgres.rb +33 -0
  16. data/etc/arutils/cre_sqlite3.rb +28 -0
  17. data/etc/arutils/mysql_boot.sql +12 -0
  18. data/etc/arutils/postgres_boot.sql +14 -0
  19. data/etc/database.mysql.yml +9 -0
  20. data/etc/database.postgres.yml +9 -0
  21. data/etc/passwd.example +3 -0
  22. data/etc/ppqinfo.rb +15 -0
  23. data/etc/runserver.sh +17 -0
  24. data/etc/stompserver_ng +50 -0
  25. data/etc/stompserver_ng.conf +13 -0
  26. data/lib/stomp_server_ng.rb +471 -0
  27. data/lib/stomp_server_ng/protocols/http.rb +128 -0
  28. data/lib/stomp_server_ng/protocols/stomp.rb +407 -0
  29. data/lib/stomp_server_ng/qmonitor.rb +58 -0
  30. data/lib/stomp_server_ng/queue.rb +248 -0
  31. data/lib/stomp_server_ng/queue/activerecord_queue.rb +118 -0
  32. data/lib/stomp_server_ng/queue/ar_message.rb +21 -0
  33. data/lib/stomp_server_ng/queue/ar_reconnect.rb +18 -0
  34. data/lib/stomp_server_ng/queue/dbm_queue.rb +72 -0
  35. data/lib/stomp_server_ng/queue/file_queue.rb +56 -0
  36. data/lib/stomp_server_ng/queue/memory_queue.rb +64 -0
  37. data/lib/stomp_server_ng/queue_manager.rb +302 -0
  38. data/lib/stomp_server_ng/stomp_auth.rb +26 -0
  39. data/lib/stomp_server_ng/stomp_frame.rb +32 -0
  40. data/lib/stomp_server_ng/stomp_frame_recognizer.rb +77 -0
  41. data/lib/stomp_server_ng/stomp_id.rb +32 -0
  42. data/lib/stomp_server_ng/stomp_user.rb +17 -0
  43. data/lib/stomp_server_ng/test_server.rb +21 -0
  44. data/lib/stomp_server_ng/topic_manager.rb +46 -0
  45. data/setup.rb +1585 -0
  46. data/stompserver_ng.gemspec +136 -0
  47. data/test/devserver/props.yaml +5 -0
  48. data/test/devserver/runserver.sh +16 -0
  49. data/test/devserver/stompserver_ng.dbm.conf +12 -0
  50. data/test/devserver/stompserver_ng.file.conf +12 -0
  51. data/test/devserver/stompserver_ng.memory.conf +12 -0
  52. data/test/noserver/mocklogger.rb +12 -0
  53. data/test/noserver/test_queue_manager.rb +134 -0
  54. data/test/noserver/test_stomp_frame.rb +138 -0
  55. data/test/noserver/test_topic_manager.rb +79 -0
  56. data/test/noserver/ts_all_no_server.rb +12 -0
  57. data/test/props.yaml +5 -0
  58. data/test/runalltests.sh +14 -0
  59. data/test/runtest.sh +4 -0
  60. data/test/test_0000_base.rb +107 -0
  61. data/test/test_0001_conn.rb +47 -0
  62. data/test/test_0002_conn_sr.rb +94 -0
  63. data/test/test_0006_client.rb +41 -0
  64. data/test/test_0011_send_recv.rb +74 -0
  65. data/test/test_0015_ack_conn.rb +78 -0
  66. data/test/test_0017_ack_client.rb +78 -0
  67. data/test/test_0019_ack_no_ack.rb +145 -0
  68. data/test/test_0022_ack_noack_conn.rb +123 -0
  69. data/test/test_0030_subscr_id.rb +44 -0
  70. data/test/test_0040_receipt_conn.rb +87 -0
  71. data/test/ts_all_server.rb +10 -0
  72. metadata +196 -0
@@ -0,0 +1,72 @@
1
+
2
+ module StompServer
3
+ class DBMQueue < Queue
4
+
5
+ def initialize *args
6
+ super
7
+
8
+ @@log = Logger.new(STDOUT)
9
+ @@log.level = StompServer::LogHelper.get_loglevel()
10
+
11
+ # Please don't use dbm files for storing large frames, it's problematic at best and uses large amounts of memory.
12
+ # sdbm croaks on marshalled data that contains certain characters, so we don't use it at all
13
+ @dbm = false
14
+ if RUBY_PLATFORM =~/linux|bsd/
15
+ types = ['bdb','dbm','gdbm']
16
+ else
17
+ types = ['bdb','gdbm']
18
+ end
19
+ types.each do |dbtype|
20
+ begin
21
+ require dbtype
22
+ @dbm = dbtype
23
+ @@log.info "#{@dbm} loaded"
24
+ break
25
+ rescue LoadError => e
26
+ end
27
+ end
28
+ raise "No DBM library found. Tried bdb,dbm,gdbm" unless @dbm
29
+ @db = Hash.new
30
+ @queues.keys.each {|q| _open_queue(q)}
31
+ end
32
+
33
+ def dbmopen(dbname)
34
+ if @dbm == 'bdb'
35
+ BDB::Hash.new(dbname, nil, "a")
36
+ elsif @dbm == 'dbm'
37
+ DBM.open(dbname)
38
+ elsif @dbm == 'gdbm'
39
+ GDBM.open(dbname)
40
+ end
41
+ end
42
+
43
+
44
+ def _open_queue(dest)
45
+ queue_name = dest.gsub('/', '_')
46
+ dbname = @directory + '/' + queue_name
47
+ @db[dest] = Hash.new
48
+ @db[dest][:dbh] = dbmopen(dbname)
49
+ @db[dest][:dbname] = dbname
50
+ end
51
+
52
+
53
+ def _close_queue(dest)
54
+ @db[dest][:dbh].close
55
+ dbname = @db[dest][:dbname]
56
+ File.delete(dbname) if File.exists?(dbname)
57
+ File.delete("#{dbname}.db") if File.exists?("#{dbname}.db")
58
+ File.delete("#{dbname}.pag") if File.exists?("#{dbname}.pag")
59
+ File.delete("#{dbname}.dir") if File.exists?("#{dbname}.dir")
60
+ end
61
+
62
+ def _writeframe(dest,frame,msgid)
63
+ @db[dest][:dbh][msgid] = Marshal::dump(frame)
64
+ end
65
+
66
+ def _readframe(dest,msgid)
67
+ frame_image = @db[dest][:dbh][msgid]
68
+ Marshal::load(frame_image)
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,56 @@
1
+ #
2
+ #
3
+ #
4
+ module StompServer
5
+ #
6
+ # Low level physical queue handler.
7
+ #
8
+ class FileQueue < Queue
9
+
10
+ # Remove queue directory if it exists.
11
+ def _close_queue(dest)
12
+ Dir.delete(@queues[dest][:queue_dir]) if File.directory?(@queues[dest][:queue_dir])
13
+ end
14
+
15
+ # Create queue directory if it does not alrady exist.
16
+ def _open_queue(dest)
17
+ # handle clashes between _ and /
18
+ queue_name = dest.gsub('_','__')
19
+ queue_name = dest.gsub('/','_')
20
+ queue_dir = @directory + '/' + queue_name
21
+ @queues[dest][:queue_dir] = queue_dir
22
+ Dir.mkdir(queue_dir) unless File.directory?(queue_dir)
23
+ end
24
+
25
+ # Write a messaage frame to the file system.
26
+ def _writeframe(dest,frame_todump,msgid)
27
+ filename = "#{@queues[dest][:queue_dir]}/#{msgid}"
28
+ frame = frame_todump.dup
29
+ frame_body = frame.body
30
+ frame.body = ''
31
+ frame_image = Marshal.dump(frame)
32
+ framelen = sprintf("%08x", frame_image.length)
33
+ bodylen = sprintf("%08x", frame_body.length)
34
+ File.open(filename,'wb') {|f| f.syswrite("#{framelen}#{bodylen}#{frame_image}#{frame_body}")}
35
+ return true
36
+ end
37
+
38
+ # Read a message frame from the file system.
39
+ def _readframe(dest,msgid)
40
+ filename = "#{@queues[dest][:queue_dir]}/#{msgid}"
41
+ file = nil
42
+ File.open(filename,'rb') {|f| file = f.read}
43
+ frame_len = file[0,8].hex
44
+ body_len = file[8,8].hex
45
+ frame = Marshal::load(file[16,frame_len])
46
+ frame.body = file[(frame_len + 16),body_len]
47
+ if File.delete(filename)
48
+ result = frame
49
+ else
50
+ result = false
51
+ end
52
+ return result
53
+ end
54
+ end # of class
55
+ end # of module
56
+
@@ -0,0 +1,64 @@
1
+
2
+ module StompServer
3
+ class MemoryQueue
4
+ attr_accessor :checkpoint_interval
5
+
6
+ def initialize
7
+
8
+ @@log = Logger.new(STDOUT)
9
+ @@log.level = StompServer::LogHelper.get_loglevel()
10
+
11
+ @frame_index =0
12
+ @stompid = StompServer::StompId.new
13
+ @stats = Hash.new
14
+ @messages = Hash.new { Array.new }
15
+ @@log.debug "MemoryQueue initialized"
16
+ end
17
+
18
+ def stop(session_id)
19
+ @@log.debug("#{session_id} memory queue shutdown")
20
+ end
21
+
22
+ def monitor
23
+ stats = Hash.new
24
+ @messages.keys.each do |dest|
25
+ stats[dest] = {'size' => @messages[dest].size, 'enqueued' => @stats[dest][:enqueued], 'dequeued' => @stats[dest][:dequeued]}
26
+ end
27
+ stats
28
+ end
29
+
30
+ def dequeue(dest, session_id)
31
+ if frame = @messages[dest].shift
32
+ @stats[dest][:dequeued] += 1
33
+ return frame
34
+ else
35
+ return false
36
+ end
37
+ end
38
+
39
+ def enqueue(dest,frame)
40
+ @frame_index += 1
41
+ if @stats[dest]
42
+ @stats[dest][:enqueued] += 1
43
+ else
44
+ @stats[dest] = Hash.new
45
+ @stats[dest][:enqueued] = 1
46
+ @stats[dest][:dequeued] = 0
47
+ end
48
+ assign_id(frame, dest)
49
+ requeue(dest, frame)
50
+ end
51
+
52
+ def requeue(dest,frame)
53
+ @messages[dest] += [frame]
54
+ end
55
+
56
+ def message_for?(dest, session_id)
57
+ !@messages[dest].empty?
58
+ end
59
+
60
+ def assign_id(frame, dest)
61
+ frame.headers['message-id'] = @stompid[@frame_index]
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,302 @@
1
+ #
2
+ # = QueueManager
3
+ #
4
+ # Used in conjunction with a storage class.
5
+ #
6
+ # The storage class MUST implement the following methods:
7
+ #
8
+ # * enqueue(queue name, frame)
9
+ #
10
+ # enqueue pushes a frame to the top of the queue in FIFO order. It's return
11
+ # value is ignored. enqueue must also set the message-id and add it to the
12
+ # frame header before inserting the frame into the queue.
13
+ #
14
+ # * dequeue(queue name)
15
+ #
16
+ # removes a frame from the bottom of the queue and returns it.
17
+ #
18
+ # * requeue(queue name,frame)
19
+ #
20
+ # does the same as enqueue, except it pushes the given frame to the
21
+ # bottom of the queue.
22
+ #
23
+ # The storage class MAY implement the following methods:
24
+ #
25
+ # * stop() method which should
26
+ #
27
+ # do any housekeeping that needs to be done before stompserver shuts down.
28
+ # stop() will be called when stompserver is shut down.
29
+ #
30
+ # * monitor() method which should
31
+ #
32
+ # return a hash of hashes containing the queue statistics.
33
+ # See the file queue for an example. Statistics are available to clients
34
+ # in /queue/monitor.
35
+ #
36
+ module StompServer
37
+ #
38
+ class QueueManager
39
+ Struct::new('QueueUser', :connection, :ack, :subid)
40
+ #
41
+ # Queue manager initialization.
42
+ #
43
+ def initialize(qstore)
44
+ @@log = Logger.new(STDOUT)
45
+ @@log.level = StompServer::LogHelper.get_loglevel()
46
+ @@log.debug("QM QueueManager initialize comletes")
47
+ #
48
+ @qstore = qstore
49
+ @queues = Hash.new { Array.new }
50
+ @pending = Hash.new
51
+ if $STOMP_SERVER
52
+ monitor = StompServer::QueueMonitor.new(@qstore,@queues)
53
+ monitor.start
54
+ @@log.debug "QM monitor started by QM initialization"
55
+ end
56
+ end
57
+ #
58
+ # Server stop / shutdown.
59
+ #
60
+ def stop(session_id)
61
+ @qstore.stop(session_id) if (@qstore.methods.include?('stop') || @qstore.methods.include?(:stop))
62
+ end
63
+ #
64
+ # Client subscribe for a destination.
65
+ #
66
+ # Called from the protocol handler (subscribe method).
67
+ #
68
+ def subscribe(dest, connection, use_ack=false, subid = nil)
69
+ @@log.debug "#{connection.session_id} QM subscribe to #{dest}, ack => #{use_ack}, connection: #{connection}, subid: #{subid}"
70
+ user = Struct::QueueUser.new(connection, use_ack, subid)
71
+ @queues[dest] += [user]
72
+ send_destination_backlog(dest,user) unless dest == '/queue/monitor'
73
+ end
74
+ #
75
+ # send_a_backlog
76
+ #
77
+ # Send at most one frame to a connection.
78
+ # Used when use_ack == true.
79
+ # Called from the ack method.
80
+ #
81
+ def send_a_backlog(connection)
82
+ @@log.debug "#{connection.session_id} QM send_a_backlog starts"
83
+ #
84
+ # lookup queues with data for this connection
85
+ #
86
+
87
+ # :stopdoc:
88
+
89
+ # 1.9 compatability
90
+ #
91
+ # The Hash#select method returns:
92
+ #
93
+ # * An Array (of Arrays) in Ruby 1.8
94
+ # * A Hash in Ruby 1.9
95
+ #
96
+ # Watch the code in this method. It is a bit ugly because of that
97
+ # difference.
98
+
99
+ # :startdoc:
100
+
101
+ possible_queues = @queues.select{ |destination, users|
102
+ @qstore.message_for?(destination, connection.session_id) &&
103
+ users.detect{|u| u.connection == connection}
104
+ }
105
+ if possible_queues.empty?
106
+ @@log.debug "#{connection.session_id} QM s_a_b nothing to send"
107
+ return
108
+ end
109
+ #
110
+ # Get a random one (avoid artificial priority between queues
111
+ # without coding a whole scheduler, which might be desirable later)
112
+ #
113
+ # Select a random destination from those possible
114
+
115
+ # :stopdoc:
116
+
117
+ # Told ya' this would get ugly. A quote from the Pickaxe. I am:
118
+ #
119
+ # 'abandoning the benefits of polymorphism, and bringing the gods of refactoring down around my ears'
120
+ #
121
+ # :-)
122
+
123
+ # :startdoc:
124
+
125
+ # The following log call results in an exception using 1.9.2p180. I cannot
126
+ # recreate this using IRB. It has something to do with 'Struct's I think.
127
+ # @@log.debug("#{connection.session_id} possible_queues: #{possible_queues.inspect}")
128
+
129
+
130
+ case possible_queues
131
+ when Hash
132
+ # possible_queues _is_ a Hash
133
+ dests_possible = possible_queues.keys # Get keys of a Hash of destination / queues
134
+ dest_index = rand(dests_possible.size) # Random index
135
+ dest = dests_possible[dest_index] # Select a destination / queue
136
+ # The selected destination has (possibly) multiple users.
137
+ # Select a random user from those possible
138
+ user_index = rand(possible_queues[dest].size) # Random index
139
+ user = possible_queues[dest][user_index] # Array entry from Hash table entry
140
+ #
141
+ when Array
142
+ # possible_queues _is_ an Array
143
+ dest_index = rand(possible_queues.size) # Random index
144
+ dest_data = possible_queues[dest_index] # Select a destination + user array
145
+ dest = dest_data[0] # Select a destination / queue
146
+ # The selected destination has (possibly) multiple users.
147
+ # Select a random user from those possible
148
+ user_index = rand(dest_data[1].size) # Random index
149
+ user = dest_data[1][user_index] # Array entry from Hash table entry
150
+ else
151
+ raise "#{connection.session_id} something is very not right : #{RUBY_VERSION}"
152
+ end
153
+
154
+ #
155
+ @@log.debug "#{connection.session_id} QM s_a_b chosen -> dest: #{dest}"
156
+ # Ditto for this log statement using 1.9.2p180.
157
+ # @@log.debug "#{connection.session_id} QM s_a_b chosen -> user: #{user}"
158
+ #
159
+ frame = @qstore.dequeue(dest, connection.session_id)
160
+ send_to_user(frame, user)
161
+ end
162
+ #
163
+ # send_destination_backlog
164
+ #
165
+ # Called from the subscribe method.
166
+ #
167
+ def send_destination_backlog(dest,user)
168
+ @@log.debug "#{user.connection.session_id} QM send_destination_backlog for #{dest}"
169
+ if user.ack
170
+ # Only send one message, then wait for client ACK.
171
+ frame = @qstore.dequeue(dest, user.connection.session_id)
172
+ if frame
173
+ send_to_user(frame, user)
174
+ @@log.debug("#{user.connection.session_id} QM s_d_b single frame sent")
175
+ end
176
+ else
177
+ # Send all available messages.
178
+ while frame = @qstore.dequeue(dest, user.connection.session_id)
179
+ send_to_user(frame, user)
180
+ end
181
+ end
182
+ end
183
+ #
184
+ # Client unsubscribe.
185
+ #
186
+ # Called from the protocol handler (unsubscribe method).
187
+ #
188
+ def unsubscribe(dest, connection)
189
+ @@log.debug "#{connection.session_id} QM unsubscribe from #{dest}, connection #{connection}"
190
+ @queues.each do |d, queue|
191
+ queue.delete_if { |qu| qu.connection == connection and d == dest}
192
+ end
193
+ @queues.delete(dest) if @queues[dest].empty?
194
+ end
195
+ #
196
+ # Client ack.
197
+ #
198
+ # Called from the protocol handler (ack method).
199
+ #
200
+ def ack(connection, frame)
201
+ @@log.debug "#{connection.session_id} QM ACK."
202
+ @@log.debug "#{connection.session_id} QM ACK for frame: #{frame.inspect}"
203
+ unless @pending[connection]
204
+ @@log.debug "#{connection.session_id} QM No message pending for connection!"
205
+ return
206
+ end
207
+ msgid = frame.headers['message-id']
208
+ p_msgid = @pending[connection].headers['message-id']
209
+ if p_msgid != msgid
210
+ @@log.debug "#{connection.session_id} QM ACK Invalid message-id (received /#{msgid}/ != /#{p_msgid}/)"
211
+ # We don't know what happened, we requeue
212
+ # (probably a client connecting to a restarted server)
213
+ frame = @pending[connection]
214
+ @qstore.requeue(frame.headers['destination'],frame)
215
+ end
216
+ @pending.delete connection
217
+ # We are free to work now, look if there's something for us
218
+ send_a_backlog(connection)
219
+ end
220
+ #
221
+ # Client disconnect.
222
+ #
223
+ # Called from the protocol handler (unbind method).
224
+ #
225
+ def disconnect(connection)
226
+ @@log.debug("#{connection.session_id} QM DISCONNECT.")
227
+ frame = @pending[connection]
228
+ @@log.debug("#{connection.session_id} QM DISCONNECT pending frame: #{frame.inspect}")
229
+ if frame
230
+ @qstore.requeue(frame.headers['destination'],frame)
231
+ @pending.delete connection
232
+ end
233
+ #
234
+ @queues.each do |dest, queue|
235
+ queue.delete_if { |qu| qu.connection == connection }
236
+ @queues.delete(dest) if queue.empty?
237
+ end
238
+ end
239
+ #
240
+ # send_to_user
241
+ #
242
+ def send_to_user(frame, user)
243
+ @@log.debug("#{user.connection.session_id} QM send_to_user")
244
+ connection = user.connection
245
+ frame.headers['subscription'] = user.subid if user.subid
246
+ if user.ack
247
+ # raise on internal logic error.
248
+ raise "#{user.connection.session_id} other connection's end already busy" if @pending[connection]
249
+ # A maximum of one frame can be pending ACK.
250
+ @pending[connection] = frame
251
+ end
252
+ connection.stomp_send_data(frame)
253
+ end
254
+ #
255
+ # sendmsg
256
+ #
257
+ # Called from the protocol handler (sendmsg method, process_frame method).
258
+ #
259
+ def sendmsg(frame)
260
+ #
261
+ @@log.debug("#{frame.headers['session']} QM client SEND Processing, #{frame}")
262
+ frame.command = "MESSAGE"
263
+ dest = frame.headers['destination']
264
+ # Lookup a user willing to handle this destination
265
+ available_users = @queues[dest].reject{|user| @pending[user.connection]}
266
+ if available_users.empty?
267
+ @qstore.enqueue(dest,frame)
268
+ return
269
+ end
270
+ #
271
+ # Look for a user with ack (we favor reliability)
272
+ #
273
+ reliable_user = available_users.find{|u| u.ack}
274
+ #
275
+ if reliable_user
276
+ # give it a message-id
277
+ @qstore.assign_id(frame, dest)
278
+ send_to_user(frame, reliable_user)
279
+ else
280
+ random_user = available_users[rand(available_users.length)]
281
+ # Note message-id header isn't set but we won't need it anyway
282
+ # <TODO> could break some clients: fix this
283
+ send_to_user(frame, random_user)
284
+ end
285
+ end
286
+ #
287
+ # dequeue: remove a message from a queue.
288
+ #
289
+ def dequeue(dest, session_id)
290
+ @qstore.dequeue(dest, session_id)
291
+ end
292
+ #
293
+ # enqueue: add a message to a queue.
294
+ #
295
+ def enqueue(frame)
296
+ frame.command = "MESSAGE"
297
+ dest = frame.headers['destination']
298
+ @qstore.enqueue(dest,frame)
299
+ end
300
+ end # of class
301
+ end # of module
302
+