modern_times 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/README.rdoc +24 -12
  2. data/Rakefile +2 -2
  3. data/VERSION +1 -1
  4. data/examples/README +4 -0
  5. data/examples/jms.yml +9 -0
  6. data/examples/requestor/README +4 -2
  7. data/examples/requestor/manager.rb +3 -2
  8. data/examples/requestor/request.rb +5 -4
  9. data/examples/requestor/reverse_echo_worker.rb +3 -2
  10. data/examples/simple/README +7 -4
  11. data/examples/simple/bar_worker.rb +4 -1
  12. data/examples/simple/baz_worker.rb +4 -3
  13. data/examples/simple/manager.rb +3 -2
  14. data/examples/simple/publish.rb +6 -5
  15. data/lib/modern_times.rb +20 -2
  16. data/lib/modern_times/base/supervisor.rb +14 -21
  17. data/lib/modern_times/base/supervisor_mbean.rb +4 -6
  18. data/lib/modern_times/base/worker.rb +17 -26
  19. data/lib/modern_times/jms.rb +23 -0
  20. data/lib/modern_times/{hornetq/client.rb → jms/connection.rb} +19 -12
  21. data/lib/modern_times/jms/publisher.rb +91 -0
  22. data/lib/modern_times/jms/supervisor.rb +19 -0
  23. data/lib/modern_times/jms/supervisor_mbean.rb +11 -0
  24. data/lib/modern_times/jms/worker.rb +166 -0
  25. data/lib/modern_times/jms_requestor.rb +10 -0
  26. data/lib/modern_times/jms_requestor/request_handle.rb +33 -0
  27. data/lib/modern_times/jms_requestor/requestor.rb +45 -0
  28. data/lib/modern_times/jms_requestor/supervisor.rb +45 -0
  29. data/lib/modern_times/jms_requestor/supervisor_mbean.rb +21 -0
  30. data/lib/modern_times/jms_requestor/worker.rb +78 -0
  31. data/lib/modern_times/manager.rb +14 -9
  32. data/lib/modern_times/manager_mbean.rb +14 -7
  33. data/lib/modern_times/marshal_strategy.rb +47 -0
  34. data/lib/modern_times/marshal_strategy/bson.rb +31 -0
  35. data/lib/modern_times/marshal_strategy/json.rb +30 -0
  36. data/lib/modern_times/marshal_strategy/ruby.rb +20 -0
  37. data/lib/modern_times/marshal_strategy/string.rb +19 -0
  38. data/lib/modern_times/railsable.rb +17 -74
  39. data/test/base_test.rb +248 -0
  40. data/test/jms.yml +8 -0
  41. data/test/jms_requestor_test.rb +263 -0
  42. data/test/jms_test.rb +296 -0
  43. data/test/marshal_strategy_test.rb +39 -0
  44. metadata +49 -46
  45. data/examples/requestor/hornetq.yml +0 -14
  46. data/examples/simple/hornetq.yml +0 -14
  47. data/lib/modern_times/hornetq.rb +0 -11
  48. data/lib/modern_times/hornetq/marshal_strategy.rb +0 -3
  49. data/lib/modern_times/hornetq/marshal_strategy/json.rb +0 -17
  50. data/lib/modern_times/hornetq/marshal_strategy/ruby.rb +0 -17
  51. data/lib/modern_times/hornetq/marshal_strategy/string.rb +0 -17
  52. data/lib/modern_times/hornetq/publisher.rb +0 -65
  53. data/lib/modern_times/hornetq/supervisor.rb +0 -22
  54. data/lib/modern_times/hornetq/supervisor_mbean.rb +0 -12
  55. data/lib/modern_times/hornetq/worker.rb +0 -127
  56. data/lib/modern_times/hornetq_requestor.rb +0 -9
  57. data/lib/modern_times/hornetq_requestor/request_handle.rb +0 -49
  58. data/lib/modern_times/hornetq_requestor/requestor.rb +0 -48
  59. data/lib/modern_times/hornetq_requestor/worker.rb +0 -29
  60. data/lib/modern_times/thread.rb +0 -16
  61. data/test/base/worker_test.rb +0 -38
  62. data/test/messaging/worker_manager_test.rb +0 -58
  63. data/test/messaging/worker_test.rb +0 -58
  64. data/test/worker_manager_test.rb +0 -48
