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,231 @@
1
+ # SMQueue
2
+ # Simple Message Queue client
3
+ # Sean O'Halpin, 2008
4
+
5
+ # The high-level client API is:
6
+ # - SMQueue(config).put msg, headers = {}
7
+ # - msg = SMQueue(config).get(headers = {})
8
+ # - SMQueue(config).get(headers = {}) do |msg|
9
+ # end
10
+ # todo - [X] add :durable option (and fail if no client_id specified)
11
+ # todo - [ ] gemify - use Mr Bones
12
+ # todo - [ ] change to class (so can be subclassed) - so you're working with an SMQueue instance
13
+ # todo - [ ] write protocol (open, close, put, get) in SMQueue (so don't have to do it in adaptors)
14
+ # todo - [ ] simplify StompAdapter (get rid of sent_messages stuff)
15
+ # todo - [ ] simplify adapter interface
16
+ # todo - [ ] sort out libpath
17
+
18
+ require 'rubygems'
19
+ require 'doodle'
20
+ require 'doodle/version'
21
+ require 'yaml'
22
+
23
+ class Doodle
24
+ if Doodle::VERSION::STRING < '0.1.9'
25
+ def to_hash
26
+ doodle.attributes.inject({}) {|hash, (name, attribute)| hash[name] = send(name); hash}
27
+ end
28
+ class DoodleAttribute
29
+ has :doc, :kind => String
30
+ end
31
+ end
32
+ end
33
+
34
+ #class SMQueue < Doodle
35
+ module SMQueue
36
+
37
+ # Mr Bones project skeleton boilerplate
38
+ # :stopdoc:
39
+ VERSION = '0.2.0'
40
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
41
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
42
+ # :startdoc:
43
+
44
+ # Returns the version string for the library.
45
+ #
46
+ def self.version
47
+ VERSION
48
+ end
49
+
50
+ # Returns the library path for the module. If any arguments are given,
51
+ # they will be joined to the end of the libray path using
52
+ # <tt>File.join</tt>.
53
+ #
54
+ def self.libpath( *args )
55
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
56
+ end
57
+
58
+ # Returns the lpath for the module. If any arguments are given,
59
+ # they will be joined to the end of the path using
60
+ # <tt>File.join</tt>.
61
+ #
62
+ def self.path( *args )
63
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
64
+ end
65
+
66
+ # Utility method used to rquire all files ending in .rb that lie in the
67
+ # directory below this file that has the same name as the filename passed
68
+ # in. Optionally, a specific _directory_ name can be passed in such that
69
+ # the _filename_ does not have to be equivalent to the directory.
70
+ #
71
+ def self.require_all_libs_relative_to( fname, dir = nil )
72
+ dir ||= ::File.basename(fname, '.*')
73
+ search_me = ::File.expand_path(
74
+ ::File.join(::File.dirname(fname), dir, '*', '*.rb'))
75
+
76
+ Dir.glob(search_me).sort.each {|rb| require rb}
77
+ end
78
+
79
+ # end Bones boilerplate
80
+
81
+ class << self
82
+ def dbg(*args, &block)
83
+ if $DEBUG
84
+ if args.size > 0
85
+ STDERR.print "SMQUEUE.DBG: "
86
+ STDERR.puts(*args)
87
+ end
88
+ if block_given?
89
+ STDERR.print "SMQUEUE.DBG: "
90
+ STDERR.puts(block.call)
91
+ end
92
+ end
93
+ end
94
+
95
+ # JMS expiry time in milliseconds from now
96
+ def calc_expiry_time(seconds = 86400 * 7) # one week
97
+ ((Time.now.utc + seconds).to_f * 1000).to_i
98
+ end
99
+
100
+ # resolve a string representing a classname
101
+ def const_resolve(constant)
102
+ constant.to_s.split(/::/).reject{|x| x.empty?}.inject(self) { |prev, this| prev.const_get(this) }
103
+ end
104
+ end
105
+
106
+ class AdapterConfiguration < Doodle
107
+ has :logger, :default => nil
108
+
109
+ # need to use custom to_yaml because YAML won't serialize classes
110
+ def to_hash
111
+ doodle.attributes.inject({}) {|hash, (name, attribute)| hash[name] = send(name); hash}
112
+ end
113
+ def to_yaml(*opts)
114
+ to_hash.to_yaml(*opts)
115
+ end
116
+ def initialize(*args, &block)
117
+ #p [self.class, :initialize, args, caller]
118
+ super
119
+ end
120
+ has :adapter_class, :kind => Class do
121
+ from String, Symbol do |s|
122
+ SMQueue.const_resolve(s.to_s)
123
+ end
124
+ # Note: use closure so this is not evaluated until after NullAdapter class has been defined
125
+ default { NullAdapter }
126
+ end
127
+ has :configuration_class, :kind => Class do
128
+ init { adapter_class::Configuration }
129
+ from String do |s|
130
+ #Doodle::Utils.const_resolve(s)
131
+ SMQueue.const_resolve(s.to_s)
132
+ end
133
+ end
134
+ end
135
+
136
+ class Adapter < Doodle
137
+ has :configuration, :kind => AdapterConfiguration, :abstract => true do
138
+ from Hash do |h|
139
+ #p [:Adapter, :configuration_from_hash]
140
+ Doodle.context.last.class::Configuration.new(h)
141
+ end
142
+ from Object do |h|
143
+ #p [:Adapter, :configuration_from_object, h.inspect, h.class]
144
+ h
145
+ end
146
+ end
147
+ # these are not called anywhere...
148
+ def open(*args, &block)
149
+ end
150
+ def close(*args, &block)
151
+ end
152
+ # these are the core methods
153
+ def get(*args, &block)
154
+ end
155
+ def put(*args, &block)
156
+ end
157
+ def self.create(configuration)
158
+ # FIXME: dup config, otherwise can use it only once - prob. better way to do this
159
+ configuration = configuration.dup
160
+ adapter = configuration.delete(:adapter)
161
+ #p [:adapter, adapter]
162
+ ac = AdapterConfiguration.new(:adapter_class => adapter)
163
+ #p [:ac, ac]
164
+ klass = ac.adapter_class
165
+ #p [:class, klass]
166
+ #puts [:configuration, configuration].pretty_inspect
167
+ # klass.new(:configuration => configuration)
168
+ klass.new(:configuration => configuration)
169
+ end
170
+ end
171
+
172
+ class NullAdapter < Adapter
173
+ class Configuration < AdapterConfiguration
174
+ end
175
+ end
176
+
177
+ class Message < Doodle
178
+ has :headers, :default => { }
179
+ has :body
180
+ end
181
+
182
+ class << self
183
+ def new(*args, &block)
184
+ a = args.first
185
+ if a.kind_of?(Hash) && a.key?(:configuration)
186
+ args = [a[:configuration]]
187
+ end
188
+ Adapter.create(*args, &block)
189
+ end
190
+ end
191
+
192
+ end
193
+ def SMQueue(*args, &block)
194
+ SMQueue.new(*args, &block)
195
+ end
196
+
197
+ # SMQueue.require_all_libs_relative_to(__FILE__)
198
+
199
+ # require adapters relative to invocation path first, then from lib
200
+ [$0, __FILE__].each do |path|
201
+ base_path = File.expand_path(File.dirname(path))
202
+ adapter_path = File.join(base_path, 'smqueue', 'adapters', '*.rb')
203
+ Dir[adapter_path].each do |file|
204
+ begin
205
+ require file
206
+ rescue Object => e
207
+ warn "warning: could not load adapter '#{file}'. Reason: #{e}"
208
+ end
209
+ end
210
+ end
211
+
212
+ if __FILE__ == $0
213
+ yaml = %[
214
+ :adapter: :StompAdapter
215
+ :host: localhost
216
+ :port: 61613
217
+ :name: /topic/smput.test
218
+ :reliable: true
219
+ :reconnect_delay: 5
220
+ :subscription_name: test_stomp
221
+ :client_id: hello_from_stomp_adapter
222
+ :durable: false
223
+ ]
224
+
225
+ adapter = SMQueue(:configuration => YAML.load(yaml))
226
+ adapter.get do |msg|
227
+ puts msg.body
228
+ end
229
+
230
+ end
231
+
@@ -0,0 +1,41 @@
1
+ require 'rubygems'
2
+ require 'smqueue'
3
+ require 'mq'
4
+
5
+ module SMQueue
6
+ class AmqpAdapter < SMQueue::Adapter
7
+ class Configuration < SMQueue::AdapterConfiguration
8
+ has :queue
9
+ end
10
+
11
+ def initialize(*args)
12
+ super
13
+ options = args.first
14
+ @configuration = options[:configuration]
15
+ end
16
+
17
+ def put(*args, &block)
18
+ AMQP.start {
19
+ channel.publish(args[0])
20
+ AMQP.stop { EM.stop }
21
+ }
22
+ end
23
+
24
+ def get(*args, &block)
25
+ if block_given?
26
+ EM.run {
27
+ channel.subscribe { |header, body|
28
+ yield ::SMQueue::Message(:headers => header, :body => body)
29
+ }
30
+ }
31
+ else
32
+ raise "TODO: Implement me"
33
+ end
34
+ end
35
+
36
+ private
37
+ def channel
38
+ MQ.queue(@configuration[:queue])
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,101 @@
1
+ require 'spread'
2
+
3
+ module SMQueue
4
+ class SpreadAdapter < Adapter
5
+ class Configuration < AdapterConfiguration
6
+ has :channel do
7
+ rx_hostname = /[a-z_\.]+/
8
+ rx_ip = /\d+(\.\d+){3}/ # dotted quad
9
+ must 'be a name in the form "port", "port@hostname", or "port@ip"' do |s|
10
+ s =~ /\d+(@(#{rx_hostname})|(#{rx_ip}))?/
11
+ end
12
+ end
13
+ has :group do
14
+ doc "a group name or array of group names"
15
+ must "be either a String group name or an array of group names" do |s|
16
+ s.kind_of?(String) || (s.kind_of?(Array) && s.all?{ |x| x.kind_of?(String)})
17
+ end
18
+ end
19
+ has :private_name, :default => '' do
20
+ doc <<EDOC
21
+ private_name is the name of this connection. It must be unique among
22
+ all the connections to a given Spread daemon. If not specified, Spread
23
+ will assign a randomly-generated unique private name.
24
+ EDOC
25
+ end
26
+ has :all_messages, :default => false do
27
+ doc <<EDOC
28
+ all_messages indicates whether this connection should receive all
29
+ Spread messages, or just data messages.
30
+ EDOC
31
+ end
32
+ has :service_type, :default => Spread::AGREED_MESS do
33
+ service_type_map = {
34
+ :unreliable => Spread::UNRELIABLE_MESS,
35
+ :reliable => Spread::RELIABLE_MESS,
36
+ :fifo => Spread::FIFO_MESS,
37
+ :causal => Spread::CAUSAL_MESS,
38
+ :agreed => Spread::AGREED_MESS,
39
+ :safe => Spread::SAFE_MESS,
40
+ :regular => Spread::REGULAR_MESS,
41
+ }
42
+ from Symbol, String do |s|
43
+ s = s.to_s.to_sym
44
+ if service_type_map.key?(s)
45
+ service_type_map[s]
46
+ else
47
+ raise Doodle::ConversionError, "Did not recognize service_type #{s.inspect} - should be one of #{service_type_map.keys.inspect}"
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ has :connection, :default => nil
54
+ has :connected, :default => false
55
+
56
+ def connect
57
+ @connection = Spread::Connection.new(configuration.channel, configuration.private_name, configuration.all_messages )
58
+ connection.join(configuration.group)
59
+ configuration.private_name = connection.private_group
60
+ connected true
61
+ end
62
+ def disconnect
63
+ connection.leave group
64
+ connection.disconnect
65
+ connected false
66
+ end
67
+ def get(&block)
68
+ m = nil
69
+ connect if !connected
70
+ loop do
71
+ msg = connection.receive
72
+ if msg.data?
73
+ m = SMQueue::Message.new(
74
+ :headers => {
75
+ :private_name => configuration.private_name,
76
+ :sender => msg.sender,
77
+ :type => msg.msg_type,
78
+ :groups => msg.groups,
79
+ :reliable => msg.reliable?,
80
+ :safe => msg.safe?,
81
+ :agreed => msg.agreed?,
82
+ :causal => msg.causal?,
83
+ :fifo => msg.fifo?,
84
+ },
85
+ :body => msg.message
86
+ )
87
+ if block_given?
88
+ yield(m)
89
+ else
90
+ break
91
+ end
92
+ end
93
+ end
94
+ m
95
+ end
96
+ def put(msg)
97
+ connect if !connected
98
+ connection.multicast(msg, configuration.group, configuration.service_type, msg_type = 0, self_discard = true)
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,60 @@
1
+ # set of utility adapters for SMQueue
2
+
3
+ module SMQueue
4
+ class StdioLineAdapter < Adapter
5
+ doc "reads STDIN input, creates new Message for each line of input"
6
+ class Configuration < AdapterConfiguration
7
+ end
8
+ def put(*args, &block)
9
+ STDOUT.puts *args
10
+ end
11
+ def get(*args, &block)
12
+ while input = STDIN.gets
13
+ msg = SMQueue::Message.new(:body => input)
14
+ if block_given?
15
+ yield(msg)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ module SMQueue
23
+ class StdioAdapter < StdioLineAdapter
24
+ doc "reads complete STDIN input, creates one shot Message with :body => input"
25
+ def get(*args, &block)
26
+ input = STDIN.read
27
+ msg = SMQueue::Message.new(:body => input)
28
+ if block_given?
29
+ yield(msg)
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ module SMQueue
36
+ class YamlAdapter < StdioAdapter
37
+ doc "outputs message as YAML"
38
+ def put(*args)
39
+ STDOUT.puts args.to_yaml
40
+ end
41
+ end
42
+ end
43
+
44
+ require 'readline'
45
+ module SMQueue
46
+ class ReadlineAdapter < StdioLineAdapter
47
+ doc "uses readline to read input from prompt, creates new Message for each line of input"
48
+ has :prompt, :default => "> "
49
+ has :history, :default => true
50
+ def get(*args, &block)
51
+ while input = Readline.readline(prompt, history)
52
+ msg = Message.new(:body => input)
53
+ if block_given?
54
+ yield(msg)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ ReadLineAdapter = ReadlineAdapter
60
+ end
@@ -0,0 +1,355 @@
1
+ require 'rstomp'
2
+ module SMQueue
3
+ class StompAdapter < Adapter
4
+ class Configuration < AdapterConfiguration
5
+ has :host, :kind => String, :default => "" do
6
+ doc <<-EDOC
7
+ The host that runs the broker you want to connect to.
8
+ EDOC
9
+ end
10
+ has :port, :kind => Integer, :default => 61613 do
11
+ doc <<-EDOC
12
+ The host that your broker is talking STOMP on.
13
+
14
+ The default port for STOMP is 61613.
15
+ EDOC
16
+ end
17
+ # TODO: document
18
+
19
+ has :secondary_host, :kind => String, :default => ""
20
+ has :secondary_port, :kind => Integer, :default => 61613
21
+ # TODO: Find out how this is used
22
+ has :name, :kind => String, :default => ""
23
+ has :user, :kind => String, :default => "" do
24
+ doc <<-EDOC
25
+ The user to attempt to authenticate at the broker with.
26
+
27
+ If your broker isn't setup for authentication just leave this blank.
28
+ EDOC
29
+ end
30
+ has :password, :kind => String, :default => "" do
31
+ doc <<-EDOC
32
+ The password to attempt to authenticate at the broker with.
33
+
34
+ If your broker isn't setup for authentication just leave this blank.
35
+ EDOC
36
+ end
37
+ # TODO: I think that reliable means the connection will be reconnected
38
+ # after a disconnect. Find out if that's the case and document it.
39
+ has :reliable, :default => false
40
+ # TODO: document this
41
+ # TODO: I think that persistent means that the message will be stored by
42
+ # the broker on a PUT before it sends an ACK.
43
+ has :persistent, :default => true
44
+ has :reconnect_delay, :default => 5 do
45
+ doc <<-EDOC
46
+ How long (in seconds) should we wait between connection attempts?
47
+
48
+ Default: 5 seconds.
49
+ EDOC
50
+ end
51
+ has :client_id, :default => nil, :kind => String do
52
+ doc <<-EDOC
53
+ A string used to identify this script to the broken.
54
+ EDOC
55
+ end
56
+ has :logfile, :default => STDERR do
57
+ doc <<-EDOC
58
+ Where should we log to?
59
+
60
+ Default: STDERR.
61
+ EDOC
62
+ end
63
+ has :logger, :default => nil do
64
+ doc <<-EDOC
65
+ A logger that's to log with. If this is left out of the options a
66
+ new Logger is built that talks to the logfile.
67
+ EDOC
68
+ end
69
+ has :subscription_name, :default => nil do
70
+ doc <<-EDOC
71
+ The subscription to consume from on the broker.
72
+
73
+ This is only used by message consumers. It doesn't make much sense
74
+ for message producers.
75
+ EDOC
76
+ end
77
+ has :home, :default => File.dirname(File.expand_path(__FILE__)) do
78
+ doc <<-EDOC
79
+ A directory to store state in.
80
+
81
+ Defaults to the directory this script is in.
82
+ EDOC
83
+ end
84
+ has :single_delivery, :default => false do
85
+ doc <<-EDOC
86
+ Note: we can't guarantee single delivery - only best effort.
87
+ Use this when receiving a message more than once is very
88
+ costly. However, be aware that you ~will~ sometimes receive
89
+ the same message more than once (so it's your responsibility
90
+ to make sure that you guard against any consequences).
91
+ EDOC
92
+ end
93
+ has :seen_messages_file do
94
+ init { File.join(home, "seen_messages.#{subscription_name}.#{client_id}.yml") }
95
+ end
96
+ has :expires, :default => 86400 * 7 do
97
+ doc <<-EDOC
98
+ Time to live in milliseconds, i.e. a relative offset not an
99
+ absolute time (as it would be in JMS).
100
+
101
+ The default time to live is one week.
102
+ EDOC
103
+ end
104
+ # to get a durable subscription, you must specify
105
+ # :durable => true
106
+ # and a :client_id (and optionally a :subscription_name)
107
+ has :durable, :default => false do
108
+ must "be true or false" do |b|
109
+ [true, false].include?(b)
110
+ end
111
+ doc <<-EDOC
112
+ Specify whether you want a durable or non-durable subscription.
113
+
114
+ Note: durable queues are ~not~ the default as this could be
115
+ v. expensive in disk usage when used thoughtlessly.
116
+ EDOC
117
+ end
118
+ must "specify client_id if durable is true" do
119
+ #pp [:durable_test, client_id, durable, !client_id.to_s.strip.empty?]
120
+ !(client_id.to_s.strip.empty? and durable)
121
+ end
122
+ end
123
+ has :connection, :default => nil
124
+
125
+ # seen_messages is used to skip over messages that have already
126
+ # been seen - only activated when :single_delivery is specified
127
+ has :seen_messages, :init => []
128
+ has :seen_message_count, :init => 10
129
+ has :seen_messages_file do
130
+ init { configuration.seen_messages_file }
131
+ end
132
+
133
+ def initialize(*args, &block)
134
+ super
135
+ restore_remembered_messages
136
+ SMQueue.dbg { [:seen_messages, seen_messages].inspect }
137
+ end
138
+
139
+ # handle an error
140
+ def handle_error(exception_class, error_message, caller)
141
+ #configuration.logger.warn error_message
142
+ raise exception_class, error_message, caller
143
+ end
144
+
145
+ # connect to message broker
146
+ def connect(*args, &block)
147
+ self.connection = RStomp::Connection.open(configuration.to_hash)
148
+ # If the connection has swapped hosts, then swap out primary and secondary
149
+ if connection.current_host != configuration.host
150
+ configuration.secondary_host = configuration.host
151
+ configuration.host = connection.current_host
152
+ end
153
+
154
+ # If the connection has swapped ports, then swap out primary and secondary
155
+ if connection.current_port != configuration.port
156
+ configuration.secondary_port = configuration.port
157
+ configuration.port = connection.current_port
158
+ end
159
+ end
160
+
161
+ # normalize hash keys (top level only)
162
+ # - normalizes keys to strings by default
163
+ # - optionally pass in name of method to use (e.g. :to_sym) to normalize keys
164
+ def normalize_keys(hash, method = :to_s)
165
+ hash = hash.dup
166
+ hash.keys.each do |k|
167
+ normalized_key = k.respond_to?(method) ? k.send(method) : k
168
+ hash[normalized_key] = hash.delete(k)
169
+ end
170
+ hash
171
+ end
172
+
173
+ # true if the message with this message_id has already been seen
174
+ def message_seen?(message_id)
175
+ self.seen_messages.include?(message_id)
176
+ end
177
+
178
+ # remember the message_id
179
+ def message_seen(message_id)
180
+ message_id = message_id.to_s.strip
181
+ if message_id != ""
182
+ self.seen_messages << message_id
183
+ SMQueue.dbg { [:smqueue, :ack, :message_seen, message_id].inspect }
184
+ if self.seen_messages.size > self.seen_message_count
185
+ self.seen_messages.shift
186
+ end
187
+ store_remembered_messages
188
+ else
189
+ SMQueue.dbg { [:smqueue, :ack, :message_seen, message_id].inspect }
190
+ end
191
+ end
192
+
193
+ # store the remembered message ids in a yaml file
194
+ def store_remembered_messages
195
+ if configuration.single_delivery
196
+ File.open(seen_messages_file, 'w') do |file|
197
+ file.write seen_messages.to_yaml
198
+ end
199
+ end
200
+ end
201
+
202
+ # reload remembered message ids from a yaml file
203
+ def restore_remembered_messages
204
+ if configuration.single_delivery
205
+ yaml = default_yaml = "--- []"
206
+ begin
207
+ File.open(seen_messages_file, 'r') do |file|
208
+ yaml = file.read
209
+ end
210
+ rescue Object
211
+ yaml = default_yaml
212
+ end
213
+ buffer = []
214
+ begin
215
+ buffer = YAML.load(yaml)
216
+ if !buffer.kind_of?(Array) or !buffer.all?{ |x| x.kind_of?(String)}
217
+ raise Exception, "Invalid seen_messages.yml file"
218
+ end
219
+ rescue Object
220
+ buffer = []
221
+ end
222
+ self.seen_messages = buffer
223
+ end
224
+ end
225
+
226
+ # acknowledge message (if headers["ack"] == "client")
227
+ def ack(subscription_headers, message)
228
+ #p [:ack, message.headers["message-id"]]
229
+ if message.headers["message-id"].to_s.strip != "" && subscription_headers["ack"].to_s == "client"
230
+ SMQueue.dbg { [:smqueue, :ack, :message, message].inspect }
231
+ connection.ack message.headers["message-id"], { }
232
+ else
233
+ SMQueue.dbg { [:smqueue, :ack, :not_acknowledging, message].inspect }
234
+ end
235
+ if ENV['PAUSE_SMQUEUE']
236
+ $stderr.print "press enter to continue> "
237
+ $stderr.flush
238
+ $stdin.gets
239
+ end
240
+ end
241
+
242
+ # get message from queue
243
+ # - if block supplied, loop forever and yield(message) for each
244
+ # message received
245
+ # default headers are:
246
+ # :ack => "client"
247
+ # :client_id => configuration.client_id
248
+ # :subscription_name => configuration.subscription_name
249
+ #
250
+ def get(headers = {}, &block)
251
+ self.connect
252
+ SMQueue.dbg { [:smqueue, :get, headers].inspect }
253
+ subscription_headers = {"ack" => "client", "activemq.prefetchSize" => 1 }
254
+ if client_id = configuration.client_id
255
+ subscription_headers["client_id"] = client_id
256
+ end
257
+ if sub_name = configuration.subscription_name
258
+ subscription_headers["subscription_name"] = sub_name
259
+ end
260
+ # if a client_id is supplied, then user wants a durable subscription
261
+ # N.B. client_id must be unique for broker
262
+ subscription_headers.update(headers)
263
+ #p [:subscription_headers_before, subscription_headers]
264
+ subscription_headers = normalize_keys(subscription_headers)
265
+ if configuration.durable and client_id = configuration.client_id || subscription_headers["client_id"]
266
+ subscription_name = configuration.subscription_name || subscription_headers["subscription_name"] || client_id
267
+ # activemq only
268
+ subscription_headers["activemq.subscriptionName"] = subscription_name
269
+ # JMS
270
+ subscription_headers["durable-subscriber-name"] = subscription_name
271
+ end
272
+ #p [:subscription_headers_after, subscription_headers]
273
+
274
+ destination = configuration.name
275
+ SMQueue.dbg { [:smqueue, :get, :subscribing, destination, :subscription_headers, subscription_headers].inspect }
276
+ connection.subscribe destination, subscription_headers
277
+ message = nil
278
+ SMQueue.dbg { [:smqueue, :get, :subscription_headers, subscription_headers].inspect }
279
+ begin
280
+ # TODO: refactor this
281
+ if block_given?
282
+ SMQueue.dbg { [:smqueue, :get, :block_given].inspect }
283
+ # todo: change to @running - (and set to false from exception handler)
284
+ # also should check to see if anything left to receive on connection before bailing out
285
+ while true
286
+ SMQueue.dbg { [:smqueue, :get, :receive].inspect }
287
+ # block until message ready
288
+ message = connection.receive
289
+ SMQueue.dbg { [:smqueue, :get, :received, message].inspect }
290
+ case message.command
291
+ when "ERROR"
292
+ SMQueue.dbg { [:smqueue, :get, :ERROR, message].inspect }
293
+ when "RECEIPT"
294
+ SMQueue.dbg { [:smqueue, :get, :RECEIPT, message].inspect }
295
+ else
296
+ SMQueue.dbg { [:smqueue, :get, :yielding].inspect }
297
+ if !message_seen?(message.headers["message-id"])
298
+ yield(message)
299
+ end
300
+ SMQueue.dbg { [:smqueue, :get, :message_seen, message.headers["message-id"]].inspect }
301
+ message_seen message.headers["message-id"]
302
+ SMQueue.dbg { [:smqueue, :get, :returned_from_yield_now_calling_ack].inspect }
303
+ ack(subscription_headers, message)
304
+ SMQueue.dbg { [:smqueue, :get, :returned_from_ack].inspect }
305
+ end
306
+ end
307
+ else
308
+ SMQueue.dbg { [:smqueue, :get, :single_shot].inspect }
309
+ message = connection.receive
310
+ SMQueue.dbg { [:smqueue, :get, :received, message].inspect }
311
+ if !(message.command == "ERROR" or message.command == "RECEIPT")
312
+ SMQueue.dbg { [:smqueue, :get, :message_seen, message.headers["message-id"]].inspect }
313
+ message_seen message.headers["message-id"]
314
+ SMQueue.dbg { [:smqueue, :get, :ack, message].inspect }
315
+ ack(subscription_headers, message)
316
+ SMQueue.dbg { [:smqueue, :get, :returned_from_ack].inspect }
317
+ end
318
+ end
319
+ rescue Object => e
320
+ SMQueue.dbg { [:smqueue, :get, :exception, e].inspect }
321
+ handle_error e, "Exception in SMQueue#get: #{e.message}", caller
322
+ ensure
323
+ SMQueue.dbg { [:smqueue, :get, :ensure].inspect }
324
+ SMQueue.dbg { [:smqueue, :unsubscribe, destination, subscription_headers].inspect }
325
+ connection.unsubscribe destination, subscription_headers
326
+ SMQueue.dbg { [:smqueue, :disconnect].inspect }
327
+ connection.disconnect
328
+ end
329
+ SMQueue.dbg { [:smqueue, :get, :return].inspect }
330
+ message
331
+ end
332
+
333
+ # put a message on the queue
334
+ # default headers are:
335
+ # :persistent => true
336
+ # :ack => "auto"
337
+ # :expires => configuration.expires
338
+ def put(body, headers = { })
339
+ SMQueue.dbg { [:smqueue, :put, body, headers].inspect }
340
+ begin
341
+ self.connect
342
+ headers = {:persistent => true, :ack => "auto", :expires => SMQueue.calc_expiry_time(configuration.expires) }.merge(headers)
343
+ destination = configuration.name
344
+ SMQueue.dbg { [:smqueue, :send, body, headers].inspect }
345
+ connection.send destination, body, headers
346
+ rescue Exception => e
347
+ SMQueue.dbg { [:smqueue, :exception, e].inspect }
348
+ handle_error e, "Exception in SMQueue#put - #{e.message}", caller
349
+ #connection.disconnect
350
+ ensure
351
+ connection.disconnect
352
+ end
353
+ end
354
+ end
355
+ end