seanohalpin-smqueue 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 0.1.0 / 2008-11-14
2
+
3
+ * Initial release
4
+ - for Craig and Rija - enjoy :D
data/Manifest.txt ADDED
@@ -0,0 +1,15 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ examples/input.rb
6
+ examples/message_queue.yml
7
+ examples/output.rb
8
+ lib/rstomp.rb
9
+ lib/smqueue.rb
10
+ lib/smqueue/adapters/spread.rb
11
+ lib/smqueue/adapters/stdio.rb
12
+ lib/smqueue/adapters/stomp.rb
13
+ smqueue.gemspec
14
+ test/helper.rb
15
+ test/test_rstomp_connection.rb
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/examples/input.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'rubygems'
2
+ require 'smqueue'
3
+ require 'pp'
4
+
5
+ configuration = YAML::load(File.read("message_queue.yml"))
6
+
7
+ input_queue = SMQueue.new(:configuration => configuration[:input])
8
+
9
+ input_queue.get do |msg|
10
+ pp msg
11
+ puts "-" * 40
12
+ puts msg.body
13
+ puts "-" * 40
14
+ end
@@ -0,0 +1,29 @@
1
+ mq: &default_mq
2
+ :adapter: StompAdapter
3
+ :host: localhost
4
+ :port: 61613
5
+ :reconnect_delay: 5
6
+
7
+ :input:
8
+ <<: *default_mq
9
+ :name: /topic/example.queue
10
+
11
+ :output:
12
+ <<: *default_mq
13
+ :name: /topic/example.queue
14
+
15
+ :null:
16
+ :adapter: NullAdapter
17
+
18
+ :stdio:
19
+ :adapter: StdioAdapter
20
+
21
+ :yaml:
22
+ :adapter: YamlAdapter
23
+
24
+ :stdio_line:
25
+ :adapter: StdioLineAdapter
26
+
27
+ :readline:
28
+ :adapter: ReadlineAdapter
29
+
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'smqueue'
3
+
4
+ configuration = YAML::load(File.read("message_queue.yml"))
5
+
6
+ input_queue = SMQueue.new(:configuration => configuration[:readline])
7
+ output_queue = SMQueue.new(:configuration => configuration[:output])
8
+
9
+ input_queue.get do |msg|
10
+ output_queue.put msg.body
11
+ end
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