thechrisoshow-smqueue 0.1.0
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.
- data/History.txt +4 -0
- data/Manifest.txt +9 -0
- data/README.txt +72 -0
- data/Rakefile +26 -0
- data/lib/rstomp.rb +582 -0
- data/lib/smqueue.rb +227 -0
- data/lib/smqueue/adapters/spread.rb +101 -0
- data/lib/smqueue/adapters/stdio.rb +59 -0
- data/lib/smqueue/adapters/stomp.rb +291 -0
- data/smqueue.gemspec +24 -0
- data/test/test_rstomp_connection.rb +56 -0
- metadata +70 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
smqueue
|
2
|
+
by Sean O'Halpin
|
3
|
+
http://github.com/seanohalpin/smqueue
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Implements a simple protocol for using message queues, with adapters
|
8
|
+
for ActiveMQ, Spread and stdio (for testing).
|
9
|
+
|
10
|
+
This is a bare-bones release to share with my colleagues - apologies
|
11
|
+
for the lack of documentation and tests.
|
12
|
+
|
13
|
+
== FEATURES:
|
14
|
+
|
15
|
+
* simple to use
|
16
|
+
* designed primarily for pipeline processing
|
17
|
+
* compatible with Rails
|
18
|
+
* comes with a modified stomp.rb library (rstomp.rb)
|
19
|
+
|
20
|
+
== BUGS
|
21
|
+
|
22
|
+
* you need the ruby spread client installed - should remove this
|
23
|
+
|
24
|
+
== SYNOPSIS:
|
25
|
+
|
26
|
+
require 'smqueue'
|
27
|
+
config = YAML::load(config_file)
|
28
|
+
input_queue = SMQueue(config[:input])
|
29
|
+
output_queue = SMQueue(config[:output])
|
30
|
+
queue.get do |msg|
|
31
|
+
# do something with msg
|
32
|
+
output_queue.put new_msg
|
33
|
+
end
|
34
|
+
|
35
|
+
== REQUIREMENTS:
|
36
|
+
|
37
|
+
* depends on doodle
|
38
|
+
* you need access to an ActiveMQ message broker or Spread publisher
|
39
|
+
* development uses bones gem
|
40
|
+
|
41
|
+
== INSTALL:
|
42
|
+
|
43
|
+
* sudo gem install doodle smqueue
|
44
|
+
|
45
|
+
For development:
|
46
|
+
|
47
|
+
* sudo gem install bones
|
48
|
+
|
49
|
+
== LICENSE:
|
50
|
+
|
51
|
+
(The MIT License)
|
52
|
+
|
53
|
+
Copyright (c) 2008 Sean O'Halpin
|
54
|
+
|
55
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
56
|
+
a copy of this software and associated documentation files (the
|
57
|
+
'Software'), to deal in the Software without restriction, including
|
58
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
59
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
60
|
+
permit persons to whom the Software is furnished to do so, subject to
|
61
|
+
the following conditions:
|
62
|
+
|
63
|
+
The above copyright notice and this permission notice shall be
|
64
|
+
included in all copies or substantial portions of the Software.
|
65
|
+
|
66
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
67
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
68
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
69
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
70
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
71
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
72
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# Look in the tasks/setup.rb file for the various options that can be
|
2
|
+
# configured in this Rakefile. The .rake files in the tasks directory
|
3
|
+
# are where the options are used.
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'bones'
|
7
|
+
Bones.setup
|
8
|
+
rescue LoadError
|
9
|
+
load 'tasks/setup.rb'
|
10
|
+
end
|
11
|
+
|
12
|
+
ensure_in_path 'lib'
|
13
|
+
require 'smqueue'
|
14
|
+
|
15
|
+
task :default => "test"
|
16
|
+
|
17
|
+
PROJ.name = 'smqueue'
|
18
|
+
PROJ.authors = "Sean O'Halpin"
|
19
|
+
PROJ.email = 'sean.ohalpin@gmail.com'
|
20
|
+
PROJ.url = 'http://github.com/seanohalpin/smqueue'
|
21
|
+
PROJ.version = SMQueue::VERSION
|
22
|
+
PROJ.rubyforge.name = 'smqueue'
|
23
|
+
|
24
|
+
PROJ.spec.opts << '--color'
|
25
|
+
|
26
|
+
# EOF
|
data/lib/rstomp.rb
ADDED
@@ -0,0 +1,582 @@
|
|
1
|
+
# Copyright 2005-2006 Brian McCallister
|
2
|
+
# Copyright 2006 LogicBlaze Inc.
|
3
|
+
# Copyright 2008 Sean O'Halpin
|
4
|
+
# - refactored to use params hash
|
5
|
+
# - made more 'ruby-like'
|
6
|
+
# - use logger instead of $stderr
|
7
|
+
#
|
8
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
9
|
+
# you may not use this file except in compliance with the License.
|
10
|
+
# You may obtain a copy of the License at
|
11
|
+
#
|
12
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
13
|
+
#
|
14
|
+
# Unless required by applicable law or agreed to in writing, software
|
15
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
16
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
17
|
+
# See the License for the specific language governing permissions and
|
18
|
+
# limitations under the License.
|
19
|
+
|
20
|
+
require 'io/wait'
|
21
|
+
require 'socket'
|
22
|
+
require 'thread'
|
23
|
+
require 'stringio'
|
24
|
+
require 'logger'
|
25
|
+
|
26
|
+
if $DEBUG
|
27
|
+
require 'pp'
|
28
|
+
end
|
29
|
+
|
30
|
+
module RStomp
|
31
|
+
class RStompException < Exception
|
32
|
+
end
|
33
|
+
class ConnectionError < RStompException
|
34
|
+
end
|
35
|
+
class ReceiveError < RStompException
|
36
|
+
end
|
37
|
+
class InvalidContentLengthError < RStompException
|
38
|
+
end
|
39
|
+
class TransmitError < RStompException
|
40
|
+
end
|
41
|
+
class NoListenerError < RStompException
|
42
|
+
end
|
43
|
+
class NoDataError < RStompException
|
44
|
+
end
|
45
|
+
class InvalidFrameTerminationError < RStompException
|
46
|
+
end
|
47
|
+
|
48
|
+
# Low level connection which maps commands and supports
|
49
|
+
# synchronous receives
|
50
|
+
class Connection
|
51
|
+
attr_reader :current_host, :current_port
|
52
|
+
|
53
|
+
DEFAULT_OPTIONS = {
|
54
|
+
:user => "",
|
55
|
+
:password => "",
|
56
|
+
:host => 'localhost',
|
57
|
+
:port => 61613,
|
58
|
+
:reliable => false,
|
59
|
+
:reconnect_delay => 5,
|
60
|
+
:client_id => nil,
|
61
|
+
:logfile => STDERR,
|
62
|
+
:logger => nil,
|
63
|
+
}
|
64
|
+
|
65
|
+
# make them attributes
|
66
|
+
DEFAULT_OPTIONS.each do |key, value|
|
67
|
+
attr_accessor key
|
68
|
+
end
|
69
|
+
|
70
|
+
def Connection.open(params = {})
|
71
|
+
params = DEFAULT_OPTIONS.merge(params)
|
72
|
+
Connection.new(params)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Create a connection
|
76
|
+
# Options:
|
77
|
+
# - :user => ''
|
78
|
+
# - :password => ''
|
79
|
+
# - :host => 'localhost'
|
80
|
+
# - :port => 61613
|
81
|
+
# - :reliable => false (will keep retrying to send if true)
|
82
|
+
# - :reconnect_delay => 5 (seconds)
|
83
|
+
# - :client_id => nil (used in durable subscriptions)
|
84
|
+
# - :logfile => STDERR
|
85
|
+
# - :logger => Logger.new(params[:logfile])
|
86
|
+
#
|
87
|
+
def initialize(params = {})
|
88
|
+
params = DEFAULT_OPTIONS.merge(params)
|
89
|
+
@host = params[:host]
|
90
|
+
@port = params[:port]
|
91
|
+
@secondary_host = params[:secondary_host]
|
92
|
+
@secondary_port = params[:secondary_port]
|
93
|
+
|
94
|
+
@current_host = @host
|
95
|
+
@current_port = @port
|
96
|
+
|
97
|
+
@user = params[:user]
|
98
|
+
@password = params[:password]
|
99
|
+
@reliable = params[:reliable]
|
100
|
+
@reconnect_delay = params[:reconnect_delay]
|
101
|
+
@client_id = params[:client_id]
|
102
|
+
@logfile = params[:logfile]
|
103
|
+
@logger = params[:logger] || Logger.new(@logfile)
|
104
|
+
|
105
|
+
@transmit_semaphore = Mutex.new
|
106
|
+
@read_semaphore = Mutex.new
|
107
|
+
@socket_semaphore = Mutex.new
|
108
|
+
|
109
|
+
@subscriptions = {}
|
110
|
+
@failure = nil
|
111
|
+
@socket = nil
|
112
|
+
@open = false
|
113
|
+
|
114
|
+
socket
|
115
|
+
end
|
116
|
+
|
117
|
+
def socket
|
118
|
+
# Need to look into why the following synchronize does not work. (SOH: fixed)
|
119
|
+
# SOH: Causes Exception ThreadError 'stopping only thread note: use sleep to stop forever' at 235
|
120
|
+
# SOH: because had nested synchronize in _receive - take outside _receive (in receive) and seems OK
|
121
|
+
@socket_semaphore.synchronize do
|
122
|
+
s = @socket
|
123
|
+
headers = {
|
124
|
+
:user => @user,
|
125
|
+
:password => @password
|
126
|
+
}
|
127
|
+
headers['client-id'] = @client_id unless @client_id.nil?
|
128
|
+
# logger.debug "headers = #{headers.inspect} client_id = #{ @client_id }"
|
129
|
+
while s.nil? or @failure != nil
|
130
|
+
begin
|
131
|
+
#p [:connecting, :socket, s, :failure, @failure, @failure.class.ancestors, :closed, closed?]
|
132
|
+
# logger.info( { :status => :connecting, :host => host, :port => port }.inspect )
|
133
|
+
@failure = nil
|
134
|
+
|
135
|
+
s = TCPSocket.open(@current_host, @current_port)
|
136
|
+
|
137
|
+
_transmit(s, "CONNECT", headers)
|
138
|
+
@connect = _receive(s)
|
139
|
+
@open = true
|
140
|
+
|
141
|
+
# replay any subscriptions.
|
142
|
+
@subscriptions.each { |k, v| _transmit(s, "SUBSCRIBE", v) }
|
143
|
+
rescue Interrupt => e
|
144
|
+
#p [:interrupt, e]
|
145
|
+
# rescue Exception => e
|
146
|
+
rescue RStompException, SystemCallError => e
|
147
|
+
#p [:Exception, e]
|
148
|
+
@failure = e
|
149
|
+
# ensure socket is closed
|
150
|
+
begin
|
151
|
+
s.close if s
|
152
|
+
rescue Object => e
|
153
|
+
end
|
154
|
+
s = nil
|
155
|
+
@open = false
|
156
|
+
|
157
|
+
switch_host_and_port unless @secondary_host.empty?
|
158
|
+
|
159
|
+
handle_error ConnectionError, "connect failed: '#{e.message}' will retry in #{@reconnect_delay} on #{@current_host} port #{@current_port}", host.empty?
|
160
|
+
sleep(@reconnect_delay)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
@socket = s
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def switch_host_and_port
|
168
|
+
# Try connecting to the slave instead
|
169
|
+
# Or if the slave goes down, connect back to the master
|
170
|
+
# if it's not a reliable queue, then if the slave queue doesn't work then fail
|
171
|
+
if !@reliable && ((@current_host == @secondary_host) && (@current_port == @secondary_port))
|
172
|
+
@current_host = ''
|
173
|
+
@current_port = ''
|
174
|
+
else # switch the host from primary to secondary (or back again)
|
175
|
+
@current_host = (@current_host == @host ? @secondary_host : @host)
|
176
|
+
@current_port = (@current_port == @port ? @secondary_port : @port)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Is this connection open?
|
181
|
+
def open?
|
182
|
+
@open
|
183
|
+
end
|
184
|
+
|
185
|
+
# Is this connection closed?
|
186
|
+
def closed?
|
187
|
+
!open?
|
188
|
+
end
|
189
|
+
|
190
|
+
# Begin a transaction, requires a name for the transaction
|
191
|
+
def begin(name, headers = {})
|
192
|
+
headers[:transaction] = name
|
193
|
+
transmit "BEGIN", headers
|
194
|
+
end
|
195
|
+
|
196
|
+
# Acknowledge a message, used then a subscription has specified
|
197
|
+
# client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client' )
|
198
|
+
#
|
199
|
+
# Accepts a transaction header ( :transaction => 'some_transaction_id' )
|
200
|
+
def ack(message_id, headers = {})
|
201
|
+
headers['message-id'] = message_id
|
202
|
+
transmit "ACK", headers
|
203
|
+
end
|
204
|
+
|
205
|
+
# Commit a transaction by name
|
206
|
+
def commit(name, headers = {})
|
207
|
+
headers[:transaction] = name
|
208
|
+
transmit "COMMIT", headers
|
209
|
+
end
|
210
|
+
|
211
|
+
# Abort a transaction by name
|
212
|
+
def abort(name, headers = {})
|
213
|
+
headers[:transaction] = name
|
214
|
+
transmit "ABORT", headers
|
215
|
+
end
|
216
|
+
|
217
|
+
# Subscribe to a destination, must specify a name
|
218
|
+
def subscribe(name, headers = {}, subscription_id = nil)
|
219
|
+
headers[:destination] = name
|
220
|
+
transmit "SUBSCRIBE", headers
|
221
|
+
|
222
|
+
# Store the sub so that we can replay if we reconnect.
|
223
|
+
if @reliable
|
224
|
+
subscription_id = name if subscription_id.nil?
|
225
|
+
@subscriptions[subscription_id]=headers
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Unsubscribe from a destination, must specify a name
|
230
|
+
def unsubscribe(name, headers = {}, subscription_id = nil)
|
231
|
+
headers[:destination] = name
|
232
|
+
transmit "UNSUBSCRIBE", headers
|
233
|
+
if @reliable
|
234
|
+
subscription_id = name if subscription_id.nil?
|
235
|
+
@subscriptions.delete(subscription_id)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# Send message to destination
|
240
|
+
#
|
241
|
+
# Accepts a transaction header ( :transaction => 'some_transaction_id' )
|
242
|
+
def send(destination, message, headers = {})
|
243
|
+
headers[:destination] = destination
|
244
|
+
transmit "SEND", headers, message
|
245
|
+
end
|
246
|
+
|
247
|
+
# drain socket
|
248
|
+
def discard_all_until_eof
|
249
|
+
@read_semaphore.synchronize do
|
250
|
+
while @socket do
|
251
|
+
break if @socket.gets.nil?
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
private :discard_all_until_eof
|
256
|
+
|
257
|
+
# Close this connection
|
258
|
+
def disconnect(headers = {})
|
259
|
+
transmit "DISCONNECT", headers
|
260
|
+
discard_all_until_eof
|
261
|
+
begin
|
262
|
+
@socket.close
|
263
|
+
rescue Object => e
|
264
|
+
end
|
265
|
+
@socket = nil
|
266
|
+
@open = false
|
267
|
+
end
|
268
|
+
|
269
|
+
# Return a pending message if one is available, otherwise
|
270
|
+
# return nil
|
271
|
+
def poll
|
272
|
+
@read_semaphore.synchronize do
|
273
|
+
if @socket.nil? or !@socket.ready?
|
274
|
+
nil
|
275
|
+
else
|
276
|
+
receive
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# Receive a frame, block until the frame is received
|
282
|
+
def receive
|
283
|
+
# The receive may fail so we may need to retry.
|
284
|
+
# TODO: use retry count?
|
285
|
+
while true
|
286
|
+
begin
|
287
|
+
s = socket
|
288
|
+
rv = _receive(s)
|
289
|
+
return rv
|
290
|
+
# rescue Interrupt
|
291
|
+
# raise
|
292
|
+
rescue RStompException, SystemCallError => e
|
293
|
+
@failure = e
|
294
|
+
handle_error ReceiveError, "receive failed: #{e.message}"
|
295
|
+
# TODO: maybe sleep here?
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
private
|
301
|
+
def _receive( s )
|
302
|
+
#logger.debug "_receive"
|
303
|
+
line = ' '
|
304
|
+
@read_semaphore.synchronize do
|
305
|
+
#logger.debug "inside semaphore"
|
306
|
+
# skip blank lines
|
307
|
+
while line =~ /^\s*$/
|
308
|
+
#logger.debug "skipping blank line " + s.inspect
|
309
|
+
line = s.gets
|
310
|
+
end
|
311
|
+
if line.nil?
|
312
|
+
# FIXME: this loses data - maybe retry here if connection returns nil?
|
313
|
+
raise NoDataError, "connection returned nil"
|
314
|
+
nil
|
315
|
+
else
|
316
|
+
#logger.debug "got message data"
|
317
|
+
Message.new do |m|
|
318
|
+
m.command = line.chomp
|
319
|
+
m.headers = {}
|
320
|
+
until (line = s.gets.chomp) == ''
|
321
|
+
k = (line.strip[0, line.strip.index(':')]).strip
|
322
|
+
v = (line.strip[line.strip.index(':') + 1, line.strip.length]).strip
|
323
|
+
m.headers[k] = v
|
324
|
+
end
|
325
|
+
|
326
|
+
if m.headers['content-length']
|
327
|
+
m.body = s.read m.headers['content-length'].to_i
|
328
|
+
# expect an ASCII NUL (i.e. 0)
|
329
|
+
c = s.getc
|
330
|
+
handle_error InvalidContentLengthError, "Invalid content length received" unless c == 0
|
331
|
+
else
|
332
|
+
m.body = ''
|
333
|
+
until (c = s.getc) == 0
|
334
|
+
m.body << c.chr
|
335
|
+
end
|
336
|
+
end
|
337
|
+
if $DEBUG
|
338
|
+
logger.debug "Message #: #{m.headers['message-id']}"
|
339
|
+
logger.debug " Command: #{m.command}"
|
340
|
+
logger.debug " Headers:"
|
341
|
+
m.headers.sort.each do |key, value|
|
342
|
+
logger.debug " #{key}: #{m.headers[key]}"
|
343
|
+
end
|
344
|
+
logger.debug " Body: [#{m.body}]\n"
|
345
|
+
end
|
346
|
+
m
|
347
|
+
#c = s.getc
|
348
|
+
#handle_error InvalidFrameTerminationError, "Invalid frame termination received" unless c == 10
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
private
|
355
|
+
|
356
|
+
# route all error handling through this method
|
357
|
+
def handle_error(exception_class, error_message, force_raise = !@reliable)
|
358
|
+
logger.warn error_message
|
359
|
+
# if not an internal exception, then raise
|
360
|
+
if !(exception_class <= RStompException)
|
361
|
+
force_raise = true
|
362
|
+
end
|
363
|
+
raise exception_class, error_message if force_raise
|
364
|
+
end
|
365
|
+
|
366
|
+
def transmit(command, headers = {}, body = '')
|
367
|
+
# The transmit may fail so we may need to retry.
|
368
|
+
# Maybe use retry count?
|
369
|
+
while true
|
370
|
+
begin
|
371
|
+
_transmit(socket, command, headers, body)
|
372
|
+
return
|
373
|
+
# rescue Interrupt
|
374
|
+
# raise
|
375
|
+
rescue RStompException, SystemCallError => e
|
376
|
+
@failure = e
|
377
|
+
handle_error TransmitError, "transmit '#{command}' failed: #{e.message} (#{body})"
|
378
|
+
end
|
379
|
+
# TODO: sleep here?
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
private
|
384
|
+
def _transmit(s, command, headers={}, body='')
|
385
|
+
msg = StringIO.new
|
386
|
+
msg.puts command
|
387
|
+
headers.each {|k, v| msg.puts "#{k}: #{v}" }
|
388
|
+
msg.puts "content-length: #{body.nil? ? 0 : body.length}"
|
389
|
+
msg.puts "content-type: text/plain; charset=UTF-8"
|
390
|
+
msg.puts
|
391
|
+
msg.write body
|
392
|
+
msg.write "\0"
|
393
|
+
if $DEBUG
|
394
|
+
msg.rewind
|
395
|
+
logger.debug "_transmit"
|
396
|
+
msg.read.each_line do |line|
|
397
|
+
logger.debug line.chomp
|
398
|
+
end
|
399
|
+
end
|
400
|
+
msg.rewind
|
401
|
+
@transmit_semaphore.synchronize do
|
402
|
+
s.write msg.read
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
# Container class for frames, misnamed technically
|
408
|
+
class Message
|
409
|
+
attr_accessor :headers, :body, :command
|
410
|
+
|
411
|
+
def initialize(&block)
|
412
|
+
yield(self) if block_given?
|
413
|
+
end
|
414
|
+
|
415
|
+
def to_s
|
416
|
+
"<#{self.class} headers=#{headers.inspect} body=#{body.inspect} command=#{command.inspect} >"
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
# Typical Stomp client class. Uses a listener thread to receive frames
|
421
|
+
# from the server, any thread can send.
|
422
|
+
#
|
423
|
+
# Receives all happen in one thread, so consider not doing much processing
|
424
|
+
# in that thread if you have much message volume.
|
425
|
+
class Client
|
426
|
+
|
427
|
+
# Accepts the same options as Connection.open
|
428
|
+
# Also accepts a :uri parameter of form 'stomp://host:port' or 'stomp://user:password@host:port' in place
|
429
|
+
# of :user, :password, :host and :port parameters
|
430
|
+
def initialize(params = {})
|
431
|
+
params = Connection::DEFAULT_OPTIONS.merge(params)
|
432
|
+
uri = params.delete(:uri)
|
433
|
+
if uri =~ /stomp:\/\/([\w\.]+):(\d+)/
|
434
|
+
params[:user] = ""
|
435
|
+
params[:password] = ""
|
436
|
+
params[:host] = $1
|
437
|
+
params[:port] = $2
|
438
|
+
elsif uri =~ /stomp:\/\/([\w\.]+):(\w+)@(\w+):(\d+)/
|
439
|
+
params[:user] = $1
|
440
|
+
params[:password] = $2
|
441
|
+
params[:host] = $3
|
442
|
+
params[:port] = $4
|
443
|
+
end
|
444
|
+
|
445
|
+
@id_mutex = Mutex.new
|
446
|
+
@ids = 1
|
447
|
+
@connection = Connection.open(params)
|
448
|
+
@listeners = {}
|
449
|
+
@receipt_listeners = {}
|
450
|
+
@running = true
|
451
|
+
@replay_messages_by_txn = {}
|
452
|
+
@listener_thread = Thread.start do
|
453
|
+
while @running
|
454
|
+
message = @connection.receive
|
455
|
+
break if message.nil?
|
456
|
+
case message.command
|
457
|
+
when 'MESSAGE':
|
458
|
+
if listener = @listeners[message.headers['destination']]
|
459
|
+
listener.call(message)
|
460
|
+
end
|
461
|
+
when 'RECEIPT':
|
462
|
+
if listener = @receipt_listeners[message.headers['receipt-id']]
|
463
|
+
listener.call(message)
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
# Join the listener thread for this client,
|
471
|
+
# generally used to wait for a quit signal
|
472
|
+
def join
|
473
|
+
@listener_thread.join
|
474
|
+
end
|
475
|
+
|
476
|
+
# Accepts the same options as Connection.open
|
477
|
+
def self.open(params = {})
|
478
|
+
params = Connection::DEFAULT_OPTIONS.merge(params)
|
479
|
+
Client.new(params)
|
480
|
+
end
|
481
|
+
|
482
|
+
# Begin a transaction by name
|
483
|
+
def begin(name, headers = {})
|
484
|
+
@connection.begin name, headers
|
485
|
+
end
|
486
|
+
|
487
|
+
# Abort a transaction by name
|
488
|
+
def abort(name, headers = {})
|
489
|
+
@connection.abort name, headers
|
490
|
+
|
491
|
+
# lets replay any ack'd messages in this transaction
|
492
|
+
replay_list = @replay_messages_by_txn[name]
|
493
|
+
if replay_list
|
494
|
+
replay_list.each do |message|
|
495
|
+
if listener = @listeners[message.headers['destination']]
|
496
|
+
listener.call(message)
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
# Commit a transaction by name
|
503
|
+
def commit(name, headers = {})
|
504
|
+
txn_id = headers[:transaction]
|
505
|
+
@replay_messages_by_txn.delete(txn_id)
|
506
|
+
@connection.commit(name, headers)
|
507
|
+
end
|
508
|
+
|
509
|
+
# Subscribe to a destination, must be passed a block taking one parameter (the message)
|
510
|
+
# which will be used as a callback listener
|
511
|
+
#
|
512
|
+
# Accepts a transaction header ( :transaction => 'some_transaction_id' )
|
513
|
+
def subscribe(destination, headers = {}, &block)
|
514
|
+
handle_error NoListenerError, "No listener given" unless block_given?
|
515
|
+
@listeners[destination] = block
|
516
|
+
@connection.subscribe(destination, headers)
|
517
|
+
end
|
518
|
+
|
519
|
+
# Unsubscribe from a channel
|
520
|
+
def unsubscribe(name, headers = {})
|
521
|
+
@connection.unsubscribe name, headers
|
522
|
+
@listeners[name] = nil
|
523
|
+
end
|
524
|
+
|
525
|
+
# Acknowledge a message, used when a subscription has specified
|
526
|
+
# client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client' )
|
527
|
+
#
|
528
|
+
# Accepts a transaction header ( :transaction => 'some_transaction_id' )
|
529
|
+
def acknowledge(message, headers = {}, &block)
|
530
|
+
txn_id = headers[:transaction]
|
531
|
+
if txn_id
|
532
|
+
# lets keep around messages ack'd in this transaction in case we rollback
|
533
|
+
replay_list = @replay_messages_by_txn[txn_id]
|
534
|
+
if replay_list.nil?
|
535
|
+
replay_list = []
|
536
|
+
@replay_messages_by_txn[txn_id] = replay_list
|
537
|
+
end
|
538
|
+
replay_list << message
|
539
|
+
end
|
540
|
+
if block_given?
|
541
|
+
headers['receipt'] = register_receipt_listener(block)
|
542
|
+
end
|
543
|
+
@connection.ack(message.headers['message-id'], headers)
|
544
|
+
end
|
545
|
+
|
546
|
+
# Send message to destination
|
547
|
+
#
|
548
|
+
# If a block is given a receipt will be requested and passed to the
|
549
|
+
# block on receipt
|
550
|
+
#
|
551
|
+
# Accepts a transaction header ( :transaction => 'some_transaction_id' )
|
552
|
+
def send(destination, message, headers = {}, &block)
|
553
|
+
if block_given?
|
554
|
+
headers['receipt'] = register_receipt_listener(block)
|
555
|
+
end
|
556
|
+
@connection.send destination, message, headers
|
557
|
+
end
|
558
|
+
|
559
|
+
# Is this client open?
|
560
|
+
def open?
|
561
|
+
@connection.open?
|
562
|
+
end
|
563
|
+
|
564
|
+
# Close out resources in use by this client
|
565
|
+
def close
|
566
|
+
@connection.disconnect
|
567
|
+
@running = false
|
568
|
+
end
|
569
|
+
|
570
|
+
private
|
571
|
+
def register_receipt_listener(listener)
|
572
|
+
id = -1
|
573
|
+
@id_mutex.synchronize do
|
574
|
+
id = @ids.to_s
|
575
|
+
@ids = @ids.succ
|
576
|
+
end
|
577
|
+
@receipt_listeners[id] = listener
|
578
|
+
id
|
579
|
+
end
|
580
|
+
|
581
|
+
end
|
582
|
+
end
|