activemessaging 0.6.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/Rakefile +50 -0
- data/generators/a13g_test_harness/a13g_test_harness_generator.rb +19 -0
- data/generators/a13g_test_harness/templates/active_messaging_test.rhtml +13 -0
- data/generators/a13g_test_harness/templates/active_messaging_test_controller.rb +29 -0
- data/generators/a13g_test_harness/templates/index.rhtml +17 -0
- data/generators/filter/USAGE +0 -0
- data/generators/filter/filter_generator.rb +19 -0
- data/generators/filter/templates/filter.rb +12 -0
- data/generators/filter/templates/filter_test.rb +28 -0
- data/generators/processor/USAGE +8 -0
- data/generators/processor/processor_generator.rb +31 -0
- data/generators/processor/templates/application.rb +18 -0
- data/generators/processor/templates/broker.yml +79 -0
- data/generators/processor/templates/jruby_poller +117 -0
- data/generators/processor/templates/messaging.rb +12 -0
- data/generators/processor/templates/poller +23 -0
- data/generators/processor/templates/poller.rb +23 -0
- data/generators/processor/templates/processor.rb +8 -0
- data/generators/processor/templates/processor_test.rb +20 -0
- data/generators/tracer/USAGE +8 -0
- data/generators/tracer/templates/controller.rb +14 -0
- data/generators/tracer/templates/helper.rb +2 -0
- data/generators/tracer/templates/index.rhtml +4 -0
- data/generators/tracer/templates/layout.rhtml +16 -0
- data/generators/tracer/templates/trace_processor.rb +100 -0
- data/generators/tracer/tracer_generator.rb +25 -0
- data/lib/activemessaging.rb +133 -0
- data/lib/activemessaging/adapter.rb +21 -0
- data/lib/activemessaging/adapters/asqs.rb +412 -0
- data/lib/activemessaging/adapters/base.rb +82 -0
- data/lib/activemessaging/adapters/jms.rb +237 -0
- data/lib/activemessaging/adapters/reliable_msg.rb +190 -0
- data/lib/activemessaging/adapters/stomp.rb +99 -0
- data/lib/activemessaging/adapters/test.rb +155 -0
- data/lib/activemessaging/adapters/wmq.rb +202 -0
- data/lib/activemessaging/filter.rb +29 -0
- data/lib/activemessaging/gateway.rb +422 -0
- data/lib/activemessaging/message_sender.rb +30 -0
- data/lib/activemessaging/named_base.rb +54 -0
- data/lib/activemessaging/processor.rb +45 -0
- data/lib/activemessaging/support.rb +17 -0
- data/lib/activemessaging/test_helper.rb +194 -0
- data/lib/activemessaging/trace_filter.rb +34 -0
- data/messaging.rb.example +5 -0
- data/tasks/start_consumers.rake +8 -0
- metadata +123 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
# 'abstract' base class for ActiveMessaging filter classes
|
2
|
+
module ActiveMessaging
|
3
|
+
class Filter
|
4
|
+
|
5
|
+
#automatically make it so filters are message senders
|
6
|
+
include MessageSender
|
7
|
+
|
8
|
+
# give filters easy access to the logger
|
9
|
+
def logger()
|
10
|
+
@@logger = ActiveMessaging.logger unless defined?(@@logger)
|
11
|
+
@@logger
|
12
|
+
end
|
13
|
+
|
14
|
+
# these are the headers available for a message from the 'details' hash
|
15
|
+
# :receiver=>processor
|
16
|
+
# :destination=>destination object
|
17
|
+
# :direction => :incoming
|
18
|
+
|
19
|
+
# :publisher => publisher - optional
|
20
|
+
# :destination => destination object
|
21
|
+
# :direction => :outgoing
|
22
|
+
|
23
|
+
# if you raise a StopProcessingException, it will cause this to be the last filter to be processed, and will prevent any further processing
|
24
|
+
def process(message, routing)
|
25
|
+
raise NotImplementedError.new("Implement the process method in your own filter class that extends ActiveMessaging::Filter")
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,422 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
module ActiveMessaging
|
6
|
+
|
7
|
+
class Gateway
|
8
|
+
cattr_accessor :adapters, :subscriptions, :named_destinations, :filters, :processor_groups, :connections
|
9
|
+
@@adapters = {}
|
10
|
+
@@subscriptions = {}
|
11
|
+
@@named_destinations = {}
|
12
|
+
@@filters = []
|
13
|
+
@@connections = {}
|
14
|
+
@@processor_groups = {}
|
15
|
+
@@current_processor_group = nil
|
16
|
+
|
17
|
+
# these are used to manage the running connection threads
|
18
|
+
@@running = true
|
19
|
+
@@connection_threads = {}
|
20
|
+
@@guard = Mutex.new
|
21
|
+
|
22
|
+
class <<self
|
23
|
+
|
24
|
+
# Starts up an message listener to start polling for messages on each configured connection, and dispatching processing
|
25
|
+
def start
|
26
|
+
|
27
|
+
# subscribe - creating connections along the way
|
28
|
+
subscribe
|
29
|
+
|
30
|
+
# for each connection, start a thread
|
31
|
+
@@connections.each do |name, conn|
|
32
|
+
@@connection_threads[name] = Thread.start do
|
33
|
+
while @@running
|
34
|
+
begin
|
35
|
+
Thread.current[:message] = nil
|
36
|
+
Thread.current[:message] = conn.receive
|
37
|
+
#catch these but then stop looping
|
38
|
+
rescue StopProcessingException=>spe
|
39
|
+
ActiveMessaging.logger.error "ActiveMessaging: thread[#{name}]: Processing Stopped - receive interrupted, will process last message if already received"
|
40
|
+
# break
|
41
|
+
#catch all others, but go back and try and recieve again
|
42
|
+
rescue Object=>exception
|
43
|
+
ActiveMessaging.logger.error "ActiveMessaging: thread[#{name}]: Exception from connection.receive: #{exception.message}\n" + exception.backtrace.join("\n\t")
|
44
|
+
ensure
|
45
|
+
dispatch Thread.current[:message] if Thread.current[:message]
|
46
|
+
Thread.current[:message] = nil
|
47
|
+
end
|
48
|
+
Thread.pass
|
49
|
+
end
|
50
|
+
ActiveMessaging.logger.error "ActiveMessaging: thread[#{name}]: receive loop terminated"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
while @@running
|
55
|
+
trap("TERM", "EXIT")
|
56
|
+
living = false
|
57
|
+
@@connection_threads.each { |name, thread| living ||= thread.alive? }
|
58
|
+
@@running = living
|
59
|
+
sleep 1
|
60
|
+
end
|
61
|
+
ActiveMessaging.logger.error "All connection threads have died..."
|
62
|
+
rescue Interrupt
|
63
|
+
ActiveMessaging.logger.error "\n<<Interrupt received>>\n"
|
64
|
+
rescue Object=>exception
|
65
|
+
ActiveMessaging.logger.error "#{exception.class.name}: #{exception.message}\n\t#{exception.backtrace.join("\n\t")}"
|
66
|
+
raise exception
|
67
|
+
ensure
|
68
|
+
ActiveMessaging.logger.error "Cleaning up..."
|
69
|
+
stop
|
70
|
+
ActiveMessaging.logger.error "=> END"
|
71
|
+
end
|
72
|
+
|
73
|
+
def stop
|
74
|
+
# first tell the threads to stop their looping, so they'll stop when next complete a receive/dispatch cycle
|
75
|
+
@@running = false
|
76
|
+
|
77
|
+
# if they are dispatching (i.e. !thread[:message].nil?), wait for them to finish
|
78
|
+
# if they are receiving (i.e. thread[:message].nil?), stop them by raising exception
|
79
|
+
dispatching = true
|
80
|
+
while dispatching
|
81
|
+
dispatching = false
|
82
|
+
@@connection_threads.each do |name, thread|
|
83
|
+
if thread[:message]
|
84
|
+
dispatching = true
|
85
|
+
# if thread got killed, but dispatch not done, try it again
|
86
|
+
if thread.alive?
|
87
|
+
ActiveMessaging.logger.error "Waiting on thread #{name} to finish processing last message..."
|
88
|
+
else
|
89
|
+
ActiveMessaging.logger.error "Starting thread #{name} to finish processing last message..."
|
90
|
+
msg = thread[:message]
|
91
|
+
thread.exit
|
92
|
+
thread = Thread.start do
|
93
|
+
begin
|
94
|
+
Thread.current[:message] = msg
|
95
|
+
dispatch Thread.current[:message]
|
96
|
+
ensure
|
97
|
+
Thread.current[:message] = nil
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
else
|
102
|
+
thread.raise StopProcessingException, "Time to stop." if thread.alive?
|
103
|
+
end
|
104
|
+
end
|
105
|
+
sleep(1)
|
106
|
+
end
|
107
|
+
unsubscribe
|
108
|
+
disconnect
|
109
|
+
end
|
110
|
+
|
111
|
+
def connection broker_name='default'
|
112
|
+
return @@connections[broker_name] if @@connections.has_key?(broker_name)
|
113
|
+
config = load_connection_configuration(broker_name)
|
114
|
+
@@connections[broker_name] = Gateway.adapters[config[:adapter]].new(config)
|
115
|
+
end
|
116
|
+
|
117
|
+
def register_adapter adapter_name, adapter_class
|
118
|
+
adapters[adapter_name] = adapter_class
|
119
|
+
end
|
120
|
+
|
121
|
+
def filter filter, options = {}
|
122
|
+
options[:direction] = :bidirectional if options[:direction].nil?
|
123
|
+
filters << [filter, options]
|
124
|
+
end
|
125
|
+
|
126
|
+
def subscribe
|
127
|
+
subscriptions.each { |key, subscription| subscription.subscribe }
|
128
|
+
end
|
129
|
+
|
130
|
+
def unsubscribe
|
131
|
+
subscriptions.each { |key, subscription| subscription.unsubscribe }
|
132
|
+
end
|
133
|
+
|
134
|
+
def disconnect
|
135
|
+
@@connections.each { |key,connection| connection.disconnect }
|
136
|
+
@@connections = {}
|
137
|
+
end
|
138
|
+
|
139
|
+
def execute_filter_chain(direction, message, details={})
|
140
|
+
filters.each do |filter, options|
|
141
|
+
if apply_filter?(direction, details, options)
|
142
|
+
begin
|
143
|
+
filter_obj = create_filter(filter, options)
|
144
|
+
filter_obj.process(message, details)
|
145
|
+
rescue ActiveMessaging::StopFilterException => sfe
|
146
|
+
ActiveMessaging.logger.error "Filter: #{filter_obj.inspect} threw StopFilterException: #{sfe.message}"
|
147
|
+
return
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
yield(message)
|
152
|
+
end
|
153
|
+
|
154
|
+
def apply_filter?(direction, details, options)
|
155
|
+
# check that it is the correct direction
|
156
|
+
result = if direction.to_sym == options[:direction] || options[:direction] == :bidirectional
|
157
|
+
if options.has_key?(:only) && [options[:only]].flatten.include?(details[:destination].name)
|
158
|
+
true
|
159
|
+
elsif options.has_key?(:except) && ![options[:except]].flatten.include?(details[:destination].name)
|
160
|
+
true
|
161
|
+
elsif !options.has_key?(:only) && !options.has_key?(:except)
|
162
|
+
true
|
163
|
+
end
|
164
|
+
end
|
165
|
+
result
|
166
|
+
end
|
167
|
+
|
168
|
+
def create_filter(filter, options)
|
169
|
+
filter_class = if filter.is_a?(String) or filter.is_a?(Symbol)
|
170
|
+
filter.to_s.camelize.constantize
|
171
|
+
elsif filter.is_a?(Class)
|
172
|
+
filter
|
173
|
+
end
|
174
|
+
|
175
|
+
if filter_class
|
176
|
+
if filter_class.respond_to?(:process) && (filter_class.method(:process).arity.abs > 0)
|
177
|
+
filter_class
|
178
|
+
elsif filter_class.instance_method(:initialize).arity.abs == 1
|
179
|
+
filter_class.new(options)
|
180
|
+
elsif filter_class.instance_method(:initialize).arity == 0
|
181
|
+
filter_class.new
|
182
|
+
else
|
183
|
+
raise "Filter #{filter} could not be created, no 'initialize' matched."
|
184
|
+
end
|
185
|
+
else
|
186
|
+
raise "Filter #{filter} could not be loaded, created, or used!"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def prepare_application
|
191
|
+
if defined? Rails
|
192
|
+
Dispatcher.prepare_application_for_dispatch
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def reset_application
|
197
|
+
if defined? Rails
|
198
|
+
Dispatcher.reset_application_after_dispatch
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def dispatch(message)
|
203
|
+
@@guard.synchronize {
|
204
|
+
begin
|
205
|
+
prepare_application
|
206
|
+
_dispatch(message)
|
207
|
+
rescue Object => exc
|
208
|
+
ActiveMessaging.logger.error "Dispatch exception: #{exc}"
|
209
|
+
ActiveMessaging.logger.error exc.backtrace.join("\n\t")
|
210
|
+
raise exc
|
211
|
+
ensure
|
212
|
+
reset_application
|
213
|
+
end
|
214
|
+
}
|
215
|
+
end
|
216
|
+
|
217
|
+
def _dispatch(message)
|
218
|
+
case message.command
|
219
|
+
when 'ERROR'
|
220
|
+
ActiveMessaging.logger.error('Error from messaging infrastructure: ' + message.headers['message'])
|
221
|
+
when 'MESSAGE'
|
222
|
+
abort = false
|
223
|
+
processed = false
|
224
|
+
|
225
|
+
subscriptions.each do |key, subscription|
|
226
|
+
if subscription.matches?(message) then
|
227
|
+
processed = true
|
228
|
+
routing = {
|
229
|
+
:receiver=>subscription.processor_class,
|
230
|
+
:destination=>subscription.destination,
|
231
|
+
:direction => :incoming
|
232
|
+
}
|
233
|
+
begin
|
234
|
+
execute_filter_chain(:incoming, message, routing) do |m|
|
235
|
+
result = subscription.processor_class.new.process!(m)
|
236
|
+
end
|
237
|
+
rescue ActiveMessaging::AbortMessageException
|
238
|
+
abort_message subscription, message
|
239
|
+
abort = true
|
240
|
+
return
|
241
|
+
ensure
|
242
|
+
acknowledge_message subscription, message unless abort
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
ActiveMessaging.logger.error("No-one responded to #{message}") unless processed
|
248
|
+
else
|
249
|
+
ActiveMessaging.logger.error('Unknown message command: ' + message.inspect)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# acknowledge_message is called when the message has been processed w/o error by at least one processor
|
254
|
+
def acknowledge_message subscription, message
|
255
|
+
connection(subscription.destination.broker_name).received message, subscription.subscribe_headers
|
256
|
+
end
|
257
|
+
|
258
|
+
# abort_message is called when procesing the message raises a ActiveMessaging::AbortMessageException
|
259
|
+
# indicating the message should be returned to the destination so it can be tried again, later
|
260
|
+
def abort_message subscription, message
|
261
|
+
connection(subscription.destination.broker_name).unreceive message, subscription.subscribe_headers
|
262
|
+
end
|
263
|
+
|
264
|
+
def define
|
265
|
+
#run the rest of messaging.rb
|
266
|
+
yield self
|
267
|
+
end
|
268
|
+
|
269
|
+
def destination destination_name, destination, publish_headers={}, broker='default'
|
270
|
+
raise "You already defined #{destination_name} to #{named_destinations[destination_name].value}" if named_destinations.has_key?(destination_name)
|
271
|
+
named_destinations[destination_name] = Destination.new destination_name, destination, publish_headers, broker
|
272
|
+
end
|
273
|
+
|
274
|
+
alias queue destination
|
275
|
+
|
276
|
+
def find_destination destination_name
|
277
|
+
real_destination = named_destinations[destination_name]
|
278
|
+
raise "You have not yet defined a destination named #{destination_name}. Destinations currently defined are [#{named_destinations.keys.join(',')}]" if real_destination.nil?
|
279
|
+
real_destination
|
280
|
+
end
|
281
|
+
|
282
|
+
alias find_queue find_destination
|
283
|
+
|
284
|
+
def subscribe_to destination_name, processor, headers={}
|
285
|
+
proc_name = processor.name.underscore
|
286
|
+
proc_sym = processor.name.underscore.to_sym
|
287
|
+
if (!current_processor_group || processor_groups[current_processor_group].include?(proc_sym))
|
288
|
+
@@subscriptions["#{proc_name}:#{destination_name}"]= Subscription.new(find_destination(destination_name), processor, headers)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def publish destination_name, body, publisher=nil, headers={}, timeout=10
|
293
|
+
raise "You cannot have a nil or empty destination name." if destination_name.nil?
|
294
|
+
raise "You cannot have a nil or empty message body." if (body.nil? || body.empty?)
|
295
|
+
|
296
|
+
real_destination = find_destination(destination_name)
|
297
|
+
details = {
|
298
|
+
:publisher => publisher,
|
299
|
+
:destination => real_destination,
|
300
|
+
:direction => :outgoing
|
301
|
+
}
|
302
|
+
message = OpenStruct.new(:body => body, :headers => headers.reverse_merge(real_destination.publish_headers))
|
303
|
+
begin
|
304
|
+
Timeout.timeout timeout do
|
305
|
+
execute_filter_chain(:outgoing, message, details) do |message|
|
306
|
+
connection(real_destination.broker_name).send real_destination.value, message.body, message.headers
|
307
|
+
end
|
308
|
+
end
|
309
|
+
rescue Timeout::Error=>toe
|
310
|
+
ActiveMessaging.logger.error("Timed out trying to send the message #{message} to destination #{destination_name} via broker #{real_destination.broker_name}")
|
311
|
+
raise toe
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
def receive destination_name, receiver=nil, subscribe_headers={}, timeout=10
|
316
|
+
raise "You cannot have a nil or empty destination name." if destination_name.nil?
|
317
|
+
conn = nil
|
318
|
+
dest = find_destination destination_name
|
319
|
+
config = load_connection_configuration(dest.broker_name)
|
320
|
+
subscribe_headers['id'] = receiver.name.underscore unless (receiver.nil? or subscribe_headers.key? 'id')
|
321
|
+
Timeout.timeout timeout do
|
322
|
+
conn = Gateway.adapters[config[:adapter]].new(config)
|
323
|
+
conn.subscribe(dest.value, subscribe_headers)
|
324
|
+
message = conn.receive
|
325
|
+
conn.received message, subscribe_headers
|
326
|
+
return message
|
327
|
+
end
|
328
|
+
rescue Timeout::Error=>toe
|
329
|
+
ActiveMessaging.logger.error("Timed out trying to receive a message on destination #{destination_name}")
|
330
|
+
raise toe
|
331
|
+
ensure
|
332
|
+
conn.disconnect unless conn.nil?
|
333
|
+
end
|
334
|
+
|
335
|
+
def processor_group group_name, *processors
|
336
|
+
if processor_groups.has_key? group_name
|
337
|
+
processor_groups[group_name] = processor_groups[group_name] + processors
|
338
|
+
else
|
339
|
+
processor_groups[group_name] = processors
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
def current_processor_group
|
344
|
+
if ARGV.length > 0 && !@@current_processor_group
|
345
|
+
ARGV.each {|arg|
|
346
|
+
pair = arg.split('=')
|
347
|
+
if pair[0] == 'process-group'
|
348
|
+
group_sym = pair[1].to_sym
|
349
|
+
if processor_groups.has_key? group_sym
|
350
|
+
@@current_processor_group = group_sym
|
351
|
+
else
|
352
|
+
ActiveMessaging.logger.error "Unrecognized process-group."
|
353
|
+
ActiveMessaging.logger.error "You specified process-group #{pair[1]}, make sure this is specified in config/messaging.rb"
|
354
|
+
ActiveMessaging.logger.error " ActiveMessaging::Gateway.define do |s|"
|
355
|
+
ActiveMessaging.logger.error " s.processor_groups = { :group1 => [:foo_bar1_processor], :group2 => [:foo_bar2_processor] }"
|
356
|
+
ActiveMessaging.logger.error " end"
|
357
|
+
exit
|
358
|
+
end
|
359
|
+
end
|
360
|
+
}
|
361
|
+
end
|
362
|
+
@@current_processor_group
|
363
|
+
end
|
364
|
+
|
365
|
+
def load_connection_configuration(label='default')
|
366
|
+
@broker_yml = YAML::load(ERB.new(IO.read(File.join(APP_ROOT, 'config', 'broker.yml'))).result) if @broker_yml.nil?
|
367
|
+
if label == 'default'
|
368
|
+
config = @broker_yml[APP_ENV].symbolize_keys
|
369
|
+
else
|
370
|
+
config = @broker_yml[APP_ENV][label].symbolize_keys
|
371
|
+
end
|
372
|
+
config[:adapter] = config[:adapter].to_sym if config[:adapter]
|
373
|
+
config[:adapter] ||= :stomp
|
374
|
+
return config
|
375
|
+
end
|
376
|
+
|
377
|
+
end
|
378
|
+
|
379
|
+
end
|
380
|
+
|
381
|
+
class Subscription
|
382
|
+
attr_accessor :destination, :processor_class, :subscribe_headers
|
383
|
+
|
384
|
+
def initialize(destination, processor_class, subscribe_headers = {})
|
385
|
+
@destination, @processor_class, @subscribe_headers = destination, processor_class, subscribe_headers
|
386
|
+
subscribe_headers['id'] = processor_class.name.underscore unless subscribe_headers.key? 'id'
|
387
|
+
end
|
388
|
+
|
389
|
+
def matches?(message)
|
390
|
+
message.headers['destination'].to_s == @destination.value.to_s
|
391
|
+
end
|
392
|
+
|
393
|
+
def subscribe
|
394
|
+
ActiveMessaging.logger.error "=> Subscribing to #{destination.value} (processed by #{processor_class})"
|
395
|
+
Gateway.connection(@destination.broker_name).subscribe(@destination.value, subscribe_headers)
|
396
|
+
# FIXME (uwe): Not sure why this needs to happen here
|
397
|
+
@processor = @processor_class.new
|
398
|
+
end
|
399
|
+
|
400
|
+
def unsubscribe
|
401
|
+
ActiveMessaging.logger.error "=> Unsubscribing from #{destination.value} (processed by #{processor_class})"
|
402
|
+
Gateway.connection(destination.broker_name).unsubscribe(destination.value, subscribe_headers)
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
class Destination
|
407
|
+
DEFAULT_PUBLISH_HEADERS = { :persistent=>true }
|
408
|
+
|
409
|
+
attr_accessor :name, :value, :publish_headers, :broker_name
|
410
|
+
|
411
|
+
def initialize(name, value, publish_headers, broker_name)
|
412
|
+
@name, @value, @publish_headers, @broker_name = name, value, publish_headers, broker_name
|
413
|
+
@publish_headers.reverse_merge! DEFAULT_PUBLISH_HEADERS
|
414
|
+
end
|
415
|
+
|
416
|
+
def to_s
|
417
|
+
"#{broker_name}: #{name} => '#{value}'"
|
418
|
+
end
|
419
|
+
|
420
|
+
end
|
421
|
+
|
422
|
+
end #module
|