craigw-smqueue 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ == 0.2.1 / 2009-01-25
2
+
3
+ * Enable TCP_KEEPALIVE on STOMP sockets
4
+
5
+ == 0.1.0 / 2008-11-14
6
+
7
+ * Initial release
8
+ - for Craig and Rija - enjoy :D
@@ -0,0 +1,16 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ examples/input.rb
6
+ examples/output.rb
7
+ examples/config/example_config.yml
8
+ lib/rstomp.rb
9
+ lib/smqueue.rb
10
+ lib/smqueue/adapters/amqp.rb
11
+ lib/smqueue/adapters/spread.rb
12
+ lib/smqueue/adapters/stdio.rb
13
+ lib/smqueue/adapters/stomp.rb
14
+ smqueue.gemspec
15
+ test/helper.rb
16
+ test/test_rstomp_connection.rb
@@ -0,0 +1,87 @@
1
+ = smqueue
2
+
3
+ by Sean O'Halpin
4
+
5
+ http://github.com/seanohalpin/smqueue
6
+
7
+ version 0.2.1
8
+
9
+ == DESCRIPTION
10
+
11
+ Implements a simple protocol for using message queues, with adapters
12
+ for ActiveMQ, Spread and stdio (for testing).
13
+
14
+ This is a bare-bones release to share with my colleagues - apologies
15
+ for the lack of documentation and tests.
16
+
17
+ == FEATURES
18
+
19
+ * simple to use
20
+ * designed primarily for pipeline processing
21
+ * compatible with Rails
22
+ * comes with a modified stomp.rb library (rstomp.rb)
23
+
24
+ == BUGS
25
+
26
+ * let me know!
27
+
28
+ == SYNOPSIS
29
+
30
+ require 'smqueue'
31
+ config = YAML::load(File.read('config.yml'))
32
+ input_queue = SMQueue(config[:input])
33
+ output_queue = SMQueue(config[:output])
34
+ input_queue.get do |msg|
35
+ # do something with msg
36
+ new_msg = msg.body
37
+ output_queue.put new_msg
38
+ end
39
+
40
+ === Configuration
41
+
42
+ The easiest way to configure SMQueue is to use YAML files. An example
43
+ can be found in ./examples/config/example_config.yml.
44
+
45
+ To find out the complete set of options each adapter supports, for now
46
+ you'll have to read the source in ./lib/smqueue/adapters/*.rb.
47
+
48
+ == REQUIREMENTS
49
+
50
+ * depends on doodle >= 0.1.9
51
+ * you need access to an ActiveMQ message broker or Spread publisher
52
+ * development uses bones gem
53
+
54
+ == INSTALL
55
+
56
+ For runtime:
57
+
58
+ sudo gem install doodle smqueue
59
+
60
+ For development:
61
+
62
+ sudo gem install bones
63
+
64
+ == LICENSE
65
+
66
+ (The MIT License)
67
+
68
+ Copyright (c) 2008 Sean O'Halpin
69
+
70
+ Permission is hereby granted, free of charge, to any person obtaining
71
+ a copy of this software and associated documentation files (the
72
+ 'Software'), to deal in the Software without restriction, including
73
+ without limitation the rights to use, copy, modify, merge, publish,
74
+ distribute, sublicense, and/or sell copies of the Software, and to
75
+ permit persons to whom the Software is furnished to do so, subject to
76
+ the following conditions:
77
+
78
+ The above copyright notice and this permission notice shall be
79
+ included in all copies or substantial portions of the Software.
80
+
81
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
82
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
83
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
84
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
85
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
86
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
87
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -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
@@ -0,0 +1,40 @@
1
+ mq: &default_mq
2
+ :adapter: StompAdapter
3
+ # :host: localhost
4
+ :host: 192.168.53.134
5
+ :port: 61613
6
+ :reconnect_delay: 5
7
+
8
+ :announce:
9
+ :adapter: SpreadAdapter
10
+ :channel: 4803@localhost
11
+ :group: announce
12
+
13
+ :scheduler:
14
+ <<: *default_mq
15
+ :name: /queue/development.scheduler
16
+ :reliable: true
17
+
18
+ :change_events:
19
+ <<: *default_mq
20
+ :name: /topic/change_events
21
+ :reliable: true
22
+
23
+ :input:
24
+ <<: *default_mq
25
+ :name: /queue/shared
26
+ :reliable: true
27
+
28
+ :output:
29
+ <<: *default_mq
30
+ :name: /queue/shared
31
+ :reliable: true
32
+
33
+ :readline:
34
+ :adapter: ReadlineAdapter
35
+
36
+ :stdio:
37
+ :adapter: StdioAdapter
38
+
39
+ :error:
40
+ :adapter: NullAdapter
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ require 'smqueue'
3
+ require 'pp'
4
+
5
+ script_path = File.dirname(__FILE__)
6
+ configuration = YAML::load(File.read(File.join(script_path, "config", "example_config.yml")))
7
+
8
+ input_queue = SMQueue.new(:configuration => configuration[:input])
9
+
10
+ input_queue.get do |msg|
11
+ pp msg
12
+ puts "-" * 40
13
+ puts msg.body
14
+ puts "-" * 40
15
+ end
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'smqueue'
3
+
4
+ script_path = File.dirname(__FILE__)
5
+ configuration = YAML::load(File.read(File.join(script_path, "config", "example_config.yml")))
6
+
7
+ input_queue = SMQueue.new(:configuration => configuration[:readline])
8
+ output_queue = SMQueue.new(:configuration => configuration[:output])
9
+
10
+ input_queue.get do |msg|
11
+ output_queue.put msg.body
12
+ end
@@ -0,0 +1,658 @@
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
+
27
+ # use keepalive to detect dead connections (see http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/)
28
+
29
+ module SocketExtensions
30
+ # Linux
31
+ module Linux
32
+ # /usr/include/netinet/tcp.h
33
+ TCP_KEEPIDLE = 4
34
+ TCP_KEEPINTVL = 5
35
+ TCP_KEEPCNT = 6
36
+ end
37
+ module Darwin
38
+ # Mac OSX
39
+ # tcp.h:#define TCP_KEEPALIVE 0x10 /* idle time used when SO_KEEPALIVE is enabled */
40
+ TCP_KEEPALIVE = 0x10
41
+ # these are sysctl vars
42
+ # /usr/include/netinet/tcp_var.h:#define TCPCTL_KEEPIDLE 6 /* keepalive idle timer */
43
+ # /usr/include/netinet/tcp_var.h:#define TCPCTL_KEEPINTVL 7 /* interval to send keepalives */
44
+ # /usr/include/netinet/tcp_var.h:#define TCPCTL_KEEPINIT 10 /* timeout for establishing syn */ end
45
+ end
46
+ end
47
+
48
+
49
+ if $DEBUG
50
+ require 'pp'
51
+ end
52
+
53
+ module RStomp
54
+ class RStompException < Exception
55
+ end
56
+ class ConnectionError < RStompException
57
+ end
58
+ class ReceiveError < RStompException
59
+ end
60
+ class InvalidContentLengthError < RStompException
61
+ end
62
+ class TransmitError < RStompException
63
+ end
64
+ class NoListenerError < RStompException
65
+ end
66
+ class NoDataError < RStompException
67
+ end
68
+ class InvalidFrameTerminationError < RStompException
69
+ end
70
+
71
+ # Low level connection which maps commands and supports
72
+ # synchronous receives
73
+ class Connection
74
+ attr_reader :current_host, :current_port
75
+
76
+ DEFAULT_OPTIONS = {
77
+ :user => "",
78
+ :password => "",
79
+ :host => 'localhost',
80
+ :port => 61613,
81
+ :reliable => false,
82
+ :reconnect_delay => 5,
83
+ :client_id => nil,
84
+ :logfile => STDERR,
85
+ :logger => nil,
86
+ }
87
+
88
+ # make them attributes
89
+ DEFAULT_OPTIONS.each do |key, value|
90
+ attr_accessor key
91
+ end
92
+
93
+ def Connection.open(params = {})
94
+ params = DEFAULT_OPTIONS.merge(params)
95
+ Connection.new(params)
96
+ end
97
+
98
+ # Create a connection
99
+ # Options:
100
+ # - :user => ''
101
+ # - :password => ''
102
+ # - :host => 'localhost'
103
+ # - :port => 61613
104
+ # - :reliable => false (will keep retrying to send if true)
105
+ # - :reconnect_delay => 5 (seconds)
106
+ # - :client_id => nil (used in durable subscriptions)
107
+ # - :logfile => STDERR
108
+ # - :logger => Logger.new(params[:logfile])
109
+ #
110
+ def initialize(params = {})
111
+ params = DEFAULT_OPTIONS.merge(params)
112
+ @host = params[:host]
113
+ @port = params[:port]
114
+ @secondary_host = params[:secondary_host]
115
+ @secondary_port = params[:secondary_port]
116
+
117
+ @current_host = @host
118
+ @current_port = @port
119
+
120
+ @user = params[:user]
121
+ @password = params[:password]
122
+ @reliable = params[:reliable]
123
+ @reconnect_delay = params[:reconnect_delay]
124
+ @client_id = params[:client_id]
125
+ @logfile = params[:logfile]
126
+ @logger = params[:logger] || Logger.new(@logfile)
127
+
128
+ @transmit_semaphore = Mutex.new
129
+ @read_semaphore = Mutex.new
130
+ @socket_semaphore = Mutex.new
131
+
132
+ @subscriptions = {}
133
+ @failure = nil
134
+ @socket = nil
135
+ @open = false
136
+
137
+ socket
138
+ end
139
+
140
+ def socket
141
+ # Need to look into why the following synchronize does not work. (SOH: fixed)
142
+ # SOH: Causes Exception ThreadError 'stopping only thread note: use sleep to stop forever' at 235
143
+ # SOH: because had nested synchronize in _receive - take outside _receive (in receive) and seems OK
144
+ @socket_semaphore.synchronize do
145
+ s = @socket
146
+ headers = {
147
+ :user => @user,
148
+ :password => @password
149
+ }
150
+ headers['client-id'] = @client_id unless @client_id.nil?
151
+ # logger.debug "headers = #{headers.inspect} client_id = #{ @client_id }"
152
+ while s.nil? or @failure != nil
153
+ begin
154
+ #p [:connecting, :socket, s, :failure, @failure, @failure.class.ancestors, :closed, closed?]
155
+ # logger.info( { :status => :connecting, :host => host, :port => port }.inspect )
156
+ @failure = nil
157
+
158
+ s = TCPSocket.open(@current_host, @current_port)
159
+
160
+ # use keepalive to detect dead connections (see http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/)
161
+ s.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
162
+
163
+ case RUBY_PLATFORM
164
+ when /linux/
165
+ # note: if using OpenSSL, you may need to do this:
166
+ # ssl_socket.to_io.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
167
+ # see http://www.lerfjhax.com/articles/2006/08/22/ruby-ssl-setsockopt
168
+
169
+ # defaults
170
+ # $ cat /proc/sys/net/ipv4/tcp_keepalive_time
171
+ # 7200
172
+ # $ cat /proc/sys/net/ipv4/tcp_keepalive_intvl
173
+ # 75
174
+ # $ cat /proc/sys/net/ipv4/tcp_keepalive_probes
175
+ # 9
176
+
177
+ # these values should all be configurable (but with sensible defaults)
178
+
179
+ # the interval between the last data packet sent (simple
180
+ # ACKs are not considered data) and the first keepalive
181
+ # probe; after the connection is marked to need
182
+ # keepalive, this counter is not used any further
183
+ s.setsockopt(Socket::IPPROTO_TCP, SocketExtensions::Linux::TCP_KEEPIDLE, 20)
184
+ # the interval between subsequential keepalive probes,
185
+ # regardless of what the connection has exchanged in the
186
+ # meantime
187
+ s.setsockopt(Socket::IPPROTO_TCP, SocketExtensions::Linux::TCP_KEEPINTVL, 10)
188
+ # the number of unacknowledged probes to send before
189
+ # considering the connection dead and notifying the
190
+ # application layer
191
+
192
+ # NOTE: I did not see any effect from setting this
193
+ # option
194
+ s.setsockopt(Socket::IPPROTO_TCP, SocketExtensions::Linux::TCP_KEEPCNT, 6)
195
+ when /darwin/
196
+ # this works, with value = 100 actually takes 12 minutes
197
+ # 55 secs to time out (with opt = 100); with value = 10,
198
+ # takes 685 seconds
199
+
200
+ # ttl = KEEPIDLE + (9 * 75) - cannot change INTVL and
201
+ # CNT per socket on Darwin
202
+
203
+ # set KEEPIDLE time (in seconds) - wait one minute
204
+ # before sending KEEPALIVE packet (for testing - use
205
+ # more realistic figure for real)
206
+ #p [:setting_keepalive]
207
+ opt = [60].pack('l')
208
+ s.setsockopt(Socket::IPPROTO_TCP, SocketExtensions::Darwin::TCP_KEEPALIVE, opt)
209
+ when /jruby/
210
+ else
211
+ end
212
+
213
+ _transmit(s, "CONNECT", headers)
214
+ @connect = _receive(s)
215
+ @open = true
216
+
217
+ # replay any subscriptions.
218
+ @subscriptions.each { |k, v| _transmit(s, "SUBSCRIBE", v) }
219
+ rescue Interrupt => e
220
+ #p [:interrupt, e]
221
+ # rescue Exception => e
222
+ rescue RStompException, SystemCallError => e
223
+ #p [:Exception, e]
224
+ @failure = e
225
+ # ensure socket is closed
226
+ begin
227
+ s.close if s
228
+ rescue Object => e
229
+ end
230
+ s = nil
231
+ @open = false
232
+
233
+ switch_host_and_port unless @secondary_host.empty?
234
+
235
+ handle_error ConnectionError, "connect failed: '#{e.message}' will retry in #{@reconnect_delay} on #{@current_host} port #{@current_port}", host.empty?
236
+ sleep(@reconnect_delay)
237
+ end
238
+ end
239
+ @socket = s
240
+ end
241
+ end
242
+
243
+ def switch_host_and_port
244
+ # Try connecting to the slave instead
245
+ # Or if the slave goes down, connect back to the master
246
+ # if it's not a reliable queue, then if the slave queue doesn't work then fail
247
+ if !@reliable && ((@current_host == @secondary_host) && (@current_port == @secondary_port))
248
+ @current_host = ''
249
+ @current_port = ''
250
+ else # switch the host from primary to secondary (or back again)
251
+ @current_host = (@current_host == @host ? @secondary_host : @host)
252
+ @current_port = (@current_port == @port ? @secondary_port : @port)
253
+ end
254
+ end
255
+
256
+ # Is this connection open?
257
+ def open?
258
+ @open
259
+ end
260
+
261
+ # Is this connection closed?
262
+ def closed?
263
+ !open?
264
+ end
265
+
266
+ # Begin a transaction, requires a name for the transaction
267
+ def begin(name, headers = {})
268
+ headers[:transaction] = name
269
+ transmit "BEGIN", headers
270
+ end
271
+
272
+ # Acknowledge a message, used then a subscription has specified
273
+ # client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client' )
274
+ #
275
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
276
+ def ack(message_id, headers = {})
277
+ headers['message-id'] = message_id
278
+ transmit "ACK", headers
279
+ end
280
+
281
+ # Commit a transaction by name
282
+ def commit(name, headers = {})
283
+ headers[:transaction] = name
284
+ transmit "COMMIT", headers
285
+ end
286
+
287
+ # Abort a transaction by name
288
+ def abort(name, headers = {})
289
+ headers[:transaction] = name
290
+ transmit "ABORT", headers
291
+ end
292
+
293
+ # Subscribe to a destination, must specify a name
294
+ def subscribe(name, headers = {}, subscription_id = nil)
295
+ headers[:destination] = name
296
+ transmit "SUBSCRIBE", headers
297
+
298
+ # Store the sub so that we can replay if we reconnect.
299
+ if @reliable
300
+ subscription_id = name if subscription_id.nil?
301
+ @subscriptions[subscription_id]=headers
302
+ end
303
+ end
304
+
305
+ # Unsubscribe from a destination, must specify a name
306
+ def unsubscribe(name, headers = {}, subscription_id = nil)
307
+ headers[:destination] = name
308
+ transmit "UNSUBSCRIBE", headers
309
+ if @reliable
310
+ subscription_id = name if subscription_id.nil?
311
+ @subscriptions.delete(subscription_id)
312
+ end
313
+ end
314
+
315
+ # Send message to destination
316
+ #
317
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
318
+ def send(destination, message, headers = {})
319
+ headers[:destination] = destination
320
+ transmit "SEND", headers, message
321
+ end
322
+
323
+ # drain socket
324
+ def discard_all_until_eof
325
+ @read_semaphore.synchronize do
326
+ while @socket do
327
+ break if @socket.gets.nil?
328
+ end
329
+ end
330
+ end
331
+ private :discard_all_until_eof
332
+
333
+ # Close this connection
334
+ def disconnect(headers = {})
335
+ transmit "DISCONNECT", headers
336
+ discard_all_until_eof
337
+ begin
338
+ @socket.close
339
+ rescue Object => e
340
+ end
341
+ @socket = nil
342
+ @open = false
343
+ end
344
+
345
+ # Return a pending message if one is available, otherwise
346
+ # return nil
347
+ def poll
348
+ @read_semaphore.synchronize do
349
+ if @socket.nil? or !@socket.ready?
350
+ nil
351
+ else
352
+ receive
353
+ end
354
+ end
355
+ end
356
+
357
+ # Receive a frame, block until the frame is received
358
+ def receive
359
+ # The receive may fail so we may need to retry.
360
+ # TODO: use retry count?
361
+ while true
362
+ begin
363
+ s = socket
364
+ rv = _receive(s)
365
+ return rv
366
+ # rescue Interrupt
367
+ # raise
368
+ rescue RStompException, SystemCallError => e
369
+ @failure = e
370
+ handle_error ReceiveError, "receive failed: #{e.message}"
371
+ # TODO: maybe sleep here?
372
+ end
373
+ end
374
+ end
375
+
376
+ private
377
+ def _receive( s )
378
+ #logger.debug "_receive"
379
+ line = ' '
380
+ @read_semaphore.synchronize do
381
+ #logger.debug "inside semaphore"
382
+ # skip blank lines
383
+ while line =~ /^\s*$/
384
+ #logger.debug "skipping blank line " + s.inspect
385
+ line = s.gets
386
+ end
387
+ if line.nil?
388
+ # FIXME: this loses data - maybe retry here if connection returns nil?
389
+ raise NoDataError, "connection returned nil"
390
+ nil
391
+ else
392
+ #logger.debug "got message data"
393
+ Message.new do |m|
394
+ m.command = line.chomp
395
+ m.headers = {}
396
+ until (line = s.gets.chomp) == ''
397
+ k = (line.strip[0, line.strip.index(':')]).strip
398
+ v = (line.strip[line.strip.index(':') + 1, line.strip.length]).strip
399
+ m.headers[k] = v
400
+ end
401
+
402
+ if m.headers['content-length']
403
+ m.body = s.read m.headers['content-length'].to_i
404
+ # expect an ASCII NUL (i.e. 0)
405
+ c = s.getc
406
+ handle_error InvalidContentLengthError, "Invalid content length received" unless c == 0
407
+ else
408
+ m.body = ''
409
+ until (c = s.getc) == 0
410
+ m.body << c.chr
411
+ end
412
+ end
413
+ if $DEBUG
414
+ logger.debug "Message #: #{m.headers['message-id']}"
415
+ logger.debug " Command: #{m.command}"
416
+ logger.debug " Headers:"
417
+ m.headers.sort.each do |key, value|
418
+ logger.debug " #{key}: #{m.headers[key]}"
419
+ end
420
+ logger.debug " Body: [#{m.body}]\n"
421
+ end
422
+ m
423
+ #c = s.getc
424
+ #handle_error InvalidFrameTerminationError, "Invalid frame termination received" unless c == 10
425
+ end
426
+ end
427
+ end
428
+ end
429
+
430
+ private
431
+
432
+ # route all error handling through this method
433
+ def handle_error(exception_class, error_message, force_raise = !@reliable)
434
+ logger.warn error_message
435
+ # if not an internal exception, then raise
436
+ if !(exception_class <= RStompException)
437
+ force_raise = true
438
+ end
439
+ raise exception_class, error_message if force_raise
440
+ end
441
+
442
+ def transmit(command, headers = {}, body = '')
443
+ # The transmit may fail so we may need to retry.
444
+ # Maybe use retry count?
445
+ while true
446
+ begin
447
+ _transmit(socket, command, headers, body)
448
+ return
449
+ # rescue Interrupt
450
+ # raise
451
+ rescue RStompException, SystemCallError => e
452
+ @failure = e
453
+ handle_error TransmitError, "transmit '#{command}' failed: #{e.message} (#{body})"
454
+ end
455
+ # TODO: sleep here?
456
+ end
457
+ end
458
+
459
+ private
460
+ def _transmit(s, command, headers={}, body='')
461
+ msg = StringIO.new
462
+ msg.puts command
463
+ headers.each {|k, v| msg.puts "#{k}: #{v}" }
464
+ msg.puts "content-length: #{body.nil? ? 0 : body.length}"
465
+ msg.puts "content-type: text/plain; charset=UTF-8"
466
+ msg.puts
467
+ msg.write body
468
+ msg.write "\0"
469
+ if $DEBUG
470
+ msg.rewind
471
+ logger.debug "_transmit"
472
+ msg.read.each_line do |line|
473
+ logger.debug line.chomp
474
+ end
475
+ end
476
+ msg.rewind
477
+ @transmit_semaphore.synchronize do
478
+ s.write msg.read
479
+ end
480
+ end
481
+ end
482
+
483
+ # Container class for frames, misnamed technically
484
+ class Message
485
+ attr_accessor :headers, :body, :command
486
+
487
+ def initialize(&block)
488
+ yield(self) if block_given?
489
+ end
490
+
491
+ def to_s
492
+ "<#{self.class} headers=#{headers.inspect} body=#{body.inspect} command=#{command.inspect} >"
493
+ end
494
+ end
495
+
496
+ # Typical Stomp client class. Uses a listener thread to receive frames
497
+ # from the server, any thread can send.
498
+ #
499
+ # Receives all happen in one thread, so consider not doing much processing
500
+ # in that thread if you have much message volume.
501
+ class Client
502
+
503
+ # Accepts the same options as Connection.open
504
+ # Also accepts a :uri parameter of form 'stomp://host:port' or 'stomp://user:password@host:port' in place
505
+ # of :user, :password, :host and :port parameters
506
+ def initialize(params = {})
507
+ params = Connection::DEFAULT_OPTIONS.merge(params)
508
+ uri = params.delete(:uri)
509
+ if uri =~ /stomp:\/\/([\w\.]+):(\d+)/
510
+ params[:user] = ""
511
+ params[:password] = ""
512
+ params[:host] = $1
513
+ params[:port] = $2
514
+ elsif uri =~ /stomp:\/\/([\w\.]+):(\w+)@(\w+):(\d+)/
515
+ params[:user] = $1
516
+ params[:password] = $2
517
+ params[:host] = $3
518
+ params[:port] = $4
519
+ end
520
+
521
+ @id_mutex = Mutex.new
522
+ @ids = 1
523
+ @connection = Connection.open(params)
524
+ @listeners = {}
525
+ @receipt_listeners = {}
526
+ @running = true
527
+ @replay_messages_by_txn = {}
528
+ @listener_thread = Thread.start do
529
+ while @running
530
+ message = @connection.receive
531
+ break if message.nil?
532
+ case message.command
533
+ when 'MESSAGE':
534
+ if listener = @listeners[message.headers['destination']]
535
+ listener.call(message)
536
+ end
537
+ when 'RECEIPT':
538
+ if listener = @receipt_listeners[message.headers['receipt-id']]
539
+ listener.call(message)
540
+ end
541
+ end
542
+ end
543
+ end
544
+ end
545
+
546
+ # Join the listener thread for this client,
547
+ # generally used to wait for a quit signal
548
+ def join
549
+ @listener_thread.join
550
+ end
551
+
552
+ # Accepts the same options as Connection.open
553
+ def self.open(params = {})
554
+ params = Connection::DEFAULT_OPTIONS.merge(params)
555
+ Client.new(params)
556
+ end
557
+
558
+ # Begin a transaction by name
559
+ def begin(name, headers = {})
560
+ @connection.begin name, headers
561
+ end
562
+
563
+ # Abort a transaction by name
564
+ def abort(name, headers = {})
565
+ @connection.abort name, headers
566
+
567
+ # lets replay any ack'd messages in this transaction
568
+ replay_list = @replay_messages_by_txn[name]
569
+ if replay_list
570
+ replay_list.each do |message|
571
+ if listener = @listeners[message.headers['destination']]
572
+ listener.call(message)
573
+ end
574
+ end
575
+ end
576
+ end
577
+
578
+ # Commit a transaction by name
579
+ def commit(name, headers = {})
580
+ txn_id = headers[:transaction]
581
+ @replay_messages_by_txn.delete(txn_id)
582
+ @connection.commit(name, headers)
583
+ end
584
+
585
+ # Subscribe to a destination, must be passed a block taking one parameter (the message)
586
+ # which will be used as a callback listener
587
+ #
588
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
589
+ def subscribe(destination, headers = {}, &block)
590
+ handle_error NoListenerError, "No listener given" unless block_given?
591
+ @listeners[destination] = block
592
+ @connection.subscribe(destination, headers)
593
+ end
594
+
595
+ # Unsubscribe from a channel
596
+ def unsubscribe(name, headers = {})
597
+ @connection.unsubscribe name, headers
598
+ @listeners[name] = nil
599
+ end
600
+
601
+ # Acknowledge a message, used when a subscription has specified
602
+ # client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client' )
603
+ #
604
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
605
+ def acknowledge(message, headers = {}, &block)
606
+ txn_id = headers[:transaction]
607
+ if txn_id
608
+ # lets keep around messages ack'd in this transaction in case we rollback
609
+ replay_list = @replay_messages_by_txn[txn_id]
610
+ if replay_list.nil?
611
+ replay_list = []
612
+ @replay_messages_by_txn[txn_id] = replay_list
613
+ end
614
+ replay_list << message
615
+ end
616
+ if block_given?
617
+ headers['receipt'] = register_receipt_listener(block)
618
+ end
619
+ @connection.ack(message.headers['message-id'], headers)
620
+ end
621
+
622
+ # Send message to destination
623
+ #
624
+ # If a block is given a receipt will be requested and passed to the
625
+ # block on receipt
626
+ #
627
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
628
+ def send(destination, message, headers = {}, &block)
629
+ if block_given?
630
+ headers['receipt'] = register_receipt_listener(block)
631
+ end
632
+ @connection.send destination, message, headers
633
+ end
634
+
635
+ # Is this client open?
636
+ def open?
637
+ @connection.open?
638
+ end
639
+
640
+ # Close out resources in use by this client
641
+ def close
642
+ @connection.disconnect
643
+ @running = false
644
+ end
645
+
646
+ private
647
+ def register_receipt_listener(listener)
648
+ id = -1
649
+ @id_mutex.synchronize do
650
+ id = @ids.to_s
651
+ @ids = @ids.succ
652
+ end
653
+ @receipt_listeners[id] = listener
654
+ id
655
+ end
656
+
657
+ end
658
+ end