rjr 0.9.0 → 0.11.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,271 @@
1
+ # Thread Pool (second implementation)
2
+ #
3
+ # Copyright (C) 2010-2012 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the Apache License, Version 2.0
5
+
6
+ require 'singleton'
7
+
8
+ # Work item to be executed in a thread launched by {ThreadPool2}.
9
+ #
10
+ # The end user just need to initialize this class with the handle
11
+ # to the job to be executed and the params to pass to it, before
12
+ # handing it off to the thread pool that will take care of the rest.
13
+ class ThreadPool2Job
14
+ attr_accessor :handler
15
+ attr_accessor :params
16
+
17
+ # used internally by the thread pool system, these shouldn't
18
+ # be set or used by the end user
19
+ attr_accessor :timestamp
20
+ attr_accessor :thread
21
+ attr_accessor :pool_lock
22
+ attr_reader :being_executed
23
+
24
+ # ThreadPoolJob initializer
25
+ # @param [Array] params arguments to pass to the job when it is invoked
26
+ # @param [Callable] block handle to callable object corresponding to job to invoke
27
+ def initialize(*params, &block)
28
+ @params = params
29
+ @handler = block
30
+ @being_executed = false
31
+ @timestamp = nil
32
+ end
33
+
34
+ # Return string representation of thread pool job
35
+ def to_s
36
+ "thread_pool2_job-#{@handler.source_location}-#{@params}"
37
+ end
38
+
39
+ def being_executed?
40
+ @being_executed
41
+ end
42
+
43
+ def completed?
44
+ !@timestamp.nil? && !@being_executed
45
+ end
46
+
47
+ # Set job metadata and execute job with specified params
48
+ def exec
49
+ # synchronized so that both timestamp is set and being_executed
50
+ # set to true before the possiblity of a timeout management
51
+ # check (see handle_timeout! below)
52
+ @pool_lock.synchronize{
53
+ @thread = Thread.current
54
+ @being_executed = true
55
+ @timestamp = Time.now
56
+ }
57
+
58
+ @handler.call *@params
59
+
60
+ # synchronized so as to ensure that a timeout check does not
61
+ # occur until before (in which case thread is killed during
62
+ # the check as one atomic operation) or after (in which case
63
+ # job is marked as completed, and thread is not killed / goes
64
+ # onto pull anther job)
65
+ @pool_lock.synchronize{
66
+ @being_executed = false
67
+ }
68
+ end
69
+
70
+ # Check timeout and kill thread if it exceeded.
71
+ def handle_timeout!(timeout)
72
+ # Synchronized so that check and kill operation occur as an
73
+ # atomic operation, see exec above
74
+ @pool_lock.synchronize {
75
+ if @being_executed && (Time.now - @timestamp) > timeout
76
+ RJR::Logger.debug "timeout detected on thread #{@thread} started at #{@timestamp}"
77
+ @thread.kill
78
+ return true
79
+ end
80
+ return false
81
+ }
82
+ end
83
+ end
84
+
85
+ # Utility to launches a specified number of threads on instantiation,
86
+ # assigning work to them in order as it arrives.
87
+ #
88
+ # Supports optional timeout which allows the developer to kill and restart
89
+ # threads if a job is taking too long to run.
90
+ #
91
+ # Second (and hopefully better) thread pool implementation.
92
+ #
93
+ # TODO move to the RJR namespace
94
+ class ThreadPool2
95
+ private
96
+
97
+ # Internal helper, launch worker thread
98
+ #
99
+ # Should only be launched from within the pool_lock
100
+ def launch_worker
101
+ @worker_threads << Thread.new {
102
+ while work = @work_queue.pop
103
+ begin
104
+ #RJR::Logger.debug "launch thread pool job #{work}"
105
+ work.pool_lock = @pool_lock
106
+ @running_queue << work
107
+ work.exec
108
+ # TODO cleaner / more immediate way to pop item off running_queue
109
+ #RJR::Logger.debug "finished thread pool job #{work}"
110
+ rescue Exception => e
111
+ # FIXME also send to rjr logger at a critical level
112
+ puts "Thread raised Fatal Exception #{e}"
113
+ puts "\n#{e.backtrace.join("\n")}"
114
+ end
115
+ end
116
+ } unless @worker_threads.size == @num_threads
117
+ end
118
+
119
+ # Internal helper, performs checks on workers
120
+ def check_workers
121
+ if @terminate
122
+ @pool_lock.synchronize {
123
+ @worker_threads.each { |t|
124
+ t.kill
125
+ }
126
+ @worker_threads = []
127
+ }
128
+
129
+ elsif @timeout
130
+ readd = []
131
+ while @running_queue.size > 0 && work = @running_queue.pop
132
+ if @timeout && work.handle_timeout!(@timeout)
133
+ @pool_lock.synchronize {
134
+ @worker_threads.delete(work.thread)
135
+ launch_worker
136
+ }
137
+ elsif !work.completed?
138
+ readd << work
139
+ end
140
+ end
141
+ readd.each { |work| @running_queue << work }
142
+ end
143
+ end
144
+
145
+ # Internal helper, launch management thread
146
+ #
147
+ # Should only be launched from within the pool_lock
148
+ def launch_manager
149
+ @manager_thread = Thread.new {
150
+ until @terminate
151
+ # sleep needs to occur b4 check workers so
152
+ # workers are guaranteed to be terminated on @terminate
153
+ # !FIXME! this enforces a mandatory setting of @timeout which was never intended:
154
+ sleep @timeout
155
+ check_workers
156
+ end
157
+ check_workers
158
+ @pool_lock.synchronize { @manager_thread = nil }
159
+ } unless @manager_thread
160
+ end
161
+
162
+ public
163
+ # Create a thread pool with a specified number of threads
164
+ # @param [Integer] num_threads the number of worker threads to create
165
+ # @param [Hash] args optional arguments to initialize thread pool with
166
+ # @option args [Integer] :timeout optional timeout to use to kill long running worker jobs
167
+ def initialize(num_threads, args = {})
168
+ @work_queue = Queue.new
169
+ @running_queue = Queue.new
170
+
171
+ @num_threads = num_threads
172
+ @pool_lock = Mutex.new
173
+ @worker_threads = []
174
+
175
+ @timeout = args[:timeout]
176
+
177
+ ObjectSpace.define_finalizer(self, self.class.finalize(self))
178
+ end
179
+
180
+ # Return internal thread pool state in string
181
+ def inspect
182
+ "wq#{@work_queue.size}/\
183
+ rq#{@running_queue.size}/\
184
+ nt#{@num_threads.size}/\
185
+ wt#{@worker_threads.select { |wt| ['sleep', 'run'].include?(wt.status) }.size}ok-\
186
+ #{@worker_threads.select { |wt| ['aborting', false, nil].include?(wt.status) }.size}nok/\
187
+ to#{@timeout}"
188
+ end
189
+
190
+ # Start the thread pool
191
+ def start
192
+ # clear work and timeout queues?
193
+ @pool_lock.synchronize {
194
+ @terminate = false
195
+ launch_manager
196
+ 0.upto(@num_threads) { |i| launch_worker }
197
+ }
198
+ end
199
+
200
+ # Ruby ObjectSpace finalizer to ensure that thread pool terminates all
201
+ # threads when object is destroyed
202
+ def self.finalize(thread_pool)
203
+ proc { thread_pool.stop ; thread_pool.join }
204
+ end
205
+
206
+ # Return boolean indicating if thread pool is running.
207
+ #
208
+ # If at least one worker thread isn't terminated, the pool is still considered running
209
+ def running?
210
+ @pool_lock.synchronize { @worker_threads.size != 0 && @worker_threads.all? { |t| t.status } }
211
+ end
212
+
213
+ # Add work to the pool
214
+ # @param [ThreadPool2Job] work job to execute in first available thread
215
+ def <<(work)
216
+ @work_queue.push work
217
+ end
218
+
219
+ # Terminate the thread pool, stopping all worker threads
220
+ def stop
221
+ @pool_lock.synchronize {
222
+ @terminate = true
223
+
224
+ # wakeup management thread so it can kill workers
225
+ # before terminating on its own
226
+ begin
227
+ @manager_thread.wakeup
228
+
229
+ # incase thread wakes up / terminates on its own
230
+ rescue ThreadError
231
+
232
+ end
233
+ }
234
+ join
235
+ end
236
+
237
+ # Block until all worker threads have finished executing
238
+ def join
239
+ #@pool_lock.synchronize { @worker_threads.each { |t| t.join unless @terminate } }
240
+ th = nil
241
+ @pool_lock.synchronize { th = @manager_thread if @manager_thread }
242
+ th.join if th
243
+ end
244
+ end
245
+
246
+ # Providers an interface to access a shared thread pool.
247
+ #
248
+ # Thread pool operations may be invoked on this class after
249
+ # the 'init' method is called
250
+ #
251
+ # ThreadPool2Manager.init
252
+ # ThreadPool2Manager << ThreadPool2Job(:foo) { "do something" }
253
+ class ThreadPool2Manager
254
+ # Initialize thread pool if it doesn't exist
255
+ def self.init(num_threads, params = {})
256
+ if @thread_pool.nil?
257
+ @thread_pool = ThreadPool2.new(num_threads, params)
258
+ end
259
+ @thread_pool.start
260
+ end
261
+
262
+ # Return shared thread pool
263
+ def self.thread_pool
264
+ @thread_pool
265
+ end
266
+
267
+ # Delegates all methods invoked on calls to thread pool
268
+ def self.method_missing(method_id, *args, &bl)
269
+ @thread_pool.send method_id, *args, &bl
270
+ end
271
+ end
data/lib/rjr/util.rb ADDED
@@ -0,0 +1,104 @@
1
+ # High level rjr utility mechanisms
2
+ #
3
+ # Copyright (C) 2013 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the Apache License, Version 2.0
5
+
6
+ require 'rjr/dispatcher'
7
+
8
+ module RJR
9
+
10
+ # Mixin providing utility methods to define rjr methods and messages
11
+ module Definitions
12
+ # Define one or more rjr methods, parameters should be in the form
13
+ # :id => Callable
14
+ #
15
+ # id may be a single id or an array of them
16
+ def rjr_method(args = {})
17
+ args.each { |k, v|
18
+ RJR::Dispatcher.add_handler(k.to_s, &v)
19
+ }
20
+ nil
21
+ end
22
+
23
+ # Define/retrieve rjr messages. When defining pass a hash
24
+ # of mesasge ids to options to use in the defintions, eg
25
+ # :id => { :foo => :bar }
26
+ #
27
+ # When retrieving simply specify the id
28
+ def rjr_message(args={})
29
+ if args.is_a?(Hash)
30
+ args.each { |k,v|
31
+ RJR::Definitions.message(k.to_s, v)
32
+ }
33
+ nil
34
+ else
35
+ RJR::Definitions.message(args)
36
+ end
37
+ end
38
+
39
+ # Helper providing access to messages
40
+ def self.message(k, v=nil)
41
+ @rjr_messages ||= {}
42
+ @rjr_messages[k] = v unless v.nil?
43
+ @rjr_messages[k]
44
+ end
45
+
46
+ # Reset message registry
47
+ def self.reset
48
+ # TODO also invoke 'Dispatcher.init_handlers' ?
49
+ @rjr_messages = {}
50
+ end
51
+
52
+ # Generate / return random message. Optionally specify the transport which
53
+ # the message must accept
54
+ def self.rand_msg(transport = nil)
55
+ @rjr_messages ||= {}
56
+ messages = @rjr_messages.select { |mid,m| m[:transports].nil? || transport.nil? ||
57
+ m[:transports].include?(transport) }
58
+ messages[messages.keys[rand(messages.keys.size)]]
59
+ end
60
+ end
61
+
62
+ # Class to encapsulate any number of rjr nodes
63
+ class EasyNode
64
+ def initialize(node_args = {})
65
+ nodes = node_args.keys.collect { |n|
66
+ case n
67
+ when :amqp then
68
+ RJR::AMQPNode.new node_args[:amqp]
69
+ when :ws then
70
+ RJR::WSNode.new node_args[:ws]
71
+ when :tcp then
72
+ RJR::TCPNode.new node_args[:tcp]
73
+ when :www then
74
+ RJR::WebNode.new node_args[:www]
75
+ end
76
+ }
77
+ @multi_node = RJR::MultiNode.new :nodes => nodes
78
+ end
79
+
80
+ def invoke_request(dst, method, *params)
81
+ # TODO allow selection of node, eg automatically deduce which node type to use from 'dst'
82
+ @multi_node.nodes.first.invoke_request(dst, method, *params)
83
+ end
84
+
85
+ # Stop node on the specified signal
86
+ def stop_on(signal)
87
+ Signal.trap(signal) {
88
+ @multi_node.stop
89
+ }
90
+ self
91
+ end
92
+
93
+ def listen
94
+ @multi_node.listen
95
+ self
96
+ end
97
+
98
+ def join
99
+ @multi_node.join
100
+ self
101
+ end
102
+ end
103
+
104
+ end # module RJR
data/lib/rjr/web_node.rb CHANGED
@@ -8,16 +8,27 @@
8
8
  # Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
