modern_times 0.2.11 → 0.3.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 (50) hide show
  1. data/README.rdoc +114 -80
  2. data/VERSION +1 -1
  3. data/examples/advanced_requestor/README +15 -0
  4. data/examples/advanced_requestor/base_request_worker.rb +13 -0
  5. data/examples/advanced_requestor/char_count_worker.rb +11 -0
  6. data/examples/advanced_requestor/exception_raiser_worker.rb +10 -0
  7. data/examples/advanced_requestor/length_worker.rb +9 -0
  8. data/examples/advanced_requestor/manager.rb +22 -0
  9. data/examples/advanced_requestor/modern_times.yml +32 -0
  10. data/examples/advanced_requestor/print_worker.rb +9 -0
  11. data/examples/advanced_requestor/publish.rb +46 -0
  12. data/examples/advanced_requestor/reverse_worker.rb +9 -0
  13. data/examples/advanced_requestor/triple_worker.rb +9 -0
  14. data/examples/requestor/request.rb +3 -3
  15. data/examples/requestor/reverse_echo_worker.rb +1 -2
  16. data/lib/modern_times.rb +1 -1
  17. data/lib/modern_times/base/supervisor.rb +2 -0
  18. data/lib/modern_times/base/worker.rb +5 -3
  19. data/lib/modern_times/jms.rb +2 -0
  20. data/lib/modern_times/jms/connection.rb +7 -0
  21. data/lib/modern_times/jms/publish_handle.rb +219 -0
  22. data/lib/modern_times/jms/publisher.rb +55 -29
  23. data/lib/modern_times/{jms_requestor/worker.rb → jms/request_worker.rb} +29 -51
  24. data/lib/modern_times/jms/supervisor.rb +30 -0
  25. data/lib/modern_times/jms/supervisor_mbean.rb +17 -1
  26. data/lib/modern_times/jms/worker.rb +43 -40
  27. data/lib/modern_times/manager.rb +6 -2
  28. data/lib/modern_times/marshal_strategy.rb +14 -17
  29. data/lib/modern_times/marshal_strategy/bson.rb +2 -0
  30. data/lib/modern_times/marshal_strategy/json.rb +3 -0
  31. data/lib/modern_times/marshal_strategy/ruby.rb +3 -0
  32. data/lib/modern_times/marshal_strategy/string.rb +3 -0
  33. data/lib/modern_times/marshal_strategy/yaml.rb +3 -0
  34. data/lib/modern_times/railsable.rb +7 -14
  35. data/lib/modern_times/time_track.rb +84 -0
  36. data/test/jms.yml +1 -0
  37. data/test/jms_failure_test.rb +128 -0
  38. data/test/jms_requestor_block_test.rb +275 -0
  39. data/test/jms_requestor_test.rb +71 -96
  40. data/test/jms_test.rb +59 -78
  41. data/test/marshal_strategy_test.rb +1 -3
  42. metadata +29 -14
  43. data/examples/exception_test/bar_worker.rb +0 -8
  44. data/examples/exception_test/base_worker.rb +0 -23
  45. data/examples/exception_test/manager.rb +0 -11
  46. data/lib/modern_times/jms_requestor.rb +0 -10
  47. data/lib/modern_times/jms_requestor/request_handle.rb +0 -42
  48. data/lib/modern_times/jms_requestor/requestor.rb +0 -56
  49. data/lib/modern_times/jms_requestor/supervisor.rb +0 -45
  50. data/lib/modern_times/jms_requestor/supervisor_mbean.rb +0 -21
@@ -19,15 +19,15 @@ $sim_count = (ARGV[3] || 1).to_i
19
19
 
20
20
  config = YAML.load(ERB.new(File.read(File.join(File.dirname(__FILE__), '..', 'jms.yml'))).result(binding))
21
21
  ModernTimes::JMS::Connection.init(config)
