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.
Files changed (46) hide show
  1. data/Rakefile +50 -0
  2. data/generators/a13g_test_harness/a13g_test_harness_generator.rb +19 -0
  3. data/generators/a13g_test_harness/templates/active_messaging_test.rhtml +13 -0
  4. data/generators/a13g_test_harness/templates/active_messaging_test_controller.rb +29 -0
  5. data/generators/a13g_test_harness/templates/index.rhtml +17 -0
  6. data/generators/filter/USAGE +0 -0
  7. data/generators/filter/filter_generator.rb +19 -0
  8. data/generators/filter/templates/filter.rb +12 -0
  9. data/generators/filter/templates/filter_test.rb +28 -0
  10. data/generators/processor/USAGE +8 -0
  11. data/generators/processor/processor_generator.rb +31 -0
  12. data/generators/processor/templates/application.rb +18 -0
  13. data/generators/processor/templates/broker.yml +79 -0
  14. data/generators/processor/templates/jruby_poller +117 -0
  15. data/generators/processor/templates/messaging.rb +12 -0
  16. data/generators/processor/templates/poller +23 -0
  17. data/generators/processor/templates/poller.rb +23 -0
  18. data/generators/processor/templates/processor.rb +8 -0
  19. data/generators/processor/templates/processor_test.rb +20 -0
  20. data/generators/tracer/USAGE +8 -0
  21. data/generators/tracer/templates/controller.rb +14 -0
  22. data/generators/tracer/templates/helper.rb +2 -0
  23. data/generators/tracer/templates/index.rhtml +4 -0
  24. data/generators/tracer/templates/layout.rhtml +16 -0
  25. data/generators/tracer/templates/trace_processor.rb +100 -0
  26. data/generators/tracer/tracer_generator.rb +25 -0
  27. data/lib/activemessaging.rb +133 -0
  28. data/lib/activemessaging/adapter.rb +21 -0
  29. data/lib/activemessaging/adapters/asqs.rb +412 -0
  30. data/lib/activemessaging/adapters/base.rb +82 -0
  31. data/lib/activemessaging/adapters/jms.rb +237 -0
  32. data/lib/activemessaging/adapters/reliable_msg.rb +190 -0
  33. data/lib/activemessaging/adapters/stomp.rb +99 -0
  34. data/lib/activemessaging/adapters/test.rb +155 -0
  35. data/lib/activemessaging/adapters/wmq.rb +202 -0
  36. data/lib/activemessaging/filter.rb +29 -0
  37. data/lib/activemessaging/gateway.rb +422 -0
  38. data/lib/activemessaging/message_sender.rb +30 -0
  39. data/lib/activemessaging/named_base.rb +54 -0
  40. data/lib/activemessaging/processor.rb +45 -0
  41. data/lib/activemessaging/support.rb +17 -0
  42. data/lib/activemessaging/test_helper.rb +194 -0
  43. data/lib/activemessaging/trace_filter.rb +34 -0
  44. data/messaging.rb.example +5 -0
  45. data/tasks/start_consumers.rake +8 -0
  46. 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