9
9
  # Licensed under the Apache License, Version 2.0
10
10
 
11
- # establish client connection w/ specified args and invoke block w/
12
- # newly created client, returning it after block terminates
11
+ skip_module = false
12
+ begin
13
+ require 'evma_httpserver'
14
+ require 'em-http-request'
15
+ # TODO also support fallback clients ? (curb / net/http / etc)
16
+ rescue LoadError
17
+ skip_module = true
18
+ end
13
19
 
14
- require 'curb'
20
+ if skip_module
21
+ # TODO output: "curb/evma_httpserver gems could not be loaded, skipping web node definition"
22
+ require 'rjr/missing_node'
23
+ RJR::WebNode = RJR::MissingNode
15
24
 
16
- require 'evma_httpserver'
17
- #require 'em-http-request'
25
+ else
26
+ require 'socket'
18
27
 
19
28
  require 'rjr/node'
20
29
  require 'rjr/message'
30
+ require 'rjr/dispatcher'
31
+ require 'rjr/thread_pool2'
21
32
 
22
33
  module RJR
23
34
 
@@ -28,6 +39,7 @@ class WebNodeCallback
28
39
  end
29
40
 
30
41
  def invoke(callback_method, *data)
42
+ # TODO throw error?
31
43
  end
32
44
  end
