seanohalpin-smqueue 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Manifest.txt +15 -0
- data/README.txt +72 -0
- data/Rakefile +26 -0
- data/examples/input.rb +14 -0
- data/examples/message_queue.yml +29 -0
- data/examples/output.rb +11 -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 +31 -0
- data/test/helper.rb +23 -0
- data/test/test_rstomp_connection.rb +56 -0
- metadata +77 -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
|