22
- $requestor = ModernTimes::JMSRequestor::Requestor.new(:queue_name => ReverseEchoWorker.default_name, :marshal => :string)
22
+ $publisher = ModernTimes::JMS::Publisher.new(:queue_name => ReverseEchoWorker.default_name, :response =>true, :marshal => :string)
23
23
 
24
24
  def make_request(ident='')
25
25
  puts "#{ident}Making request at #{Time.now.to_f}"
26
- handle = $requestor.request("#{ident}#{$echo_string}", $timeout)
26
+ handle = $publisher.publish("#{ident}#{$echo_string}")
27
27
  # Here's where we'd go off and do other work
28
28
  sleep $sleep_time
29
29
  puts "#{ident}Finished sleeping at #{Time.now.to_f}"
30
- response = handle.read_response
30
+ response = handle.read_response($timeout)
31
31
  puts "#{ident}Received at #{Time.now.to_f}: #{response}"
32
32
  rescue Exception => e
33
33
  puts "#{ident}Exception: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
@@ -1,6 +1,5 @@
1
1
  class ReverseEchoWorker
2
- include ModernTimes::JMSRequestor::Worker
3
- marshal :string
2
+ include ModernTimes::JMS::RequestWorker
4
3
 
5
4
  def request(obj)
6
5
  puts "#{self}: Received #{obj} at #{Time.now}"
data/lib/modern_times.rb CHANGED
@@ -3,11 +3,11 @@ require 'modern_times/remote_exception'
3
3
  require 'modern_times/marshal_strategy'
4
4
  require 'modern_times/base'
5
5
  require 'modern_times/jms'
6
- require 'modern_times/jms_requestor'
7
6
  require 'modern_times/manager_mbean'
8
7
  require 'modern_times/manager'
9
8
  require 'modern_times/loggable'
10
9
  require 'modern_times/railsable'
10
+ require 'modern_times/time_track'
11
11
 
12
12
  module ModernTimes
13
13
  extend ModernTimes::Loggable
@@ -47,6 +47,8 @@ module ModernTimes
47
47
  (count...curr_count).each { |index| @workers[index].stop }
48
48
  (count...curr_count).each { |index| @workers[index].thread.join }
49
49
  @workers = @workers[0, count]
50
+ else
51
+ return
50
52
  end
51
53
  manager.save_persist_state
52
54
  end
@@ -1,7 +1,8 @@
1
1
  module ModernTimes
2
2
  module Base
3
3
  module Worker
4
- attr_accessor :name, :index, :thread
4
+ attr_reader :name, :options
5
+ attr_accessor :index, :thread
5
6
 
6
7
  module ClassMethods
7
8
  def default_name
@@ -18,8 +19,9 @@ module ModernTimes
18
19
  base.extend(ClassMethods)
19
20
  end
20
21
 
21
- def initialize(opts={})
22
- @name = opts[:name] || self.class.default_name
22
+ def initialize(options={})
23
+ @name = options[:name] || self.class.default_name
24
+ @options = options
23
25
  end
24
26
 
25
27
  # One time initialization prior to first thread
@@ -1,9 +1,11 @@
1
1
  require 'modern_times/jms/connection'
2
2
  require 'modern_times/jms/consumer'
3
3
  require 'modern_times/jms/publisher'
4
+ require 'modern_times/jms/publish_handle'
4
5
  require 'modern_times/jms/supervisor_mbean'
5
6
  require 'modern_times/jms/supervisor'
6
7
  require 'modern_times/jms/worker'
8
+ require 'modern_times/jms/request_worker'
7
9
 
8
10
  module ModernTimes
9
11
  module JMS
@@ -11,6 +11,9 @@ module ModernTimes
11
11
  def init(config)
12
12
  @config = config
13
13
  @inited = true
14
+ @log_times = config.delete(:log_times)
15
+ # Default to true
16
+ @log_times = true if @log_times.nil?
14
17
 
15
18
  # Let's not create a session_pool unless we're going to use it
