modern_times 0.1.2 → 0.2.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 (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