@@ -0,0 +1,23 @@
1
+ require 'modern_times/jms/connection'
2
+ require 'modern_times/jms/publisher'
3
+ require 'modern_times/jms/supervisor_mbean'
4
+ require 'modern_times/jms/supervisor'
5
+ require 'modern_times/jms/worker'
6
+
7
+ module ModernTimes
8
+ module JMS
9
+ def self.same_destination?(options1, options2)
10
+ if options1[:queue_name]
11
+ return options1[:queue_name] == options2[:queue_name]
12
+ elsif options1[:topic_name]
13
+ return options1[:topic_name] == options2[:topic_name]
14
+ elsif options1[:virtual_topic_name]
15
+ return options1[:virtual_topic_name] == options2[:virtual_topic_name]
16
+ elsif options1[:destination]
17
+ return options1[:destination] == options2[:destination]
18
+ else
19
+ return false
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,16 +1,18 @@
1
- require 'hornetq'
1
+ require 'jms'
2
2
 
3
3
  # Handle Messaging and Queuing
4
4
  module ModernTimes
5
- module HornetQ
6
- module Client
5
+ module JMS
6
+ module Connection
7
7
  # Singleton-ize
8
8
  extend self
9
9
 
10
10
  # Initialize the messaging system and connection pool for this VM
11
11
  def init(config)
12
12
  @config = config
13
- @connection = ::HornetQ::Client::Connection.new(@config[:connection])
13
+ @inited = true
14
+ @connection = ::JMS::Connection.new(config)
15
+ @connection.start
14
16
  # Let's not create a session_pool unless we're going to use it
15
17
  @session_pool_mutex = Mutex.new
16
18
 
@@ -19,13 +21,13 @@ module ModernTimes
19
21
  end
20
22
  end
21
23
 
22
- def invm?
23
- @connection.invm?
24
+ def inited?
25
+ @inited
24
26
  end
25
27
 
26
28
  # Create a session targeted for a consumer (producers should use the session_pool)
27
29
  def create_consumer_session
28
- @connection.create_session(config[:session])
30
+ connection.create_session(@config || {})
29
31
  end
30
32
 
31
33
  def session_pool
@@ -34,19 +36,24 @@ module ModernTimes
34
36
  @session_pool_mutex.synchronize do
35
37
  # if it's been created in between the above call and now, return it
36
38
  return @session_pool if @session_pool
37
- return @session_pool = @connection.create_session_pool(config[:session])
39
+ return @session_pool = connection.create_session_pool(@config)
38
40
  end
39
41
  end
40
42
 
41
43
  def close
44
+ return if @closed
42
45
  ModernTimes.logger.info "Closing #{self.name}"
43
46
  @session_pool.close if @session_pool
44
- @connection.close if @connection
47
+ if @connection
48
+ @connection.stop
49
+ @connection.close
50
+ end
51
+ @closed = true
45
52
  end
46
53
 
47
- def config
48
- raise "#{self.name} never had it's init method called" unless @config
49
- @config
54
+ def connection
55
+ raise "#{self.name} never had it's init method called" unless @connection
56
+ @connection
50
57
  end
51
58
  end
52
59
  end