16
19
  @session_pool_mutex = Mutex.new
@@ -27,6 +30,10 @@ module ModernTimes
27
30
  @inited
28
31
  end
29
32
 
33
+ def log_times?
34
+ @log_times
35
+ end
36
+
30
37
  # Create a session targeted for a consumer (producers should use the session_pool)
31
38
  def create_consumer_session
32
39
  connection.create_session(@config || {})
@@ -0,0 +1,219 @@
1
+ require 'timeout'
2
+ require 'yaml'
3
+
4
+ module ModernTimes
5
+ module JMS
6
+ class PublishHandle
7
+ def initialize(publisher, jms_message_id, start)
8
+ @publisher = publisher
9
+ @jms_message_id = jms_message_id
10
+ @start = start
11
+ # Dummy hash will store all the responses from the RequestWorker's matching our publishing destination.
12
+ @dummy_hash = {}
13
+ end
14
+
15
+ # Waits the given timeout for a response message on the queue.
16
+ #
17
+ # If called w/o a block:
18
+ # Returns the message
19
+ # Returns nil on timeout
20
+ # Raises RemoteException on a remote exception
21
+ #
22
+ # If called with a block, for instance:
23
+ # handle.read_response(timeout) do |response|
24
+ # response.on_message 'CharCount' do |hash|
25
+ # puts "CharCount returned #{hash.inspect}"
26
+ # end
27
+ # response.on_message 'Length', 'Reverse' do |val|
28
+ # puts "#{response.name} returned #{val}"
29
+ # end
30
+ # response.on_message 'ExceptionRaiser' do |val|
31
+ # puts "#{response.name} didn't raise an exception but returned #{val}"
32
+ # end
33
+ # response.on_timeout 'Reverse' do
34
+ # puts "Reverse has it's own timeout handler"
35
+ # end
36
+ # response.on_timeout do
37
+ # puts "#{response.name} did not respond in time"
38
+ # end
39
+ # response.on_remote_exception 'ExceptionRaiser' do
40
+ # puts "It figures that ExceptionRaiser would raise an exception"
41
+ # end
42
+ # response.on_remote_exception do |e|
43
+ # puts "#{response.name} raised an exception #{e.message}\n\t#{e.backtrace.join("\n\t")}"
44
+ # end
45
+ # end
46
+ #
47
+ # The specified blocks will be called for each response. For instance, LengthWorker#request
48
+ # might return 4 and "Length returned 4" would be displayed. If it failed to respond within the
49
+ # timeout, then "Length did no respond in time" would be displayed.
50
+ # For Workers that raise an exception, they will either be handled by their specific handler if it exists or
51
+ # the default exception handler. If that doesn't exist either, then the RemoteException will be raised for the
52
+ # whole read_response call. Timeouts will also be handled by the default timeout handler unless a specific one
53
+ # is specified. All messages must have a specific handler specified because the call won't return until all
54
+ # specified handlers either return, timeout, or return an exception.
55
+ #
56
+ def read_response(timeout, &block)
57
+ reply_queue = @publisher.reply_queue
58
+ raise "Invalid call to read_response for #{@publisher}, not setup for responding" unless reply_queue
59
+ options = { :destination => reply_queue, :selector => "JMSCorrelationID = '#{@jms_message_id}'" }
60
+ ModernTimes::JMS::Connection.session_pool.consumer(options) do |session, consumer|
61
+ do_read_response(consumer, timeout, &block)
62
+ end
63
+ end
64
+
65
+ def dummy_read_response(timeout, &block)
66
+ raise "Invalid call to read_response for #{@publisher}, not setup for responding" unless @publisher.response
67
+ do_read_response(nil, timeout, &block)
68
+ end
69
+
70
+ def add_dummy_response(name, object)
71
+ @dummy_hash[name] = object
72
+ end
73
+
74
+ def self.setup_dummy_handling
75
+ alias_method :real_read_response, :read_response
76
+ alias_method :read_response, :dummy_read_response
77
+ alias_method :real_read_single_response, :read_single_response
78
+ alias_method :read_single_response, :dummy_read_single_response
79
+ end
80
+
81
+ # For testing
82
+ def self.clear_dummy_handling
83
+ alias_method :dummy_read_response, :read_response
84
+ alias_method :read_response, :real_read_response
85
+ alias_method :dummy_read_single_response, :read_single_response
86
+ alias_method :read_single_response, :real_read_single_response
87
+ end
88
+ #######
89
+ private
90
+ #######
91
+
92
+ class WorkerResponse
93
+ attr_reader :name, :start
94
+
95
+ def initialize(start)
96
+ @start = start
97
+ @message_hash = {}
98
+ @timeout_hash = {}
99
+ @exception_hash = {}
100
+ @default_timeout_block = nil
101
+ @default_exception_block = nil
102
+ @done_array = []
103
+ end
104
+
105
+ # Msecs since publish
106
+ def msec_delta
107
+ (Time.now - @start) * 1000
108
+ end
109
+
110
+ def on_message(*names, &block)
111
+ raise 'Must explicitly define all message handlers so we know that we\'re done' if names.empty?
112
+ names.each {|name| @message_hash[name] = block}
113
+ end
114
+
115
+ def on_timeout(*names, &block)
116
+ if names.empty?
117
+ @default_timeout_block = block
118
+ else
119
+ names.each {|name| @timeout_hash[name] = block}
120
+ end
121
+ end
122
+
123
+ def on_remote_exception(*names, &block)
124
+ if names.empty?
125
+ @default_exception_block = block
126
+ else
127
+ names.each {|name| @exception_hash[name] = block}
128
+ end
129
+ @remote_exception_block = block
130
+ end
131
+
132
+ def make_message_call(name, obj)
133
+ # Give the client access to the name
134
+ @name = name
135
+ block = @message_hash[name]
136
+ block.call(obj) if block
137
+ @done_array << name
138
+ end
139
+
140
+ def done?
141
+ (@message_hash.keys - @done_array).empty?
142
+ end
143
+
144
+ def make_timeout_calls
145
+ @timeouts = @message_hash.keys - @done_array
146
+ @timeouts.each do |name|
147
+ # Give the client access to the name
148
+ @name = name
149
+ block = @timeout_hash[name] || @default_timeout_block
150
+ block.call if block
151
+ end
152
+ end
153
+
154
+ def make_exception_call(name, e)
155
+ @name = name
156
+ block = @exception_hash[name] || @default_exception_block
157
+ if block
158
+ block.call(e)
159
+ @done_array << name
160
+ else
161
+ raise e
162
+ end
163
+ end
164
+ end
165
+
166
+ def do_read_response(consumer, timeout, &block)
167
+ if block_given?
168
+ return read_multiple_response(consumer, timeout, &block)
169
+ else
170
+ response = read_single_response(consumer, timeout)
171
+ raise response if response.kind_of?(ModernTimes::RemoteException)
172
+ return response
173
+ end
174
+ end
175
+
176
+ def read_single_response(consumer, timeout)
177
+ message = nil
178
+ leftover_timeout = ((@start + timeout - Time.now) * 1000).to_i
179
+ if leftover_timeout > 100
180
+ message = consumer.receive(leftover_timeout)
181
+ else
182
+ #message = consumer.receive_no_wait
183
+ message = consumer.receive(100)
184
+ end
185
+ return nil unless message
186
+ @name = message['worker']
187
+ if error_yaml = message['exception']
188
+ return ModernTimes::RemoteException.from_hash(YAML.load(error_yaml))
189
+ end
190
+ marshaler = ModernTimes::MarshalStrategy.find(message['marshal'] || :ruby)
191
+ return marshaler.unmarshal(message.data)
192
+ end
193
+
194
+ def dummy_read_single_response(consumer, timeout)
195
+ @name = @dummy_hash.keys.first
196
+ return nil unless @name
197
+ return @dummy_hash.delete(@name)
198
+ end
199
+
200
+ def read_multiple_response(consumer, timeout, &block)
201
+ worker_response = WorkerResponse.new(@start)
202
+ yield worker_response
203
+
204
+ until worker_response.done? do
205
+ response = read_single_response(consumer, timeout)
206
+ if !response
207
+ worker_response.make_timeout_calls
208
+ return
209
+ end
210
+ if response.kind_of?(ModernTimes::RemoteException)
211
+ worker_response.make_exception_call(@name, response)
212
+ else
213
+ worker_response.make_message_call(@name, response)
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -4,20 +4,25 @@ require 'jms'
4
4
  module ModernTimes
