activemessaging 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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