rjr 0.9.0 → 0.11.7

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.
@@ -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