5
5
  module JMS
6
6
  class Publisher
7
- attr_reader :producer_options, :persistent, :marshaler
7
+ attr_reader :producer_options, :persistent, :marshaler, :reply_queue, :response
8
+
9
+ @@dummy_publishing = false
8
10
 
9
11
  # Parameters:
10
12
  # 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
+ # :queue_name => String: Name of the Queue to publish to
14
+ # :topic_name => String: Name of the Topic to publish to
13
15
  # :virtual_topic_name => String: Name of the Virtual Topic to publish to
14
16
  # (ActiveMQ only, see http://activemq.apache.org/virtual-destinations.html
15
- # :destination=> Explicit javax::Jms::Destination to use
17
+ # :destination => Explicit javax::Jms::Destination to use
16
18
  # 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
19
+ # :persistent => true or false (defaults to false)
20
+ # :marshal => Symbol: One of :ruby, :string, :json, :bson, :yaml or any registered types (See ModernTimes::MarshalStrategy), defaults to :ruby
21
+ # => Module: Module that defines marshal and unmarshal method
22
+ # :time_to_live => expiration time in ms for the message
23
+ # :response => if true, a temporary reply queue will be setup for handling responses (defaults to false)
20
24
  def initialize(options)
25
+ raise "ModernTimes::JMS::Connection has not been initialized" unless ModernTimes::JMS::Connection.inited? || @@dummy_publishing
21
26
  producer_keys = [:queue_name, :topic_name, :virtual_topic_name, :destination]
