craigw-smqueue 0.2.3

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.
@@ -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