modern_times 0.2.11 → 0.3.0

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