22
27
  @producer_options = options.reject {|k,v| !producer_keys.include?(k)}
23
28
  raise "One of #{producer_keys.join(',')} must be given in #{self.class.name}" if @producer_options.empty?
@@ -31,43 +36,67 @@ module ModernTimes
31
36
  # If we're in dummy mode, this probably won't be defined
32
37
  #@persistent = options[:persistent] ? ::JMS::DeliveryMode::PERSISTENT : ::JMS::DeliveryMode::NON_PERSISTENT
33
38
  @persistent = options[:persistent] ? :persistent : :non_persistent
34
- @marshaler = ModernTimes::MarshalStrategy.find(options[:marshal])
39
+ @marshal = options[:marshal] || :ruby
40
+ @marshaler = ModernTimes::MarshalStrategy.find(@marshal)
35
41
  @time_to_live = options[:time_to_live]
42
+
43
+ @reply_queue = nil
44
+ @response = options[:response]
45
+ if !@@dummy_publishing && @response
46
+ ModernTimes::JMS::Connection.session_pool.session do |session|
47
+ @reply_queue = session.create_destination(:queue_name => :temporary)
48
+ end
49
+ end
36
50
  end
37
51
 
38
52
  # Publish the given object to the address.
39
53
  def publish(object, props={})
54
+ start = Time.now
40
55
  message = nil
41
56
  Connection.session_pool.producer(@real_producer_options) do |session, producer|
42
57
  producer.time_to_live = @time_to_live if @time_to_live
43
58
  message = ModernTimes::JMS.create_message(session, @marshaler, object)
