thechrisoshow-smqueue 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +4 -0
- data/Manifest.txt +9 -0
- data/README.txt +72 -0
- data/Rakefile +26 -0
- data/lib/rstomp.rb +582 -0
- data/lib/smqueue.rb +227 -0
- data/lib/smqueue/adapters/spread.rb +101 -0
- data/lib/smqueue/adapters/stdio.rb +59 -0
- data/lib/smqueue/adapters/stomp.rb +291 -0
- data/smqueue.gemspec +24 -0
- data/test/test_rstomp_connection.rb +56 -0
- metadata +70 -0
data/lib/smqueue.rb
ADDED
@@ -0,0 +1,227 @@
|
|
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.1.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
|
+
require file
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
if __FILE__ == $0
|
209
|
+
yaml = %[
|
210
|
+
:adapter: :StompAdapter
|
211
|
+
:host: localhost
|
212
|
+
:port: 61613
|
213
|
+
:name: /topic/smput.test
|
214
|
+
:reliable: true
|
215
|
+
:reconnect_delay: 5
|
216
|
+
:subscription_name: test_stomp
|
217
|
+
:client_id: hello_from_stomp_adapter
|
218
|
+
:durable: false
|
219
|
+
]
|
220
|
+
|
221
|
+
adapter = SMQueue(:configuration => YAML.load(yaml))
|
222
|
+
adapter.get do |msg|
|
223
|
+
puts msg.body
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
227
|
+
|
@@ -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,59 @@
|
|
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
|
+
end
|
@@ -0,0 +1,291 @@
|
|
1
|
+
require 'rstomp'
|
2
|
+
module SMQueue
|
3
|
+
class StompAdapter < Adapter
|
4
|
+
class Configuration < AdapterConfiguration
|
5
|
+
has :host, :kind => String, :default => ""
|
6
|
+
has :port, :kind => Integer, :default => 61613 # Stomp
|
7
|
+
has :secondary_host, :kind => String, :default => ""
|
8
|
+
has :secondary_port, :kind => Integer, :default => 61613
|
9
|
+
has :name, :kind => String, :default => ""
|
10
|
+
has :user, :kind => String, :default => ""
|
11
|
+
has :password, :kind => String, :default => ""
|
12
|
+
has :reliable, :default => false
|
13
|
+
has :persistent, :default => true
|
14
|
+
has :reconnect_delay, :default => 5
|
15
|
+
has :client_id, :default => nil, :kind => String
|
16
|
+
has :logfile, :default => STDERR
|
17
|
+
has :logger, :default => nil
|
18
|
+
has :subscription_name, :default => nil
|
19
|
+
has :home, :default => File.dirname(File.expand_path(__FILE__))
|
20
|
+
has :single_delivery, :default => false do
|
21
|
+
doc <<-EDOC
|
22
|
+
Note: we can't guarantee single delivery - only best effort.
|
23
|
+
Use this when receiving a message more than once is very
|
24
|
+
costly. However, be aware that you ~will~ sometimes receive
|
25
|
+
the same message more than once (so it's your responsibility
|
26
|
+
to make sure that you guard against any consequences).
|
27
|
+
EDOC
|
28
|
+
end
|
29
|
+
has :seen_messages_file do
|
30
|
+
init { File.join(home, "seen_messages.#{subscription_name}.#{client_id}.yml") }
|
31
|
+
end
|
32
|
+
has :expires, :default => 86400 * 7 do
|
33
|
+
doc <<-EDOC
|
34
|
+
Time to live in milliseconds, i.e. a relative offset not an
|
35
|
+
absolute time (as it would be in JMS).
|
36
|
+
|
37
|
+
The default time to live is one week.
|
38
|
+
EDOC
|
39
|
+
end
|
40
|
+
# to get a durable subscription, you must specify
|
41
|
+
# :durable => true
|
42
|
+
# and a :client_id (and optionally a :subscription_name)
|
43
|
+
has :durable, :default => false do
|
44
|
+
must "be true or false" do |b|
|
45
|
+
[true, false].include?(b)
|
46
|
+
end
|
47
|
+
doc <<-EDOC
|
48
|
+
Specify whether you want a durable or non-durable subscription.
|
49
|
+
|
50
|
+
Note: durable queues are ~not~ the default as this could be
|
51
|
+
v. expensive in disk usage when used thoughtlessly.
|
52
|
+
EDOC
|
53
|
+
end
|
54
|
+
must "specify client_id if durable is true" do
|
55
|
+
#pp [:durable_test, client_id, durable, !client_id.to_s.strip.empty?]
|
56
|
+
!(client_id.to_s.strip.empty? and durable)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
has :connection, :default => nil
|
60
|
+
|
61
|
+
# seen_messages is used to skip over messages that have already
|
62
|
+
# been seen - only activated when :single_delivery is specified
|
63
|
+
has :seen_messages, :init => []
|
64
|
+
has :seen_message_count, :init => 10
|
65
|
+
has :seen_messages_file do
|
66
|
+
init { configuration.seen_messages_file }
|
67
|
+
end
|
68
|
+
|
69
|
+
def initialize(*args, &block)
|
70
|
+
super
|
71
|
+
restore_remembered_messages
|
72
|
+
SMQueue.dbg { [:seen_messages, seen_messages].inspect }
|
73
|
+
end
|
74
|
+
|
75
|
+
# handle an error
|
76
|
+
def handle_error(exception_class, error_message, caller)
|
77
|
+
#configuration.logger.warn error_message
|
78
|
+
raise exception_class, error_message, caller
|
79
|
+
end
|
80
|
+
|
81
|
+
# connect to message broker
|
82
|
+
def connect(*args, &block)
|
83
|
+
self.connection = RStomp::Connection.open(configuration.to_hash)
|
84
|
+
# If the connection has swapped hosts, then swap out primary and secondary
|
85
|
+
if connection.current_host != configuration.host
|
86
|
+
configuration.secondary_host = configuration.host
|
87
|
+
configuration.host = connection.current_host
|
88
|
+
end
|
89
|
+
|
90
|
+
# If the connection has swapped ports, then swap out primary and secondary
|
91
|
+
if connection.current_port != configuration.port
|
92
|
+
configuration.secondary_port = configuration.port
|
93
|
+
configuration.port = connection.current_port
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# normalize hash keys (top level only)
|
98
|
+
# - normalizes keys to strings by default
|
99
|
+
# - optionally pass in name of method to use (e.g. :to_sym) to normalize keys
|
100
|
+
def normalize_keys(hash, method = :to_s)
|
101
|
+
hash = hash.dup
|
102
|
+
hash.keys.each do |k|
|
103
|
+
normalized_key = k.respond_to?(method) ? k.send(method) : k
|
104
|
+
hash[normalized_key] = hash.delete(k)
|
105
|
+
end
|
106
|
+
hash
|
107
|
+
end
|
108
|
+
|
109
|
+
# true if the message with this message_id has already been seen
|
110
|
+
def message_seen?(message_id)
|
111
|
+
self.seen_messages.include?(message_id)
|
112
|
+
end
|
113
|
+
|
114
|
+
# remember the message_id
|
115
|
+
def message_seen(message_id)
|
116
|
+
message_id = message_id.to_s.strip
|
117
|
+
if message_id != ""
|
118
|
+
self.seen_messages << message_id
|
119
|
+
SMQueue.dbg { [:smqueue, :ack, :message_seen, message_id].inspect }
|
120
|
+
if self.seen_messages.size > self.seen_message_count
|
121
|
+
self.seen_messages.shift
|
122
|
+
end
|
123
|
+
store_remembered_messages
|
124
|
+
else
|
125
|
+
SMQueue.dbg { [:smqueue, :ack, :message_seen, message_id].inspect }
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# store the remembered message ids in a yaml file
|
130
|
+
def store_remembered_messages
|
131
|
+
if configuration.single_delivery
|
132
|
+
File.open(seen_messages_file, 'w') do |file|
|
133
|
+
file.write seen_messages.to_yaml
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# reload remembered message ids from a yaml file
|
139
|
+
def restore_remembered_messages
|
140
|
+
if configuration.single_delivery
|
141
|
+
yaml = default_yaml = "--- []"
|
142
|
+
begin
|
143
|
+
File.open(seen_messages_file, 'r') do |file|
|
144
|
+
yaml = file.read
|
145
|
+
end
|
146
|
+
rescue Object
|
147
|
+
yaml = default_yaml
|
148
|
+
end
|
149
|
+
buffer = []
|
150
|
+
begin
|
151
|
+
buffer = YAML.load(yaml)
|
152
|
+
if !buffer.kind_of?(Array) or !buffer.all?{ |x| x.kind_of?(String)}
|
153
|
+
raise Exception, "Invalid seen_messages.yml file"
|
154
|
+
end
|
155
|
+
rescue Object
|
156
|
+
buffer = []
|
157
|
+
end
|
158
|
+
self.seen_messages = buffer
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# acknowledge message (if headers["ack"] == "client")
|
163
|
+
def ack(subscription_headers, message)
|
164
|
+
#p [:ack, message.headers["message-id"]]
|
165
|
+
if message.headers["message-id"].to_s.strip != "" && subscription_headers["ack"].to_s == "client"
|
166
|
+
SMQueue.dbg { [:smqueue, :ack, :message, message].inspect }
|
167
|
+
connection.ack message.headers["message-id"], { }
|
168
|
+
else
|
169
|
+
SMQueue.dbg { [:smqueue, :ack, :not_acknowledging, message].inspect }
|
170
|
+
end
|
171
|
+
if ENV['PAUSE_SMQUEUE']
|
172
|
+
$stderr.print "press enter to continue> "
|
173
|
+
$stderr.flush
|
174
|
+
$stdin.gets
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# get message from queue
|
179
|
+
# - if block supplied, loop forever and yield(message) for each
|
180
|
+
# message received
|
181
|
+
# default headers are:
|
182
|
+
# :ack => "client"
|
183
|
+
# :client_id => configuration.client_id
|
184
|
+
# :subscription_name => configuration.subscription_name
|
185
|
+
#
|
186
|
+
def get(headers = {}, &block)
|
187
|
+
self.connect
|
188
|
+
SMQueue.dbg { [:smqueue, :get, headers].inspect }
|
189
|
+
subscription_headers = {"ack" => "client", "activemq.prefetchSize" => 1 }
|
190
|
+
if client_id = configuration.client_id
|
191
|
+
subscription_headers["client_id"] = client_id
|
192
|
+
end
|
193
|
+
if sub_name = configuration.subscription_name
|
194
|
+
subscription_headers["subscription_name"] = sub_name
|
195
|
+
end
|
196
|
+
# if a client_id is supplied, then user wants a durable subscription
|
197
|
+
# N.B. client_id must be unique for broker
|
198
|
+
subscription_headers.update(headers)
|
199
|
+
#p [:subscription_headers_before, subscription_headers]
|
200
|
+
subscription_headers = normalize_keys(subscription_headers)
|
201
|
+
if configuration.durable and client_id = configuration.client_id || subscription_headers["client_id"]
|
202
|
+
subscription_name = configuration.subscription_name || subscription_headers["subscription_name"] || client_id
|
203
|
+
# activemq only
|
204
|
+
subscription_headers["activemq.subscriptionName"] = subscription_name
|
205
|
+
# JMS
|
206
|
+
subscription_headers["durable-subscriber-name"] = subscription_name
|
207
|
+
end
|
208
|
+
#p [:subscription_headers_after, subscription_headers]
|
209
|
+
|
210
|
+
destination = configuration.name
|
211
|
+
SMQueue.dbg { [:smqueue, :get, :subscribing, destination, :subscription_headers, subscription_headers].inspect }
|
212
|
+
connection.subscribe destination, subscription_headers
|
213
|
+
message = nil
|
214
|
+
SMQueue.dbg { [:smqueue, :get, :subscription_headers, subscription_headers].inspect }
|
215
|
+
begin
|
216
|
+
# TODO: refactor this
|
217
|
+
if block_given?
|
218
|
+
SMQueue.dbg { [:smqueue, :get, :block_given].inspect }
|
219
|
+
# todo: change to @running - (and set to false from exception handler)
|
220
|
+
# also should check to see if anything left to receive on connection before bailing out
|
221
|
+
while true
|
222
|
+
SMQueue.dbg { [:smqueue, :get, :receive].inspect }
|
223
|
+
# block until message ready
|
224
|
+
message = connection.receive
|
225
|
+
SMQueue.dbg { [:smqueue, :get, :received, message].inspect }
|
226
|
+
case message.command
|
227
|
+
when "ERROR"
|
228
|
+
SMQueue.dbg { [:smqueue, :get, :ERROR, message].inspect }
|
229
|
+
when "RECEIPT"
|
230
|
+
SMQueue.dbg { [:smqueue, :get, :RECEIPT, message].inspect }
|
231
|
+
else
|
232
|
+
SMQueue.dbg { [:smqueue, :get, :yielding].inspect }
|
233
|
+
if !message_seen?(message.headers["message-id"])
|
234
|
+
yield(message)
|
235
|
+
end
|
236
|
+
SMQueue.dbg { [:smqueue, :get, :message_seen, message.headers["message-id"]].inspect }
|
237
|
+
message_seen message.headers["message-id"]
|
238
|
+
SMQueue.dbg { [:smqueue, :get, :returned_from_yield_now_calling_ack].inspect }
|
239
|
+
ack(subscription_headers, message)
|
240
|
+
SMQueue.dbg { [:smqueue, :get, :returned_from_ack].inspect }
|
241
|
+
end
|
242
|
+
end
|
243
|
+
else
|
244
|
+
SMQueue.dbg { [:smqueue, :get, :single_shot].inspect }
|
245
|
+
message = connection.receive
|
246
|
+
SMQueue.dbg { [:smqueue, :get, :received, message].inspect }
|
247
|
+
if !(message.command == "ERROR" or message.command == "RECEIPT")
|
248
|
+
SMQueue.dbg { [:smqueue, :get, :message_seen, message.headers["message-id"]].inspect }
|
249
|
+
message_seen message.headers["message-id"]
|
250
|
+
SMQueue.dbg { [:smqueue, :get, :ack, message].inspect }
|
251
|
+
ack(subscription_headers, message)
|
252
|
+
SMQueue.dbg { [:smqueue, :get, :returned_from_ack].inspect }
|
253
|
+
end
|
254
|
+
end
|
255
|
+
rescue Object => e
|
256
|
+
SMQueue.dbg { [:smqueue, :get, :exception, e].inspect }
|
257
|
+
handle_error e, "Exception in SMQueue#get: #{e.message}", caller
|
258
|
+
ensure
|
259
|
+
SMQueue.dbg { [:smqueue, :get, :ensure].inspect }
|
260
|
+
SMQueue.dbg { [:smqueue, :unsubscribe, destination, subscription_headers].inspect }
|
261
|
+
connection.unsubscribe destination, subscription_headers
|
262
|
+
SMQueue.dbg { [:smqueue, :disconnect].inspect }
|
263
|
+
connection.disconnect
|
264
|
+
end
|
265
|
+
SMQueue.dbg { [:smqueue, :get, :return].inspect }
|
266
|
+
message
|
267
|
+
end
|
268
|
+
|
269
|
+
# put a message on the queue
|
270
|
+
# default headers are:
|
271
|
+
# :persistent => true
|
272
|
+
# :ack => "auto"
|
273
|
+
# :expires => configuration.expires
|
274
|
+
def put(body, headers = { })
|
275
|
+
SMQueue.dbg { [:smqueue, :put, body, headers].inspect }
|
276
|
+
begin
|
277
|
+
self.connect
|
278
|
+
headers = {:persistent => true, :ack => "auto", :expires => SMQueue.calc_expiry_time(configuration.expires) }.merge(headers)
|
279
|
+
destination = configuration.name
|
280
|
+
SMQueue.dbg { [:smqueue, :send, body, headers].inspect }
|
281
|
+
connection.send destination, body, headers
|
282
|
+
rescue Exception => e
|
283
|
+
SMQueue.dbg { [:smqueue, :exception, e].inspect }
|
284
|
+
handle_error e, "Exception in SMQueue#put - #{e.message}", caller
|
285
|
+
#connection.disconnect
|
286
|
+
ensure
|
287
|
+
connection.disconnect
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|