stomp 1.0.6 → 1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,207 @@
1
+ module Stomp
2
+
3
+ # Typical Stomp client class. Uses a listener thread to receive frames
4
+ # from the server, any thread can send.
5
+ #
6
+ # Receives all happen in one thread, so consider not doing much processing
7
+ # in that thread if you have much message volume.
8
+ class Client
9
+
10
+ attr_reader :login, :passcode, :host, :port, :reliable, :running
11
+
12
+ # A new Client object can be initialized using two forms:
13
+ #
14
+ # Standard positional parameters:
15
+ # login (String, default : '')
16
+ # passcode (String, default : '')
17
+ # host (String, default : 'localhost')
18
+ # port (Integer, default : 61613)
19
+ # reliable (Boolean, default : false)
20
+ #
21
+ # e.g. c = Client.new('login', 'passcode', 'localhost', 61613, true)
22
+ #
23
+ # Stomp URL :
24
+ # A Stomp URL must begin with 'stomp://' and can be in one of the following forms:
25
+ #
26
+ # stomp://host:port
27
+ # stomp://host.domain.tld:port
28
+ # stomp://login:passcode@host:port
29
+ # stomp://login:passcode@host.domain.tld:port
30
+ #
31
+ def initialize(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false)
32
+
33
+ # Parse stomp:// URL's or set positional params
34
+ case login
35
+ when /stomp:\/\/([\w\.]+):(\d+)/ # e.g. stomp://host:port
36
+ # grabs the matching positions out of the regex which are stored as
37
+ # $1 (host), $2 (port), etc
38
+ @login = ''
39
+ @passcode = ''
40
+ @host = $1
41
+ @port = $2.to_i
42
+ @reliable = false
43
+ when /stomp:\/\/([\w\.]+):(\w+)@([\w\.]+):(\d+)/ # e.g. stomp://login:passcode@host:port
44
+ @login = $1
45
+ @passcode = $2
46
+ @host = $3
47
+ @port = $4.to_i
48
+ @reliable = false
49
+ else
50
+ @login = login
51
+ @passcode = passcode
52
+ @host = host
53
+ @port = port.to_i
54
+ @reliable = reliable
55
+ end
56
+
57
+ raise ArgumentError if @host.nil? || @host.empty?
58
+ raise ArgumentError if @port.nil? || @port == '' || @port < 1 || @port > 65535
59
+ raise ArgumentError unless @reliable.is_a?(TrueClass) || @reliable.is_a?(FalseClass)
60
+
61
+ @id_mutex = Mutex.new
62
+ @ids = 1
63
+ @connection = Connection.new(@login, @passcode, @host, @port, @reliable)
64
+ @listeners = {}
65
+ @receipt_listeners = {}
66
+ @running = true
67
+ @replay_messages_by_txn = {}
68
+
69
+ @listener_thread = Thread.start do
70
+ while @running
71
+ message = @connection.receive
72
+ case
73
+ when message.nil?
74
+ break
75
+ when message.command == 'MESSAGE'
76
+ if listener = @listeners[message.headers['destination']]
77
+ listener.call(message)
78
+ end
79
+ when message.command == 'RECEIPT'
80
+ if listener = @receipt_listeners[message.headers['receipt-id']]
81
+ listener.call(message)
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ end
88
+
89
+ # Syntactic sugar for 'Client.new' See 'initialize' for usage.
90
+ def self.open(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false)
91
+ Client.new(login, passcode, host, port, reliable)
92
+ end
93
+
94
+ # Join the listener thread for this client,
95
+ # generally used to wait for a quit signal
96
+ def join
97
+ @listener_thread.join
98
+ end
99
+
100
+ # Begin a transaction by name
101
+ def begin(name, headers = {})
102
+ @connection.begin(name, headers)
103
+ end
104
+
105
+ # Abort a transaction by name
106
+ def abort(name, headers = {})
107
+ @connection.abort(name, headers)
108
+
109
+ # lets replay any ack'd messages in this transaction
110
+ replay_list = @replay_messages_by_txn[name]
111
+ if replay_list
112
+ replay_list.each do |message|
113
+ if listener = @listeners[message.headers['destination']]
114
+ listener.call(message)
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ # Commit a transaction by name
121
+ def commit(name, headers = {})
122
+ txn_id = headers[:transaction]
123
+ @replay_messages_by_txn.delete(txn_id)
124
+ @connection.commit(name, headers)
125
+ end
126
+
127
+ # Subscribe to a destination, must be passed a block
128
+ # which will be used as a callback listener
129
+ #
130
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
131
+ def subscribe(destination, headers = {})
132
+ raise "No listener given" unless block_given?
133
+ @listeners[destination] = lambda {|msg| yield msg}
134
+ @connection.subscribe(destination, headers)
135
+ end
136
+
137
+ # Unsubecribe from a channel
138
+ def unsubscribe(name, headers = {})
139
+ @connection.unsubscribe(name, headers)
140
+ @listeners[name] = nil
141
+ end
142
+
143
+ # Acknowledge a message, used when a subscription has specified
144
+ # client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
145
+ #
146
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
147
+ def acknowledge(message, headers = {})
148
+ txn_id = headers[:transaction]
149
+ if txn_id
150
+ # lets keep around messages ack'd in this transaction in case we rollback
151
+ replay_list = @replay_messages_by_txn[txn_id]
152
+ if replay_list.nil?
153
+ replay_list = []
154
+ @replay_messages_by_txn[txn_id] = replay_list
155
+ end
156
+ replay_list << message
157
+ end
158
+ if block_given?
159
+ headers['receipt'] = register_receipt_listener lambda {|r| yield r}
160
+ end
161
+ @connection.ack message.headers['message-id'], headers
162
+ end
163
+
164
+ # Send message to destination
165
+ #
166
+ # If a block is given a receipt will be requested and passed to the
167
+ # block on receipt
168
+ #
169
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
170
+ def send(destination, message, headers = {})
171
+ if block_given?
172
+ headers['receipt'] = register_receipt_listener lambda {|r| yield r}
173
+ end
174
+ @connection.send(destination, message, headers)
175
+ end
176
+
177
+ # Is this client open?
178
+ def open?
179
+ @connection.open?
180
+ end
181
+
182
+ # Is this client closed?
183
+ def closed?
184
+ @connection.closed?
185
+ end
186
+
187
+ # Close out resources in use by this client
188
+ def close
189
+ @connection.disconnect
190
+ @running = false
191
+ end
192
+
193
+ private
194
+
195
+ def register_receipt_listener(listener)
196
+ id = -1
197
+ @id_mutex.synchronize do
198
+ id = @ids.to_s
199
+ @ids = @ids.succ
200
+ end
201
+ @receipt_listeners[id] = listener
202
+ id
203
+ end
204
+
205
+ end
206
+ end
207
+
@@ -0,0 +1,256 @@
1
+ module Stomp
2
+
3
+ # Low level connection which maps commands and supports
4
+ # synchronous receives
5
+ class Connection
6
+
7
+
8
+ # A new Connection object accepts the following parameters:
9
+ #
10
+ # login (String, default : '')
11
+ # passcode (String, default : '')
12
+ # host (String, default : 'localhost')
13
+ # port (Integer, default : 61613)
14
+ # reliable (Boolean, default : false)
15
+ # reconnect_delay (Integer, default : 5)
16
+ #
17
+ # e.g. c = Client.new("username", "password", "localhost", 61613, true)
18
+ #
19
+ # TODO
20
+ # Stomp URL :
21
+ # A Stomp URL must begin with 'stomp://' and can be in one of the following forms:
22
+ #
23
+ # stomp://host:port
24
+ # stomp://host.domain.tld:port
25
+ # stomp://user:pass@host:port
26
+ # stomp://user:pass@host.domain.tld:port
27
+ #
28
+ def initialize(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false, reconnect_delay = 5, connect_headers = {})
29
+ @host = host
30
+ @port = port
31
+ @login = login
32
+ @passcode = passcode
33
+ @transmit_semaphore = Mutex.new
34
+ @read_semaphore = Mutex.new
35
+ @socket_semaphore = Mutex.new
36
+ @reliable = reliable
37
+ @reconnect_delay = reconnect_delay
38
+ @connect_headers = connect_headers
39
+ @closed = false
40
+ @subscriptions = {}
41
+ @failure = nil
42
+ socket
43
+ end
44
+
45
+ # Syntactic sugar for 'Connection.new' See 'initialize' for usage.
46
+ def Connection.open(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false, reconnect_delay = 5, connect_headers = {})
47
+ Connection.new(login, passcode, host, port, reliable, reconnect_delay, connect_headers)
48
+ end
49
+
50
+ def socket
51
+ # Need to look into why the following synchronize does not work.
52
+ #@read_semaphore.synchronize do
53
+ s = @socket;
54
+ while s.nil? || !@failure.nil?
55
+ @failure = nil
56
+ begin
57
+ s = TCPSocket.open @host, @port
58
+ headers = @connect_headers.clone
59
+ headers[:login] = @login
60
+ headers[:passcode] = @passcode
61
+ _transmit(s, "CONNECT", headers)
62
+ @connect = _receive(s)
63
+ # replay any subscriptions.
64
+ @subscriptions.each { |k,v| _transmit(s, "SUBSCRIBE", v) }
65
+ rescue
66
+ @failure = $!;
67
+ s=nil;
68
+ raise unless @reliable
69
+ $stderr.print "connect failed: " + $! +" will retry in #{@reconnect_delay}\n";
70
+ sleep(@reconnect_delay);
71
+ end
72
+ end
73
+ @socket = s
74
+ return s;
75
+ #end
76
+ end
77
+
78
+ # Is this connection open?
79
+ def open?
80
+ !@closed
81
+ end
82
+
83
+ # Is this connection closed?
84
+ def closed?
85
+ @closed
86
+ end
87
+
88
+ # Begin a transaction, requires a name for the transaction
89
+ def begin(name, headers = {})
90
+ headers[:transaction] = name
91
+ transmit("BEGIN", headers)
92
+ end
93
+
94
+ # Acknowledge a message, used when a subscription has specified
95
+ # client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
96
+ #
97
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
98
+ def ack(message_id, headers = {})
99
+ headers['message-id'] = message_id
100
+ transmit("ACK", headers)
101
+ end
102
+
103
+ # Commit a transaction by name
104
+ def commit(name, headers = {})
105
+ headers[:transaction] = name
106
+ transmit("COMMIT", headers)
107
+ end
108
+
109
+ # Abort a transaction by name
110
+ def abort(name, headers = {})
111
+ headers[:transaction] = name
112
+ transmit("ABORT", headers)
113
+ end
114
+
115
+ # Subscribe to a destination, must specify a name
116
+ def subscribe(name, headers = {}, subId = nil)
117
+ headers[:destination] = name
118
+ transmit("SUBSCRIBE", headers)
119
+
120
+ # Store the sub so that we can replay if we reconnect.
121
+ if @reliable
122
+ subId = name if subId.nil?
123
+ @subscriptions[subId] = headers
124
+ end
125
+ end
126
+
127
+ # Unsubscribe from a destination, must specify a name
128
+ def unsubscribe(name, headers = {}, subId = nil)
129
+ headers[:destination] = name
130
+ transmit("UNSUBSCRIBE", headers)
131
+ if @reliable
132
+ subId = name if subId.nil?
133
+ @subscriptions.delete(subId)
134
+ end
135
+ end
136
+
137
+ # Send message to destination
138
+ #
139
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
140
+ def send(destination, message, headers = {})
141
+ headers[:destination] = destination
142
+ transmit("SEND", headers, message)
143
+ end
144
+
145
+ # Close this connection
146
+ def disconnect(headers = {})
147
+ transmit("DISCONNECT", headers)
148
+ @closed = true
149
+ end
150
+
151
+ # Return a pending message if one is available, otherwise
152
+ # return nil
153
+ def poll
154
+ @read_semaphore.synchronize do
155
+ return nil if @socket.nil? || !@socket.ready?
156
+ return receive
157
+ end
158
+ end
159
+
160
+ # Receive a frame, block until the frame is received
161
+ def __old_receive
162
+ # The recive my fail so we may need to retry.
163
+ while TRUE
164
+ begin
165
+ s = socket
166
+ return _receive(s)
167
+ rescue
168
+ @failure = $!;
169
+ raise unless @reliable
170
+ $stderr.print "receive failed: " + $!;
171
+ end
172
+ end
173
+ end
174
+
175
+ def receive
176
+ super_result = __old_receive()
177
+ if super_result.nil? && @reliable
178
+ $stderr.print "connection.receive returning EOF as nil - resetting connection.\n"
179
+ @socket = nil
180
+ super_result = __old_receive()
181
+ end
182
+ return super_result
183
+ end
184
+
185
+ private
186
+
187
+ def _receive( s )
188
+ line = ' '
189
+ @read_semaphore.synchronize do
190
+ line = s.gets while line =~ /^\s*$/
191
+ return nil if line.nil?
192
+
193
+ message = Message.new do |m|
194
+ m.command = line.chomp
195
+ m.headers = {}
196
+ until (line = s.gets.chomp) == ''
197
+ k = (line.strip[0, line.strip.index(':')]).strip
198
+ v = (line.strip[line.strip.index(':') + 1, line.strip.length]).strip
199
+ m.headers[k] = v
200
+ end
201
+
202
+ if (m.headers['content-length'])
203
+ m.body = s.read m.headers['content-length'].to_i
204
+ c = RUBY_VERSION > '1.9' ? s.getc.ord : s.getc
205
+ raise "Invalid content length received" unless c == 0
206
+ else
207
+ m.body = ''
208
+ if RUBY_VERSION > '1.9'
209
+ until (c = s.getc.ord) == 0
210
+ m.body << c.chr
211
+ end
212
+ else
213
+ until (c = s.getc) == 0
214
+ m.body << c.chr
215
+ end
216
+ end
217
+ end
218
+ #c = s.getc
219
+ #raise "Invalid frame termination received" unless c == 10
220
+ end # message
221
+ return message
222
+
223
+ end
224
+ end
225
+
226
+ def transmit(command, headers = {}, body = '')
227
+ # The transmit may fail so we may need to retry.
228
+ while TRUE
229
+ begin
230
+ s = socket
231
+ _transmit(s, command, headers, body)
232
+ return
233
+ rescue
234
+ @failure = $!;
235
+ raise unless @reliable
236
+ $stderr.print "transmit failed: " + $!+"\n";
237
+ end
238
+ end
239
+ end
240
+
241
+ def _transmit(s, command, headers = {}, body = '')
242
+ @transmit_semaphore.synchronize do
243
+ s.puts command
244
+ headers.each {|k,v| s.puts "#{k}:#{v}" }
245
+ s.puts "content-length: #{body.length}"
246
+ s.puts "content-type: text/plain; charset=UTF-8"
247
+ s.puts
248
+ s.write body
249
+ s.write "\0"
250
+ end
251
+ end
252
+
253
+ end
254
+
255
+ end
256
+