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,58 @@
1
+ #
2
+ #
3
+ #
4
+ module StompServer
5
+ #
6
+ # = Queue Monitor
7
+ #
8
+ class QueueMonitor
9
+ #
10
+ # Initialize the queue monitor.
11
+ #
12
+ def initialize(qstore,queues)
13
+ @qstore = qstore
14
+ @queues = queues
15
+ @stompid = StompServer::StompId.new
16
+ #
17
+ @@log = Logger.new(STDOUT)
18
+ @@log.level = StompServer::LogHelper.get_loglevel()
19
+ @@log.debug("QueueMonitor initialize comletes")
20
+ #
21
+ end
22
+ #
23
+ # Start monitor timer.
24
+ #
25
+ def start
26
+ count =0
27
+ EventMachine::add_periodic_timer 5, proc {count+=1; monitor(count) }
28
+ end
29
+ #
30
+ # Respond to calls from the timer. Do nothing if no clients are connected
31
+ # to the '/queue/monitor' destination.
32
+ #
33
+ def monitor(count)
34
+ return unless (@qstore.methods.include?(:monitor) | @qstore.methods.include?('monitor'))
35
+ users = @queues['/queue/monitor']
36
+ return if users.size == 0
37
+ stats = @qstore.monitor
38
+ return if stats.size == 0
39
+ body = ''
40
+ #
41
+ stats.each do |queue,qstats|
42
+ body << "Queue: #{queue}\n"
43
+ qstats.each {|stat,value| body << "#{stat}: #{value}\n"}
44
+ body << "\n"
45
+ end
46
+ #
47
+ headers = {
48
+ 'message-id' => @stompid[count],
49
+ 'destination' => '/queue/monitor',
50
+ 'content-length' => body.size.to_s
51
+ }
52
+ #
53
+ frame = StompServer::StompFrame.new('MESSAGE', headers, body)
54
+ users.each {|user| user.connection.stomp_send_data(frame)}
55
+ end
56
+ end # of class QueueMonitor
57
+ end # of module StompServer
58
+
@@ -0,0 +1,248 @@
1
+ #
2
+ #
3
+ #
4
+ module StompServer
5
+ #
6
+ # == Queue
7
+ #
8
+ class Queue
9
+ # the check point interval
10
+ attr_accessor :checkpoint_interval
11
+
12
+ # initiialize
13
+ def initialize(directory='.stompserver', delete_empty=true)
14
+
15
+ @@log = Logger.new(STDOUT)
16
+ @@log.level = StompServer::LogHelper.get_loglevel()
17
+ @@log.debug("Q #{self} initialization starts")
18
+
19
+ @stompid = StompServer::StompId.new
20
+ @delete_empty = delete_empty
21
+ @directory = directory
22
+ Dir.mkdir(@directory) unless File.directory?(@directory)
23
+ if File.exists?("#{@directory}/qinfo")
24
+ qinfo = Hash.new
25
+ File.open("#{@directory}/qinfo", "rb") { |f| qinfo = Marshal.load(f.read)}
26
+ @queues = qinfo[:queues]
27
+ @frames = qinfo[:frames]
28
+ else
29
+ @queues = Hash.new
30
+ @frames = Hash.new
31
+ end
32
+
33
+ @queues.keys.each do |dest|
34
+ @@log.debug "Q #{self} dest=#{dest} size=#{@queues[dest][:size]} enqueued=#{@queues[dest][:enqueued]} dequeued=#{@queues[dest][:dequeued]}"
35
+ end
36
+
37
+ @@log.debug("Q #{self} initialized in #{@directory}")
38
+
39
+ #
40
+ # Cleanup dead queues and save the state of the queues every so often.
41
+ # Alternatively we could save the queue state every X number
42
+ # of frames that are put in the queue.
43
+ # Should probably also read it after saving it to confirm integrity.
44
+ #
45
+ # Removed: this badly corrupts the queue when stopping with messages
46
+ #
47
+ # EventMachine::add_periodic_timer 1800, proc {@queues.keys.each
48
+ # {|dest| close_queue(dest)};save_queue_state }
49
+ #
50
+ end
51
+
52
+ # stop
53
+ def stop(session_id)
54
+ @@log.debug "#{session_id} Shutting down Queues, queue count: #{@queues.size}"
55
+ #
56
+ @queues.keys.each do |dest|
57
+ @@log.debug "#{session_id}: Queue #{dest}: size=#{@queues[dest][:size]} enqueued=#{@queues[dest][:enqueued]} dequeued=#{@queues[dest][:dequeued]}"
58
+ close_queue(dest, session_id)
59
+ end
60
+ save_queue_state(session_id)
61
+ end
62
+
63
+ # save_queue_state
64
+ def save_queue_state(session_id)
65
+ @@log.debug "#{session_id} save_queue_state"
66
+ now=Time.now
67
+ @next_save ||=now
68
+ if now >= @next_save
69
+ @@log.debug "#{session_id} saving state"
70
+ qinfo = {:queues => @queues, :frames => @frames}
71
+ # write then rename to make sure this is atomic
72
+ File.open("#{@directory}/qinfo.new", "wb") { |f| f.write Marshal.dump(qinfo)}
73
+ File.rename("#{@directory}/qinfo.new","#{@directory}/qinfo")
74
+ @next_save=now+checkpoint_interval
75
+ end
76
+ end
77
+
78
+ # monitor
79
+ def monitor
80
+ @@log.debug "#{self} monitor"
81
+ stats = Hash.new
82
+ @queues.keys.each do |dest|
83
+ stats[dest] = {
84
+ 'size' => @queues[dest][:size],
85
+ 'enqueued' => @queues[dest][:enqueued],
86
+ 'dequeued' => @queues[dest][:dequeued],
87
+ 'exceptions' => @queues[dest][:exceptions],
88
+ }
89
+ end
90
+ stats
91
+ end
92
+
93
+ # close_queue
94
+ def close_queue(dest, session_id)
95
+ @@log.debug "#{session_id} close_queue"
96
+ if @queues[dest][:size] == 0 and @queues[dest][:frames].size == 0 and @delete_empty
97
+ _close_queue(dest)
98
+ @queues.delete(dest)
99
+ @frames.delete(dest)
100
+ @@log.debug "#{session_id} Queue #{dest} removed."
101
+ end
102
+ end
103
+
104
+ # open_queue
105
+ def open_queue(dest, session_id)
106
+ @@log.debug "#{session_id} open_queue"
107
+ # New queue
108
+ @queues[dest] = Hash.new
109
+ # New frames for this queue
110
+ @frames[dest] = Hash.new
111
+ # Update queues
112
+ # :size, :frames, :msgid, :enqueued, :dequeued, :exceptions
113
+ @queues[dest][:size] = 0
114
+ @queues[dest][:frames] = Array.new
115
+ @queues[dest][:msgid] = 1
116
+ @queues[dest][:enqueued] = 0
117
+ @queues[dest][:dequeued] = 0
118
+ @queues[dest][:exceptions] = 0
119
+ _open_queue(dest)
120
+ @@log.debug "Created queue #{dest}"
121
+ end
122
+
123
+ # requeue
124
+ def requeue(dest,frame)
125
+ @@log.debug "#{frame.headers['session']} requeue, for #{dest}, frame: #{frame.inspect}"
126
+ open_queue(dest, frame.headers['session']) unless @queues.has_key?(dest)
127
+ msgid = frame.headers['message-id']
128
+ #
129
+ # Note: frame.headers['max-exceptions'] is currently _never_ set any where!
130
+ #
131
+ if frame.headers['max-exceptions'] and @frames[dest][msgid][:exceptions] >= frame.headers['max-exceptions'].to_i
132
+ enqueue("/queue/deadletter",frame)
133
+ return
134
+ end
135
+ #
136
+ writeframe(dest,frame,msgid)
137
+
138
+ # update queues (queues[dest])
139
+ # :size, :frames, :msgid, :enqueued, :dequeued, :exceptions
140
+ @queues[dest][:size] += 1
141
+ @queues[dest][:frames].unshift(msgid)
142
+ # no :msgid here
143
+ # no :enqueued here
144
+ # no :dequeued here
145
+ @queues[dest][:exceptions] += 1
146
+
147
+ # update frames
148
+ #
149
+ # Is this _always_ the case in this method ?????
150
+ unless @frames[dest][msgid]
151
+ new_frames_entry(dest, frame, msgid)
152
+ end
153
+ #
154
+ @frames[dest][msgid][:exceptions] += 1
155
+ @frames[dest][msgid][:requeued] += 1
156
+ save_queue_state(frame.headers['session'])
157
+ return true
158
+ end
159
+
160
+ # enqueue
161
+ def enqueue(dest,frame)
162
+ @@log.debug "#{frame.headers['session']} enqueue"
163
+ open_queue(dest, frame.headers['session']) unless @queues.has_key?(dest)
164
+ msgid = assign_id(frame, dest)
165
+ @@log.debug("#{frame.headers['session']} Enqueue for message: #{msgid} Client: #{frame.headers['client-id'] if frame.headers['client-id']}")
166
+ writeframe(dest,frame,msgid)
167
+
168
+ # update queues (queues[dest])
169
+ # :size, :frames, :msgid, :enqueued, :dequeued, :exceptions
170
+ @queues[dest][:size] += 1
171
+ @queues[dest][:frames].push(msgid)
172
+ @queues[dest][:msgid] += 1
173
+ @queues[dest][:enqueued] += 1
174
+ # no :dequeue here
175
+ # no :exceptions here
176
+
177
+ # Update frames
178
+ # Initialize frames entry for this: dest, frame, and msgid
179
+ new_frames_entry(dest, frame, msgid)
180
+ save_queue_state(frame.headers['session'])
181
+ return true
182
+ end
183
+
184
+ # dequeue
185
+ def dequeue(dest, session_id)
186
+ @@log.debug "#{session_id} dequeue, dest: #{dest}"
187
+ return false unless message_for?(dest, session_id)
188
+ # update queues ... dest .... :frames here
189
+ msgid = @queues[dest][:frames].shift
190
+ frame = readframe(dest,msgid,session_id)
191
+ @@log.debug("#{frame.headers['session']} Dequeue for message: #{msgid} Client: #{frame.headers['client-id'] if frame.headers['client-id']}")
192
+
193
+ # update queues (queues[dest])
194
+ # :size, :frames, :msgid, :enqueued, :dequeued, :exceptions
195
+ @queues[dest][:size] -= 1
196
+ # :frames - see above
197
+ @queues[dest][:msgid] -= 1
198
+ # :enqueued - no change
199
+ @queues[dest][:dequeued] += 1
200
+ # :exceptions - no change
201
+
202
+ @queues[dest].delete(msgid)
203
+
204
+ close_queue(dest, frame.headers['session'])
205
+ save_queue_state(frame.headers['session'])
206
+ return frame
207
+ end
208
+
209
+ # messsage_for?
210
+ def message_for?(dest, session_id)
211
+ retval = (@queues.has_key?(dest) and (!@queues[dest][:frames].empty?))
212
+ @@log.debug "#{session_id} message_for?, dest: #{dest}, #{retval}"
213
+ return retval
214
+ end
215
+
216
+ # writeframe
217
+ def writeframe(dest,frame,msgid)
218
+ @@log.debug "#{frame.headers['session']} writeframe, dest: #{dest}, frame: #{frame}, msgid: #{msgid}"
219
+ _writeframe(dest,frame,msgid)
220
+ end
221
+
222
+ # readframe
223
+ def readframe(dest,msgid, session_id)
224
+ @@log.debug "#{session_id} readframe, dest: #{dest}, msgid: #{msgid}"
225
+ _readframe(dest,msgid)
226
+ end
227
+
228
+ # assign_id
229
+ def assign_id(frame, dest)
230
+ @@log.debug "#{frame.headers['session']} assign_id, frame: #{frame}, dest: #{dest}"
231
+ msg_id = @queues[dest].nil? ? 1 : @queues[dest][:msgid]
232
+ frame.headers['message-id'] = @stompid[msg_id]
233
+ end
234
+
235
+ private
236
+ # new_frames_entry
237
+ def new_frames_entry(dest, frame, msgid)
238
+ @frames[dest][msgid] = Hash.new
239
+ @frames[dest][msgid][:exceptions] = 0
240
+ @frames[dest][msgid][:requeued] = 0
241
+ @frames[dest][msgid][:client_id] = frame.headers['client-id'] if frame.headers['client-id']
242
+ @frames[dest][msgid][:expires] = frame.headers['expires'] if frame.headers['expires']
243
+ end
244
+ #
245
+ end # class Queue
246
+ #
247
+ end # module StompServer
248
+
@@ -0,0 +1,118 @@
1
+ ## Queue implementation using ActiveRecord
2
+ ##
3
+ ## all messages are stored in a single table
4
+ ## they are indexed by 'stomp_id' which is the stomp 'message-id' header
5
+ ## which must be unique accross all queues
6
+ ##
7
+ require 'stomp_server_ng/queue/ar_message'
8
+ require 'yaml'
9
+
10
+ module StompServer
11
+ class ActiveRecordQueue
12
+ attr_accessor :checkpoint_interval
13
+
14
+ def initialize(configdir, storagedir, db_ymlfile)
15
+ # Default configuration, use SQLite for simplicity
16
+ db_params = {
17
+ 'adapter' => 'sqlite3',
18
+ 'database' => "#{configdir}/stompserver_development"
19
+ }
20
+ @@log = Logger::new(STDOUT)
21
+ @@log.level = StompServer::LogHelper.get_loglevel()
22
+ # Load DB configuration
23
+ @@log.debug "trying to read from #{db_ymlfile}"
24
+ if File.exists? db_ymlfile
25
+ @@log.debug("File #{db_ymlfile} exists.")
26
+ db_params.merge! YAML::load(File.open(db_ymlfile))
27
+ else
28
+ @@log.warn("File #{db_ymlfile} not found, using sqlite3 default.")
29
+ end
30
+ @@log.debug("using DB params: #{db_params.inspect}")
31
+ # Setup activerecord
32
+ ActiveRecord::Base.establish_connection(db_params)
33
+ @@log.debug("connection complete")
34
+
35
+ # AR Logger
36
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
37
+ ActiveRecord::Base.logger.level = StompServer::LogHelper.get_loglevel()
38
+
39
+ # we need the connection, it can't be done earlier
40
+ ArMessage.reset_column_information
41
+ reload_queues
42
+ @stompid = StompServer::StompId.new
43
+ end
44
+
45
+ # Add a frame to the queue
46
+ def enqueue(queue_name, frame)
47
+ unless @frames[queue_name]
48
+ @frames[queue_name] = {
49
+ :last_index => 0,
50
+ :frames => [],
51
+ }
52
+ end
53
+ affect_msgid_and_store(frame, queue_name)
54
+ @frames[queue_name][:frames] << frame
55
+ end
56
+
57
+ # Get and remove a frame from the queue
58
+ def dequeue(queue_name, session_id)
59
+ return nil unless @frames[queue_name] && !@frames[queue_name][:frames].empty?
60
+ frame = @frames[queue_name][:frames].shift
61
+ remove_from_store(frame.headers['message-id'])
62
+ return frame
63
+ end
64
+
65
+ # Requeue the frame previously pending
66
+ def requeue(queue_name, frame)
67
+ @frames[queue_name][:frames] << frame
68
+ ArMessage.create!(:stomp_id => frame.headers['message-id'],
69
+ :frame => frame)
70
+ end
71
+
72
+ # remove a frame from the store
73
+ def remove_from_store(message_id)
74
+ ArMessage.find_by_stomp_id(message_id).destroy
75
+ end
76
+
77
+ # store a frame (assigning it a message-id)
78
+ def affect_msgid_and_store(frame, queue_name)
79
+ msgid = assign_id(frame, queue_name)
80
+ ArMessage.create!(:stomp_id => msgid, :frame => frame)
81
+ end
82
+
83
+ def message_for?(queue_name, session_id)
84
+ @frames[queue_name] && !@frames[queue_name][:frames].empty?
85
+ end
86
+
87
+ def assign_id(frame, queue_name)
88
+
89
+ unless @frames[queue_name]
90
+ @frames[queue_name] = {
91
+ :last_index => 0,
92
+ :frames => [],
93
+ }
94
+ end
95
+
96
+ msgid = @stompid[@frames[queue_name][:last_index] += 1]
97
+ frame.headers['message-id'] = msgid
98
+ end
99
+
100
+ private
101
+ def reload_queues
102
+ @frames = Hash.new
103
+ ArMessage.find(:all).each { |message|
104
+ frame = message.frame
105
+ destination = frame.dest
106
+ msgid = message.stomp_id
107
+ @frames[destination] ||= Hash.new
108
+ @frames[destination][:frames] ||= Array.new
109
+ @frames[destination][:frames] << frame
110
+ }
111
+ # compute base index for each destination
112
+ @frames.each_pair { |destination,hash|
113
+ hash[:last_index] = hash[:frames].map{|f|
114
+ f.headers['message-id'].match(/(\d+)\Z/)[0].to_i}.max
115
+ }
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,21 @@
1
+ gem 'activerecord'
2
+ require 'active_record'
3
+ require 'stomp_server_ng/queue/ar_reconnect'
4
+ #
5
+ #
6
+ #
7
+ class Logger
8
+ private
9
+ # Rails overrides this method so that it can customize the format
10
+ # of it's logs. A consequence it that the date, time, etc. disappear
11
+ # from log output. This little hack overrides the override :-).
12
+ def format_message(*args)
13
+ old_format_message(*args)
14
+ end
15
+ end
16
+ #
17
+ #
18
+ #
19
+ class ArMessage < ActiveRecord::Base
20
+ serialize :frame
21
+ end
@@ -0,0 +1,18 @@
1
+ require 'active_record/connection_adapters/mysql_adapter'
2
+ module ActiveRecord::ConnectionAdapters
3
+ class MysqlAdapter
4
+ alias_method :execute_without_retry, :execute
5
+ def execute(*args)
6
+ execute_without_retry(*args)
7
+ rescue ActiveRecord::StatementInvalid
8
+ if $!.message =~ /server has gone away/i
9
+ warn "Server timed out, retrying"
10
+ reconnect!
11
+ retry
12
+ end
13
+
14
+ raise
15
+ end
16
+ end
17
+ end
18
+