stompserver_ng 1.0.6

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