@@ -0,0 +1,91 @@
1
+ require 'jms'
2
+
3
+ # Protocol independent class to handle Publishing
4
+ module ModernTimes
5
+ module JMS
6
+ class Publisher
7
+ attr_reader :producer_options, :persistent, :marshaler
8
+
9
+ # Parameters:
10
+ # One of the following must be specified
11
+ # :queue_name => String: Name of the Queue to publish to
12
+ # :topic_name => String: Name of the Topic to publish to
13
+ # :virtual_topic_name => String: Name of the Virtual Topic to publish to
14
+ # (ActiveMQ only, see http://activemq.apache.org/virtual-destinations.html
15
+ # :destination=> Explicit javax::Jms::Destination to use
16
+ # Optional:
17
+ # :persistent => true or false (defaults to false)
18
+ # :marshal => Symbol: One of :ruby, :string, or :json
19
+ # => Module: Module that defines marshal and unmarshal method
20
+ def initialize(options)
21
+ producer_keys = [:queue_name, :topic_name, :virtual_topic_name, :destination]
22
+ @producer_options = options.reject {|k,v| !producer_keys.include?(k)}
23
+ raise "One of #{producer_keys.join(',')} must be given in #{self.class.name}" if @producer_options.empty?
24
+
25
+ # Save our @producer_options for destination comparison when doing dummy_publish,
26
+ # but create the real options by translating virtual_topic_name to a real topic_name.
27
+ @real_producer_options = @producer_options.dup
28
+ virtual_topic_name = @real_producer_options.delete(:virtual_topic_name)
29
+ @real_producer_options[:topic_name] = "VirtualTopic.#{virtual_topic_name}" if virtual_topic_name
30
+
31
+ # If we're in dummy mode, this probably won't be defined
32
+ #@persistent = options[:persistent] ? ::JMS::DeliveryMode::PERSISTENT : ::JMS::DeliveryMode::NON_PERSISTENT
33
+ @persistent = options[:persistent] ? :persistent : :non_persistent
34
+ @marshaler = ModernTimes::MarshalStrategy.find(options[:marshal])
35
+ end
36
+
37
+ # Publish the given object to the address.
38
+ def publish(object, props={})
39
+ message = nil
40
+ Connection.session_pool.producer(@real_producer_options) do |session, producer|
41
+ message = case @marshaler.marshal_type
42
+ when :text
43
+ session.create_text_message(@marshaler.marshal(object))
44
+ when :bytes
45
+ msg = session.create_bytes_message()
46
+ msg.data = @marshaler.marshal(object)
47
+ msg
48
+ else raise "Invalid marshal type: #{@marshaler.marshal_type}"
49
+ end
50
+ message.jms_delivery_mode_sym = @persistent
51
+ props.each do |key, value|
52
+ message.send("#{key}=", value)
53
+ end
54
+ # TODO: Is send_with_retry possible?
55
+ #producer.send_with_retry(message)
56
+ producer.send(message)
57
+ end
58
+ return message
59
+ end
60
+
61
+ # For non-configured Rails projects, The above publish method will be overridden to
62
+ # call this publish method instead which calls all the JMS workers that
63
+ # operate on the given address.
64
+ def dummy_publish(object)
65
+ @@worker_instances.each do |worker|
66
+ if worker.kind_of?(Worker) && ModernTimes::JMS.same_destination?(@producer_options, worker.class.destination_options)
67
+ ModernTimes.logger.debug "Dummy publishing #{object} to #{worker}"
68
+ worker.perform(object)
69
+ end
70
+ end
71
+ end
72
+
73
+ def to_s
74
+ "#{self.class.name}:#{@real_producer_options.inspect}"
75
+ end
76
+
77
+ def self.setup_dummy_publishing(workers)
78
+ @@worker_instances = workers.map {|worker| worker.new}
79
+ alias_method :real_publish, :publish
80
+ alias_method :publish, :dummy_publish
81
+ end
82
+
83
+ # For testing
84
+ def self.clear_dummy_publishing
85
+ alias_method :dummy_publish, :publish
86
+ alias_method :publish, :real_publish
87
+ #remove_method :real_publish
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,19 @@
1
+ module ModernTimes
2
+ module JMS
3
+ class Supervisor < ModernTimes::Base::Supervisor
4
+
5
+ def initialize(manager, worker_name, supervisor_options, worker_options)
6
+ super
7
+ end
8
+
9
+ def message_counts
10
+ workers.map { |w| w.message_count }
11
+ end
12
+
13
+ # Make JMS::SupervisorMBean our mbean
14
+ def create_mbean(domain)
15
+ SupervisorMBean.new(mbean_name(domain), mbean_description, self, {})
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ module ModernTimes
2
+ module JMS
3
+ class SupervisorMBean < ModernTimes::Base::SupervisorMBean
4
+ r_attribute :message_counts, :list, 'Message counts for the workers', :message_counts
5
+
6
+ def message_counts
7
+ java.util.ArrayList.new(supervisor.message_counts)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,166 @@
1
+ module ModernTimes
2
+ module JMS
3
+
4
+ # Base Worker Class for any class that will be processing messages from topics or queues
5
+ # By default, it will consume messages from a queue with the class name minus the Worker postfix.
6
+ # For example, the queue call is unneccessary as it will default to a value of 'Foo' anyways:
7
+ # class FooWorker < ModernTimes::JMS::Worker
8
+ # queue 'Foo'
9
+ # def perform(obj)
10
+ # # Perform work on obj
11
+ # end
12
+ # end
13
+ #
14
+ # A topic can be specified using virtual_topic as follows (ActiveMQ only). Multiple separate workers can
15
+ # subscribe to the same topic (under ActiveMQ - see http://activemq.apache.org/virtual-destinations.html):
16
+ # class FooWorker < ModernTimes::JMS::Worker
17
+ # virtual_topic 'Zulu'
18
+ # def perform(obj)
19
+ # # Perform work on obj
20
+ # end
21
+ # end
22
+ #
23
+ # Filters can also be specified within the class:
24
+ # class FooWorker < ModernTimes::JMS::Worker
25
+ # filter 'age > 30'
26
+ # def perform(obj)
27
+ # # Perform work on obj
28
+ # end
29
+ # end
30
+ #
31
+ #
32
+ module Worker
33
+ include ModernTimes::Base::Worker
34
+
35
+ attr_reader :session, :message, :message_count
36
+
37
+ module ClassMethods
38
+ def create_supervisor(manager, worker_options)
39
+ Supervisor.new(manager, self, {}, worker_options)
40
+ end
41
+
42
+ def marshal(option)
43
+ @marshaler = ModernTimes::MarshalStrategy.find(option)
44
+ end
45
+
46
+ def destination_options
47
+ options = dest_options.dup
48
+ # Default the queue name to the Worker name if a destinations hasn't been specified
49
+ if options.keys.select {|k| [:virtual_topic_name, :queue_name, :destination].include?(k)}.empty?
50
+ options[:queue_name] = default_name
51
+ end
52
+ return options
53
+ end
54
+
55
+ def virtual_topic(name, opts={})
56
+ # ActiveMQ only
57
+ dest_options[:virtual_topic_name] = name.to_s
58
+ end
59
+
60
+ def queue(name, opts={})
61
+ dest_options[:queue_name] = name.to_s
62
+ end
63
+
64
+ def dest_options
65
+ @dest_options ||= {}
66
+ end
67
+
68
+ def marshaler
69
+ # Default to ruby marshaling, but extenders can override as necessary
70
+ @marshaler ||= ModernTimes::MarshalStrategy::Ruby
71
+ end
72
+ end
73
+
74
+ def self.included(base)
75
+ base.extend(ModernTimes::Base::Worker::ClassMethods)
76
+ base.extend(ClassMethods)
77
+ end
78
+
79
+ def initialize(opts={})
80
+ super
81
+ @status = 'initialized'
82
+ @message_count = 0
83
+ end
84
+
85
+ def setup
86
+ end
87
+
88
+ def status
89
+ @status || "Processing message #{message_count}"
90
+ end
91
+
92
+ def real_destination_options
93
+ options = self.class.destination_options
94
+ virtual_topic_name = options.delete(:virtual_topic_name)
95
+ options[:queue_name] = "Consumer.#{name}.VirtualTopic.#{virtual_topic_name}" if virtual_topic_name
96
+ return options
97
+ end
98
+
99
+ def on_message(message)
100
+ @message = message
101
+ object = self.class.marshaler.unmarshal(message.data)
102
+ ModernTimes.logger.debug "#{self}: Received Object: #{object}" if ModernTimes.logger.debug?
103
+ perform(object)
104
+ ModernTimes.logger.debug "#{self}: Finished processing message" if ModernTimes.logger.debug?
105
+ ModernTimes.logger.flush if ModernTimes.logger.respond_to?(:flush)
106
+ rescue Exception => e
107
+ ModernTimes.logger.error "#{self}: Messaging Exception: #{e.inspect}\n#{e.backtrace.inspect}"
108
+ rescue java.lang.Exception => e
109
+ ModernTimes.logger.error "#{self}: Java Messaging Exception: #{e.inspect}\n#{e.backtrace.inspect}"
110
+ end
111
+
112
+ def perform(object)
113
+ raise "#{self}: Need to override perform method in #{self.class.name} in order to act on #{object}"
114
+ end
115
+
116
+ def to_s
117
+ "#{real_destination_options.to_a.join('=>')}:#{index}"
118
+ end
119
+
120
+ # Start the event loop for handling messages off the queue
121
+ def start
122
+ @session = Connection.create_consumer_session
123
+ @consumer = @session.consumer(real_destination_options)
124
+ @session.start
125
+
126
+ ModernTimes.logger.debug "#{self}: Starting receive loop"
127
+ @status = nil
128
+ while msg = @consumer.receive
129
+ @message_count += 1
130
+ on_message(msg)
131
+ msg.acknowledge
132
+ end
133
+ @status = 'Exited'
134
+ ModernTimes.logger.info "#{self}: Exiting"
135
+ rescue javax.jms.IllegalStateException => e
136
+ #if e.cause.code == Java::org.jms.api.core.JMSException::OBJECT_CLOSED
137
+ # Normal exit
138
+ @status = 'Exited'
139
+ ModernTimes.logger.info "#{self}: Exiting due to close"
140
+ #else
141
+ # @status = "Exited with JMS exception #{e.message}"
142
+ # ModernTImes.logger.error "#{self} JMSException: #{e.message}\n#{e.backtrace.join("\n")}"
143
+ #end
144
+ rescue Exception => e
145
+ @status = "Exited with exception #{e.message}"
146
+ ModernTimes.logger.error "#{self}: Exception, thread terminating: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
147
+ rescue java.lang.Exception => e
148
+ @status = "Exited with java exception #{e.message}"
149
+ ModernTimes.logger.error "#{self}: Java exception, thread terminating: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
150
+ end
151
+
152
+ def stop
153
+ @consumer.close if @consumer
154
+ @session.close if @session
155
+ end
156
+
157
+ #########
158
+ protected
159
+ #########
160
+
161
+ # Create session information and allow extenders to initialize anything necessary prior to the event loop
162
+ def session_init
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,10 @@
1
+ require 'modern_times/jms_requestor/request_handle'
2
+ require 'modern_times/jms_requestor/requestor'
3
+ require 'modern_times/jms_requestor/supervisor_mbean'
4
+ require 'modern_times/jms_requestor/supervisor'
5
+ require 'modern_times/jms_requestor/worker'
6
+
7
+ module ModernTimes
8
+ module JMSRequestor
9
+ end
10
+ end
@@ -0,0 +1,33 @@
1
+ require 'timeout'
2
+
3
+ module ModernTimes
4
+ module JMSRequestor
5
+ class RequestHandle
6
+ def initialize(requestor, message, start, timeout)
7
+ @requestor = requestor
8
+ @reply_queue = requestor.reply_queue
9
+ @message = message
10
+ @start = start
11
+ @timeout = timeout
12
+ end
13
+
14
+ def read_response
15
+ response = nil
16
+ opts = { :destination => @reply_queue, :selector => "JMSCorrelationID = '#{@message.jms_message_id}'" }
17
+ #opts = { :destination => @reply_queue }
18
+ #opts = {:queue_name => 'foobarzulu'}
19
+ ModernTimes::JMS::Connection.session_pool.consumer(opts) do |session, consumer|
20
+ leftover_timeout = ((@start + @timeout - Time.now) * 1000).to_i
21
+ if leftover_timeout > 100
22
+ response = consumer.receive(leftover_timeout)
23
+ else
24
+ #response = consumer.receive_no_wait
25
+ response = consumer.receive(100)
26
+ end
27
+ end
28
+ raise Timeout::Error, "Timeout waiting for for response from message #{@message.jms_message_id} on queue #{@reply_queue}" unless response
29
+ return @requestor.marshaler.unmarshal(response.data)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,45 @@
1
+ module ModernTimes
2
+ module JMSRequestor
3
+ class Requestor < ModernTimes::JMS::Publisher
4
+ attr_reader :reply_queue
5
+
6
+ def initialize(options)
7
+ super
8
+ ModernTimes::JMS::Connection.session_pool.session do |session|
9
+ @reply_queue = session.create_destination(:queue_name => :temporary)
10
+ end
11
+ end
12
+
13
+ def request(object, timeout)
14
+ start = Time.now
15
+ message = publish(object, :jms_reply_to => @reply_queue)
16
+ return RequestHandle.new(self, message, start, timeout)
17
+ end
18
+
19
+ # For non-configured Rails projects, The above request method will be overridden to
20
+ # call this request method instead which calls all the JMS workers that
21
+ # operate on the given address.
22
+ def dummy_request(object, timeout)
23
+ @@worker_instances.each do |worker|
24
+ if worker.kind_of?(Worker) && ModernTimes::JMS.same_destination?(producer_options, worker.destination_options)
25
+ ModernTimes.logger.debug "Dummy requesting #{object} to #{worker}"
26
+ return new OpenStruct(:read_response => worker.request(object))
27
+ end
28
+ end
29
+ raise "No worker to handle #{address} request of #{object}"
30
+ end
31
+
32
+ def self.setup_dummy_requesting(workers)
33
+ @@worker_instances = workers.map {|worker| worker.new}
34
+ alias_method :real_request, :request
35
+ alias_method :request, :dummy_request
36
+ end
37
+
38
+ # For testing
39
+ def self.clear_dummy_requesting
40
+ alias_method :dummy_request, :request
41
+ alias_method :request, :real_request
42
+ end
43
+ end
44
+ end
45
+ end