stompserver 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,10 @@
1
+ == 0.9.0 / 16 Oct 2006
2
+
3
+ * Initialial Beta Release
4
+ * Seems to work
5
+ * Passes numerous test cases
6
+ * Journals using madeleine
7
+ * Needs documentaion
8
+ * Needs to command line processing
9
+ * Needs service/daemon options
10
+
data/Manifest.txt ADDED
@@ -0,0 +1,16 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ setup.rb
6
+ bin/stompserver
7
+ lib/frame_journal.rb
8
+ lib/queue_manager.rb
9
+ lib/stomp_frame.rb
10
+ lib/stomp_server.rb
11
+ lib/topic_manager.rb
12
+ test/test_frame_journal.rb
13
+ test/test_queue_manager.rb
14
+ test/test_stomp_frame.rb
15
+ test/test_stomp_server.rb
16
+ test/test_topic_manager.rb
data/README.txt ADDED
@@ -0,0 +1,58 @@
1
+ stompserver
2
+ by Patrick Hurley
3
+ http://stompserver.rubyforge.org/
4
+
5
+ == DESCRIPTION:
6
+
7
+ Don't want to install a JVM, but still want to use messaging? Me too,
8
+ so I threw together this little server. All the hard work was done
9
+ by Francis Cianfrocca (big thank you) in his event machine gem (which
10
+ is required by this server).
11
+
12
+ == FEATURES/PROBLEMS:
13
+
14
+ Handles basic message queue processing
15
+ Does not support any server to server messaging
16
+ (although you could write a client to do this)
17
+ Server Id is not being well initialized
18
+ Quite a bit of polish is still required to make into a daemon/service
19
+ and add command line handling.
20
+ And oh yeah, I need to write some docs (see the tests for now)
21
+
22
+ == SYNOPSYS:
23
+
24
+ Handles basic message queue processing
25
+
26
+ == REQUIREMENTS:
27
+
28
+ + EventMachine
29
+ + madeleine
30
+
31
+ == INSTALL:
32
+
33
+ + Grab the gem
34
+
35
+ == LICENSE:
36
+
37
+ (The MIT License)
38
+
39
+ Copyright (c) 2006 Patrick Hurley
40
+
41
+ Permission is hereby granted, free of charge, to any person obtaining
42
+ a copy of this software and associated documentation files (the
43
+ 'Software'), to deal in the Software without restriction, including
44
+ without limitation the rights to use, copy, modify, merge, publish,
45
+ distribute, sublicense, and/or sell copies of the Software, and to
46
+ permit persons to whom the Software is furnished to do so, subject to
47
+ the following conditions:
48
+
49
+ The above copyright notice and this permission notice shall be
50
+ included in all copies or substantial portions of the Software.
51
+
52
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
53
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
54
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
55
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
56
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
57
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
58
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ $LOAD_PATH << "./lib"
6
+ require 'stomp_server'
7
+
8
+ Hoe.new('stompserver', StompServer::VERSION) do |p|
9
+ p.rubyforge_name = 'stompserver'
10
+ p.summary = 'A very light messaging server'
11
+ p.description = p.paragraphs_of('README.txt', 2..4).join("\n\n")
12
+ p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
13
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
14
+ p.email = "phurley@gmail.com"
15
+ p.author = ["Patrick Hurley"]
16
+ p.extra_deps = [
17
+ ["eventmachine", ">= 5.0.0"],
18
+ ["madeleine", ">= 0.7.3"],
19
+ ["hoe", ">= 1.1.1"]
20
+ ]
21
+ end
22
+
23
+ # vim: syntax=Ruby
data/bin/stompserver ADDED
File without changes
@@ -0,0 +1,134 @@
1
+ # Simple Journal File(s) Manager
2
+ # You select the directory, and it will collect the messages
3
+ # The journal file format is:
4
+ #
5
+ # Status Byte: 0 - pending, 1 - processed
6
+ # Frame Size: 4 byte long (network endian - yes I limit my messages to 4G)
7
+ # Message
8
+ #
9
+ # Repeat
10
+ #
11
+ # When the size of a journal file exceeds its limit
12
+
13
+ require 'madeleine'
14
+ require 'madeleine/automatic'
15
+
16
+ class MadFrameJournal
17
+ include Madeleine::Automatic::Interceptor
18
+ attr_reader :frames
19
+ automatic_read_only :frames
20
+ attr_accessor :frame_index
21
+ automatic_read_only :frame_index
22
+ attr_accessor :system_id
23
+ automatic_read_only :system_id
24
+
25
+ def initialize
26
+ @frames = {}
27
+ @frame_index = 0
28
+ @system_id = nil
29
+ end
30
+
31
+ def add(msgid, frame)
32
+ @frames[msgid] = frame
33
+ end
34
+
35
+ def delete(msgid)
36
+ @frames.delete(msgid)
37
+ end
38
+
39
+ def clear
40
+ @frames.clear
41
+ end
42
+
43
+ automatic_read_only :lookup
44
+ def lookup(msgid)
45
+ @frames[msgid]
46
+ end
47
+ end
48
+
49
+ class FrameJournal
50
+ def initialize(directory='frame-journal', snap_freq = 60 * 5)
51
+ @directory = directory
52
+ @mad = AutomaticSnapshotMadeleine.new(directory) do
53
+ MadFrameJournal.new
54
+ end
55
+
56
+ # always snap on startup, in case we had an previous failure
57
+ @modified = true
58
+ Thread.new(@mad, snap_freq) do |mad, freq|
59
+ while true
60
+ sleep(freq)
61
+ mad.take_snapshot if @modified
62
+ @modified = false
63
+ end
64
+ end
65
+ end
66
+
67
+ def []=(msgid, frame)
68
+ @modified = true
69
+ @mad.system.add(msgid, frame)
70
+ end
71
+
72
+ def [](msgid)
73
+ @mad.system.lookup(msgid)
74
+ end
75
+
76
+ def delete(msgid)
77
+ @modified = true
78
+ @mad.system.delete(msgid)
79
+ end
80
+
81
+ def keys
82
+ @mad.system.frames.keys
83
+ end
84
+
85
+ def clear
86
+ @modified = true
87
+ @mad.system.clear
88
+ @mad.take_snapshot
89
+ end
90
+
91
+ def index
92
+ @mad.system.frame_index
93
+ end
94
+
95
+ def next_index
96
+ @modified = true
97
+ @mad.system.frame_index += 1
98
+ end
99
+
100
+ def system_id
101
+ unless name = @mad.system.system_id
102
+ # todo - grab default name from some place smarter...
103
+ @modified = true
104
+ @mad.system.system_id = 'cmastomp'
105
+ name = @mad.system.system_id
106
+ end
107
+ name
108
+ end
109
+ end
110
+
111
+ if __FILE__ == $0
112
+ fj = FrameJournal.new('fj', 3)
113
+ until ARGV.empty?
114
+ case cmd = ARGV.shift
115
+ when "keys"
116
+ puts fj.keys.inspect
117
+ when "dump"
118
+ fj.keys.each do |key|
119
+ puts "#{key}: #{fj[key]}"
120
+ end
121
+ when "show"
122
+ key = ARGV.shift
123
+ puts "#{key}: #{fj[key]}"
124
+ when "add"
125
+ key = ARGV.shift
126
+ val = ARGV.shift
127
+ fj[key] = val
128
+ when "sleep"
129
+ sleep ARGV.shift.to_i
130
+ when "delete"
131
+ fj.delete(ARGV.shift)
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,68 @@
1
+ # queue - persistent sent to a single subscriber
2
+ # queue_monitor - looks, but does not remove from queue
3
+
4
+ class QueueManager
5
+ Struct::new('QueueUser', :user, :ack)
6
+
7
+ def initialize(journal)
8
+ # read journal information
9
+ @journal = journal
10
+ @queues = Hash.new { Array.new }
11
+ @pending = Hash.new { Array.new }
12
+ @messages = Hash.new { Array.new }
13
+
14
+ # recover from previous run
15
+ msgids = @journal.keys.sort
16
+ msgids.each do |msgid|
17
+ sendmsg(@journal[msgid])
18
+ end
19
+ end
20
+
21
+ def subscribe(dest, user, use_ack=false)
22
+ user = Struct::QueueUser.new(user, use_ack)
23
+ @queues[dest] += [user]
24
+ @messages[dest].each do |frame|
25
+ send_to_user(frame, user)
26
+ end
27
+ end
28
+
29
+ def unsubscribe(topic, user)
30
+ @queues[topic].delete_if { |u| u.user == user }
31
+ end
32
+
33
+ def ack(user, frame)
34
+ pending_size = @pending[user]
35
+ msgid = frame.headers['message-id']
36
+ @pending[user].delete_if { |pf| pf.headers['message-id'] == msgid }
37
+ raise "Message (#{msgid}) not found" if pending_size == @pending[user]
38
+ @journal.delete(msgid)
39
+ end
40
+
41
+ def disconnect(user)
42
+ @pending[user].each do |frame|
43
+ sendmsg(frame)
44
+ end
45
+ end
46
+
47
+ def send_to_user(frame, user)
48
+ if user.ack
49
+ @pending[user.user] += [frame]
50
+ else
51
+ @journal.delete(frame.headers['message-id'])
52
+ end
53
+ user.user.send_data(frame.to_s)
54
+ end
55
+
56
+ def sendmsg(frame)
57
+ frame.command = "MESSAGE"
58
+ dest = frame.headers['destination']
59
+ @journal[frame.headers['message-id']] = frame
60
+
61
+ if user = @queues[dest].shift
62
+ send_to_user(frame, user)
63
+ @queues[dest].push(user)
64
+ else
65
+ @messages[dest] += [frame]
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,92 @@
1
+ class StompFrame
2
+ attr_accessor :command, :headers, :body
3
+ def initialize(command=nil, headers=nil, body=nil)
4
+ @command = command
5
+ @headers = headers || {}
6
+ @body = body || ''
7
+ end
8
+
9
+ def to_s
10
+ result = @command + "\n"
11
+ @headers['content-length'] = @body.size.to_s if @body.include?(0)
12
+ @headers.each_pair do |key, value|
13
+ result << "#{key}:#{value}\n"
14
+ end
15
+ result << "\n"
16
+ result << @body.to_s
17
+ result << "\000\n"
18
+ end
19
+
20
+ def dest
21
+ #@dest || (@dest = @headers['destination'])
22
+ @headers['destination']
23
+ end
24
+ end
25
+
26
+ class StompFrameRecognizer
27
+ attr_accessor :frames
28
+
29
+ def initialize
30
+ @buffer = ''
31
+ @body_length = nil
32
+ @frame = StompFrame.new
33
+ @frames = []
34
+ end
35
+
36
+ def parse_body(len)
37
+ raise RuntimeError.new("Invalid stompframe (missing null term)") unless @buffer[len] == 0
38
+ @frame.body = @buffer[0...len]
39
+ @buffer = @buffer[len+1..-1]
40
+ @frames << @frame
41
+ @frame = StompFrame.new
42
+ end
43
+
44
+ def parse_binary_body
45
+ if @buffer.length > @body_length
46
+ parse_body(@body_length)
47
+ end
48
+ end
49
+
50
+ def parse_text_body
51
+ if pos = @buffer.index(0)
52
+ parse_body(pos)
53
+ end
54
+ end
55
+
56
+ def parse_header
57
+ if match = @buffer.match(/^\s*(\S+)$((?:\s*.*?\s*:\s*.*?\s*$)*)$\r?\n$\r?\n/)
58
+ @frame.command, headers = match.captures
59
+ @buffer = match.post_match
60
+ headers.split(/\n/).each do |data|
61
+ if data =~ /^\s*(\S+)\s*:\s*(.*?)\s*$/
62
+ @frame.headers[$1] = $2
63
+ end
64
+ end
65
+
66
+ # body_length is nil, if there is no content-length, otherwise it is the length (as in integer)
67
+ @body_length = @frame.headers['content-length'] && @frame.headers['content-length'].to_i
68
+ end
69
+ end
70
+
71
+ def parse
72
+ count = @frames.size
73
+
74
+ parse_header unless @frame.command
75
+ if @frame.command
76
+ if @body_length
77
+ parse_binary_body
78
+ else
79
+ parse_text_body
80
+ end
81
+ end
82
+
83
+ # parse_XXX_body return the frame if they succeed and nil if they fail
84
+ # the result will fall through
85
+ parse if count != @frames.size
86
+ end
87
+
88
+ def<< (buf)
89
+ @buffer << buf
90
+ parse
91
+ end
92
+ end
@@ -0,0 +1,156 @@
1
+ require 'eventmachine'
2
+ require 'stomp_frame'
3
+ require 'topic_manager'
4
+ require 'queue_manager'
5
+ require 'frame_journal'
6
+
7
+ module StompServer
8
+ VERSION = '0.9.1'
9
+ VALID_COMMANDS = [:connect, :send, :subscribe, :unsubscribe, :begin, :commit, :abort, :ack, :disconnect]
10
+
11
+ def self.setup(j = FrameJournal.new, tm = TopicManager.new, qm = QueueManager.new(j))
12
+ @@journal = j
13
+ @@topic_manager = tm
14
+ @@queue_manager = qm
15
+ end
16
+
17
+ def post_init
18
+ @sfr = StompFrameRecognizer.new
19
+ @transactions = {}
20
+ @connected = false
21
+ end
22
+
23
+ def receive_data(data)
24
+ begin
25
+ puts "receive_data: #{data.inspect}" if $DEBUG
26
+ @sfr << data
27
+ process_frames
28
+ rescue Exception => e
29
+ puts "err: #{e} #{e.backtrace.join("\n")}" if $DEBUG
30
+ send_error(e.to_s)
31
+ close_connection_after_writing
32
+ end
33
+ end
34
+
35
+ def process_frames
36
+ frame = nil
37
+ process_frame(frame) while frame = @sfr.frames.shift
38
+ end
39
+
40
+ def process_frame(frame)
41
+ cmd = frame.command.downcase.to_sym
42
+ raise "Unhandled frame: #{cmd}" unless VALID_COMMANDS.include?(cmd)
43
+ raise "Not connected" if !@connected && cmd != :connect
44
+
45
+ # I really like this code, but my needs are a little trickier
46
+ #
47
+
48
+ if trans = frame.headers['transaction']
49
+ handle_transaction(frame, trans, cmd)
50
+ else
51
+ cmd = :sendmsg if cmd == :send
52
+ send(cmd, frame)
53
+ end
54
+
55
+ send_receipt(frame.headers['receipt']) if frame.headers['receipt']
56
+ end
57
+
58
+ def handle_transaction(frame, trans, cmd)
59
+ if [:begin, :commit, :abort].include?(cmd)
60
+ send(cmd, frame, trans)
61
+ else
62
+ raise "transaction does not exist" unless @transactions.has_key?(trans)
63
+ @transactions[trans] << frame
64
+ end
65
+ end
66
+
67
+ def connect(frame)
68
+ puts "Connecting" if $DEBUG
69
+ response = StompFrame.new("CONNECTED", {'session' => 'wow'})
70
+ send_data(response.to_s)
71
+ @connected = true
72
+ end
73
+
74
+ def sendmsg(frame)
75
+ # set message id
76
+ frame.headers['message-id'] = "msg-#{@@journal.system_id}-#{@@journal.next_index}"
77
+ if frame.dest.match(%r|^/queue|)
78
+ @@queue_manager.sendmsg(frame)
79
+ else
80
+ @@topic_manager.sendmsg(frame)
81
+ end
82
+ end
83
+
84
+ def subscribe(frame)
85
+ if frame.dest =~ %r|^/queue|
86
+ @@queue_manager.subscribe(frame.dest, self)
87
+ else
88
+ @@topic_manager.subscribe(frame.dest, self)
89
+ end
90
+ end
91
+
92
+ def unsubscribe(frame)
93
+ if frame.dest =~ %r|^/queue|
94
+ @@queue_manager.unsubscribe(self)
95
+ else
96
+ @@topic_manager.unsubscribe(self)
97
+ end
98
+ end
99
+
100
+ def begin(frame, trans=nil)
101
+ raise "Missing transaction" unless trans
102
+ raise "transaction exists" if @transactions.has_key?(trans)
103
+ @transactions[trans] = []
104
+ end
105
+
106
+ def commit(frame, trans=nil)
107
+ raise "Missing transaction" unless trans
108
+ raise "transaction does not exist" unless @transactions.has_key?(trans)
109
+
110
+ (@transactions[trans]).each do |frame|
111
+ frame.headers.delete('transaction')
112
+ process_frame(frame)
113
+ end
114
+ @transactions.delete(trans)
115
+ end
116
+
117
+ def abort(frame, trans=nil)
118
+ raise "Missing transaction" unless trans
119
+ raise "transaction does not exist" unless @transactions.has_key?(trans)
120
+ @transactions.delete(trans)
121
+ end
122
+
123
+ def ack(frame)
124
+ @@queue_manager.ack(self, frame)
125
+ end
126
+
127
+ def disconnect(frame)
128
+ puts "Polite disconnect" if $DEBUG
129
+ close_connection_after_writing
130
+ end
131
+
132
+ def send_message(msg)
133
+ msg.command = "MESSAGE"
134
+ send_data(msg.to_s)
135
+ end
136
+
137
+ def send_receipt(id)
138
+ send_frame("RECEIPT", { 'receipt-id' => id})
139
+ end
140
+
141
+ def send_error(msg)
142
+ send_frame("ERROR",{},msg)
143
+ end
144
+
145
+ def send_frame(command, headers={}, body='')
146
+ response = StompFrame.new(command, headers, body)
147
+ send_data(response.to_s)
148
+ end
149
+ end
150
+
151
+ if $0 == __FILE__
152
+ StompServer.setup
153
+ EventMachine::run do
154
+ EventMachine.start_server "0.0.0.0", 61613, StompServer
155
+ end
156
+ end
@@ -0,0 +1,24 @@
1
+ # topic - non persistent, sent to all interested parties
2
+
3
+ class TopicManager
4
+ def initialize
5
+ @topics = Hash.new { Array.new }
6
+ end
7
+
8
+ def subscribe(topic, user)
9
+ @topics[topic] += [user]
10
+ end
11
+
12
+ def unsubscribe(topic, user)
13
+ @topics[topic].delete(user)
14
+ end
15
+
16
+ def sendmsg(msg)
17
+ msg.command = "MESSAGE"
18
+ topic = msg.headers['destination']
19
+ payload = msg.to_s
20
+ @topics[topic].each do |user|
21
+ user.send_data(payload)
22
+ end
23
+ end
24
+ end