33
45
 
@@ -49,8 +61,7 @@ class WebRequestHandler < EventMachine::Connection
49
61
  def process_http_request
50
62
  # TODO support http protocols other than POST
51
63
  msg = @http_post_content.nil? ? '' : @http_post_content
52
- #@thread_pool << ThreadPoolJob.new { handle_request(msg) }
53
- handle_request(msg)
64
+ ThreadPool2Manager << ThreadPool2Job.new(msg) { |m| handle_request(m) }
54
65
  end
55
66
 
56
67
  private
@@ -59,9 +70,12 @@ class WebRequestHandler < EventMachine::Connection
59
70
  def handle_request(message)
60
71
  msg = nil
61
72
  result = nil
73
+ notification = NotificationMessage.is_notification_message?(msg)
74
+
62
75
  begin
63
76
  client_port, client_ip = Socket.unpack_sockaddr_in(get_peername)
64
- msg = RequestMessage.new(:message => message, :headers => @web_node.message_headers)
77
+ msg = notification ? NotificationMessage.new(:message => message, :headers => @web_node.message_headers) :
78
+ RequestMessage.new(:message => message, :headers => @web_node.message_headers)
65
79
  headers = @web_node.message_headers.merge(msg.headers)
66
80
  result = Dispatcher.dispatch_request(msg.jr_method,
67
81
  :method_args => msg.jr_args,
@@ -79,12 +93,14 @@ class WebRequestHandler < EventMachine::Connection
79
93
  msg_id = msg.nil? ? nil : msg.msg_id
80
94
  response = ResponseMessage.new(:id => msg_id, :result => result, :headers => headers)
81
95
 
82
- resp = EventMachine::DelegatedHttpResponse.new(self)
83
- #resp.status = response.result.success ? 200 : 500
84
- resp.status = 200
85
- resp.content = response.to_s
86
- resp.content_type "application/json"
87
- resp.send_response
96
+ unless notification
97
+ resp = EventMachine::DelegatedHttpResponse.new(self)
98
+ #resp.status = response.result.success ? 200 : 500
99
+ resp.status = 200
100
+ resp.content = response.to_s
101
+ resp.content_type "application/json"
102
+ resp.send_response
103
+ end
88
104
  end
89
105
  end
90
106
 
@@ -117,13 +133,48 @@ end
117
133
  #
118
134
  class WebNode < RJR::Node
119
135
  private
120
- # Initialize the web subsystem
121
- def init_node
136
+
137
+ # Internal helper, handle response message received
138
+ def handle_response(http)
139
+ msg = ResponseMessage.new(:message => http.response, :headers => @message_headers)
140
+ res = err = nil
141
+ begin
142
+ res = Dispatcher.handle_response(msg.result)
143
+ rescue Exception => e
144
+ err = e
145
+ end
146
+
147
+ @response_lock.synchronize {
148
+ result = [msg.msg_id, res]
149
+ result << err if !err.nil?
150
+ @responses << result
151
+ @response_cv.signal
152
+ }
153
+ end
154
+
155
+ # Internal helper, block until response matching message id is received
156
+ def wait_for_result(message)
157
+ res = nil
158
+ while res.nil?
159
+ @response_lock.synchronize{
160
+ # FIXME throw err if more than 1 match found
161
+ res = @responses.select { |response| message.msg_id == response.first }.first
162
+ if !res.nil?
163
+ @responses.delete(res)
164
+
165
+ else
166
+ @response_cv.signal
167
+ @response_cv.wait @response_lock
168
+
169
+ end
170
+ }
171
+ end
172
+ return res
122
173
  end
123
174
 
124
175
  public
125
176
 
126
- # TCPNode initializer
177
+ # WebNode initializer
127
178
  # @param [Hash] args the options to create the tcp node with
128
179
  # @option args [String] :host the hostname/ip which to listen on
129
180
  # @option args [Integer] :port the port which to listen on
@@ -131,6 +182,10 @@ class WebNode < RJR::Node
131
182
  super(args)
132
183
  @host = args[:host]
133
184
  @port = args[:port]
185
+
186
+ @response_lock = Mutex.new
187
+ @response_cv = ConditionVariable.new
188
+ @responses = []
134
189
  end
135
190
 
136
191
  # Register connection event handler,
@@ -148,7 +203,6 @@ class WebNode < RJR::Node
148
203
  # Implementation of {RJR::Node#listen}
149
204
  def listen
150
205
  em_run do
151
- init_node
152
206
  EventMachine::start_server(@host, @port, WebRequestHandler, self)
153
207
  end
154
208
  end
@@ -159,15 +213,53 @@ class WebNode < RJR::Node
159
213
  # @param [String] rpc_method json-rpc method to invoke on destination
160
214
  # @param [Array] args array of arguments to convert to json and invoke remote method wtih
161
215
  def invoke_request(uri, rpc_method, *args)
162
- init_node
163
216
  message = RequestMessage.new :method => rpc_method,
164
217
  :args => args,
165
218
  :headers => @message_headers
166
- res = Curl::Easy.http_post uri, message.to_s
167
- msg = ResponseMessage.new(:message => res.body_str, :headers => @message_headers)
168
- headers = @message_headers.merge(msg.headers)
169
- return Dispatcher.handle_response(msg.result)
219
+ cb = lambda { |http|
220
+ # TODO handle errors
221
+ handle_response(http)
222
+ }
223
+
224
+ em_run do
225
+ http = EventMachine::HttpRequest.new(uri).post :body => message.to_s
226
+ http.errback &cb
227
+ http.callback &cb
228
+ end
229
+
230
+ # will block until response message is received
231
+ # TODO optional timeout for response ?
232
+ result = wait_for_result(message)
233
+ if result.size > 2
234
+ raise Exception, result[2]
235
+ end
236
+ return result[1]
237
+ end
238
+
239
+ # Instructs node to send rpc notification (immadiately returns / no response is generated)
240
+ #
241
+ # @param [String] uri location of node to send request to, should be
242
+ # in format of http://hostname:port
243
+ # @param [String] rpc_method json-rpc method to invoke on destination
244
+ # @param [Array] args array of arguments to convert to json and invoke remote method wtih
245
+ def send_notification(uri, rpc_method, *args)
246
+ # will block until message is published
247
+ published_l = Mutex.new
248
+ published_c = ConditionVariable.new
249
+
250
+ message = NotificationMessage.new :method => rpc_method,
251
+ :args => args,
252
+ :headers => @message_headers
253
+ cb = lambda { |arg| published_l.synchronize { published_c.signal }}
254
+ em_run do
255
+ http = EventMachine::HttpRequest.new(uri).post :body => message.to_s
256
+ http.errback &cb
257
+ http.callback &cb
258
+ end
259
+ published_l.synchronize { published_c.wait published_l }
260
+ nil
170
261
  end
171
262
  end
172
263
 
173
264
  end # module RJR
265
+ end