44
59
  message.jms_delivery_mode_sym = @persistent
60
+ message.jms_reply_to = @reply_queue if @reply_queue
61
+ message['marshal'] = @marshal.to_s
45
62
  props.each do |key, value|
46
63
  message.send("#{key}=", value)
47
64
  end
48
- # TODO: Is send_with_retry possible?
49
- #producer.send_with_retry(message)
50
65
  producer.send(message)
51
66
  end
52
- return message.jms_message_id
67
+ return PublishHandle.new(self, message.jms_message_id, start)
53
68
  end
54
69
 
55
70
  # For non-configured Rails projects, The above publish method will be overridden to
56
71
  # call this publish method instead which calls all the JMS workers that
57
72
  # operate on the given address.
58
73
  def dummy_publish(object, props={})
59
- @@message_id += 1
60
- @@worker_instances.each do |worker|
61
- if worker.kind_of?(Worker) && ModernTimes::JMS.same_destination?(@producer_options, worker.class.destination_options)
62
- ModernTimes.logger.debug "Dummy publishing #{object} to #{worker}"
63
- worker.message = OpenStruct.new(:jms_message_id => @@message_id.to_s)
64
- worker.perform(object)
74
+ dummy_handle = PublishHandle.new(self, nil, Time.now)
75
+ # Model real queue marshaling/unmarshaling
76
+ trans_object = @marshaler.unmarshal(@marshaler.marshal(object))
77
+ @@workers.each do |worker|
78
+ if ModernTimes::JMS.same_destination?(@producer_options, worker.class.destination_options)
79
+ if worker.kind_of?(RequestWorker)
80
+ ModernTimes.logger.debug "Dummy request publishing #{trans_object} to #{worker}"
81
+ m = worker.marshaler
82
+ # Model real queue marshaling/unmarshaling
83
+ begin
84
+ response_object = m.unmarshal(m.marshal(worker.request(trans_object)))
85
+ dummy_handle.add_dummy_response(worker.name, response_object)
86
+ rescue Exception => e
87
+ dummy_handle.add_dummy_response(worker.name, ModernTimes::RemoteException.new(e))
88
+ end
89
+ elsif worker.kind_of?(Worker)
90
+ ModernTimes.logger.debug "Dummy publishing #{trans_object} to #{worker}"
91
+ begin
92
+ worker.perform(trans_object)
93
+ rescue Exception => e
94
+ ModernTimes.logger.error("#{worker} Exception: #{e.message}\n\t#{e.backtrace.join("\n\t")}")
95
+ end
96
+ end
65
97
  end
66
98
  end
67
- if correlation_id = props[:jms_correlation_id]
68
- @@dummy_cache[correlation_id] = object
69
- end
70
- return @@message_id.to_s
99
+ return dummy_handle
71
100
  end
72
101
 
73
102
  def to_s
@@ -75,23 +104,20 @@ module ModernTimes
75
104
  end
76
105
 
77
106
  def self.setup_dummy_publishing(workers)
78
- require 'ostruct'
79
- @@message_id = 0
80
- @@dummy_cache = {}
81
- @@worker_instances = workers.map {|worker| worker.new}
107
+ @@dummy_publishing = true
108
+ @@workers = workers
82
109
  alias_method :real_publish, :publish
83
110
  alias_method :publish, :dummy_publish
111
+ PublishHandle.setup_dummy_handling
84
112
  end
85
113
 
86
114
  # For testing
87
115
  def self.clear_dummy_publishing
116
+ @@dummy_publishing = false
88
117
  alias_method :dummy_publish, :publish
89
118
  alias_method :publish, :real_publish
90
119
  #remove_method :real_publish
91
- end
92
-
93
- def self.dummy_cache(correlation_id)
94
- @@dummy_cache.delete(correlation_id)
120
+ PublishHandle.clear_dummy_handling
95
121
  end
96
122
  end
97
123
  end