rjr 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,146 @@
1
+ # RJR Message
2
+ #
3
+ # Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
5
+
6
+ # establish client connection w/ specified args and invoke block w/
7
+ # newly created client, returning it after block terminates
8
+
9
+ require 'json'
10
+
11
+ module RJR
12
+
13
+ # Message sent from client to server to invoke a json-rpc method
14
+ class RequestMessage
15
+ # Helper method to generate a random id
16
+ def self.gen_uuid
17
+ ["%02x"*4, "%02x"*2, "%02x"*2, "%02x"*2, "%02x"*6].join("-") %
18
+ Array.new(16) {|x| rand(0xff) }
19
+ end
20
+
21
+ attr_accessor :json_message
22
+ attr_accessor :jr_method
23
+ attr_accessor :jr_args
24
+ attr_accessor :msg_id
25
+ attr_accessor :headers
26
+
27
+ def initialize(args = {})
28
+ if args.has_key?(:message)
29
+ begin
30
+ request = JSON.parse(args[:message])
31
+ @json_message = args[:message]
32
+ @jr_method = request['method']
33
+ @jr_args = request['params']
34
+ @msg_id = request['id']
35
+ @headers = args.has_key?(:headers) ? {}.merge!(args[:headers]) : {}
36
+
37
+ request.keys.select { |k|
38
+ !['jsonrpc', 'id', 'method', 'params'].include?(k)
39
+ }.each { |k| @headers[k] = request[k] }
40
+
41
+ rescue Exception => e
42
+ #puts "Exception Parsing Request #{e}"
43
+ # TODO
44
+ raise e
45
+ end
46
+
47
+ elsif args.has_key?(:method)
48
+ @jr_method = args[:method]
49
+ @jr_args = args[:args]
50
+ @headers = args[:headers]
51
+ @msg_id = RequestMessage.gen_uuid
52
+
53
+ end
54
+ end
55
+
56
+ def self.is_request_message?(message)
57
+ begin
58
+ JSON.parse(message).has_key?('method')
59
+ rescue Exception => e
60
+ false
61
+ end
62
+ end
63
+
64
+ def to_s
65
+ request = { 'jsonrpc' => '2.0',
66
+ 'method' => @jr_method,
67
+ 'params' => @jr_args }
68
+ request['id'] = @msg_id unless @msg_id.nil?
69
+ request.merge!(@headers) unless @headers.nil?
70
+ request.to_json.to_s
71
+ end
72
+
73
+ end
74
+
75
+ # Message sent from server to client in response to request message
76
+ class ResponseMessage
77
+ attr_accessor :json_message
78
+ attr_accessor :msg_id
79
+ attr_accessor :result
80
+ attr_accessor :headers
81
+
82
+ def initialize(args = {})
83
+ if args.has_key?(:message)
84
+ response = JSON.parse(args[:message])
85
+ @json_message = args[:message]
86
+ @msg_id = response['id']
87
+ @result = Result.new
88
+ @result.success = response.has_key?('result')
89
+ @result.failed = !response.has_key?('result')
90
+ @headers = args.has_key?(:headers) ? {}.merge!(args[:headers]) : {}
91
+
92
+ if @result.success
93
+ @result.result = response['result']
94
+
95
+ elsif response.has_key?('error')
96
+ @result.error_code = response['error']['code']
97
+ @result.error_msg = response['error']['message']
98
+ @result.error_class = response['error']['class'] # TODO safely constantize this
99
+
100
+ end
101
+
102
+ response.keys.select { |k|
103
+ !['jsonrpc', 'id', 'result', 'error'].include?(k)
104
+ }.each { |k| @headers[k] = response[k] }
105
+
106
+ elsif args.has_key?(:result)
107
+ @msg_id = args[:id]
108
+ @result = args[:result]
109
+ @headers = args[:headers]
110
+
111
+ #else
112
+ # raise ArgumentError, "must specify :message or :result"
113
+
114
+ end
115
+
116
+ end
117
+
118
+ def self.is_response_message?(message)
119
+ begin
120
+ JSON.parse(message).has_key?('result')
121
+ rescue Exception => e
122
+ false
123
+ end
124
+ end
125
+
126
+ def to_s
127
+ s = ''
128
+ if result.success
129
+ s = {'jsonrpc' => '2.0',
130
+ 'id' => @msg_id,
131
+ 'result' => @result.result}
132
+
133
+ else
134
+ s = {'jsonrpc' => '2.0',
135
+ 'id' => @msg_id,
136
+ 'error' => { 'code' => @result.error_code,
137
+ 'message' => @result.error_msg,
138
+ 'class' => @result.error_class}}
139
+ end
140
+
141
+ s.merge! @headers unless headers.nil?
142
+ return s.to_json.to_s
143
+ end
144
+ end
145
+
146
+ end
@@ -0,0 +1,35 @@
1
+ # RJR MultiNode Endpoint
2
+ #
3
+ # Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
5
+
6
+ # establish client connection w/ specified args and invoke block w/
7
+ # newly created client, returning it after block terminates
8
+
9
+ require 'eventmachine'
10
+ require 'rjr/node'
11
+ require 'rjr/message'
12
+
13
+ module RJR
14
+
15
+ class MultiNode < RJR::Node
16
+ # initialize the node w/ the specified params
17
+ def initialize(args = {})
18
+ super(args)
19
+ @nodes = args[:nodes]
20
+ end
21
+
22
+ def <<(node)
23
+ @nodes << node
24
+ end
25
+
26
+
27
+ # Instruct Node to start listening for and dispatching rpc requests
28
+ def listen
29
+ @nodes.each { |node|
30
+ node.listen
31
+ }
32
+ end
33
+ end
34
+
35
+ end
data/lib/rjr/node.rb ADDED
@@ -0,0 +1,81 @@
1
+ # RJR Node
2
+ #
3
+ # Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
5
+
6
+ # establish client connection w/ specified args and invoke block w/
7
+ # newly created client, returning it after block terminates
8
+
9
+ require 'eventmachine'
10
+ require 'rjr/thread_pool'
11
+
12
+ module RJR
13
+
14
+ # Defines a node which can be used to dispatch rpc requests and/or register
15
+ # handlers for incomping requests.
16
+ class Node
17
+ # node always has a node id
18
+ attr_reader :node_id
19
+
20
+ # attitional parameters to set on messages
21
+ attr_accessor :message_headers
22
+
23
+ def initialize(args = {})
24
+ @node_id = args[:node_id]
25
+
26
+ @message_headers = {}
27
+ @message_headers.merge!(args[:headers]) if args.has_key?(:headers)
28
+
29
+ # threads pool to handle incoming requests
30
+ # FIXME make the # of threads and timeout configurable)
31
+ @thread_pool = ThreadPool.new(10, :timeout => 5)
32
+ end
33
+
34
+ # run job in event machine
35
+ def em_run(&bl)
36
+ @@em_jobs ||= 0
37
+ @@em_jobs += 1
38
+
39
+ @@em_thread ||= nil
40
+
41
+ if @@em_thread.nil?
42
+ @@em_thread =
43
+ Thread.new{
44
+ begin
45
+ EventMachine.run
46
+ rescue Exception => e
47
+ puts "Critical exception #{e}"
48
+ ensure
49
+ end
50
+ }
51
+ #sleep 0.5 until EventMachine.reactor_running? # XXX hacky way to do this
52
+ end
53
+ EventMachine.schedule bl
54
+ end
55
+
56
+ def em_running?
57
+ EventMachine.reactor_running?
58
+ end
59
+
60
+ def join
61
+ if @@em_thread
62
+ @@em_thread.join
63
+ @@em_thread = nil
64
+ end
65
+ end
66
+
67
+ def stop
68
+ @@em_jobs -= 1
69
+ if @@em_jobs == 0
70
+ EventMachine.stop
71
+ @thread_pool.stop
72
+ end
73
+ end
74
+
75
+ def halt
76
+ @@em_jobs = 0
77
+ EventMachine.stop
78
+ end
79
+
80
+ end
81
+ end # module RJR
@@ -0,0 +1,58 @@
1
+ #
2
+ # $Id: semaphore.rb,v 1.2 2003/03/15 20:10:10 fukumoto Exp $
3
+ #
4
+ # Copied unmodified from:
5
+ # http://www.imasy.or.jp/~fukumoto/ruby/semaphore.rb
6
+ # Originally licensed under The Ruby License:
7
+ # http://raa.ruby-lang.org/project/semaphore/
8
+
9
+ class CountingSemaphore
10
+
11
+ def initialize(initvalue = 0)
12
+ @counter = initvalue
13
+ @waiting_list = []
14
+ end
15
+
16
+ def wait
17
+ Thread.critical = true
18
+ if (@counter -= 1) < 0
19
+ @waiting_list.push(Thread.current)
20
+ Thread.stop
21
+ end
22
+ self
23
+ ensure
24
+ Thread.critical = false
25
+ end
26
+
27
+ def signal
28
+ Thread.critical = true
29
+ begin
30
+ if (@counter += 1) <= 0
31
+ t = @waiting_list.shift
32
+ t.wakeup if t
33
+ end
34
+ rescue ThreadError
35
+ retry
36
+ end
37
+ self
38
+ ensure
39
+ Thread.critical = false
40
+ end
41
+
42
+ alias down wait
43
+ alias up signal
44
+ alias P wait
45
+ alias V signal
46
+
47
+ def exclusive
48
+ wait
49
+ yield
50
+ ensure
51
+ signal
52
+ end
53
+
54
+ alias synchronize exclusive
55
+
56
+ end
57
+
58
+ Semaphore = CountingSemaphore
@@ -0,0 +1 @@
1
+ # TODO
@@ -0,0 +1,165 @@
1
+ # Thread Pool
2
+ #
3
+ # Copyright (C) 2010 Mohammed Morsi <movitto@yahoo.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person
6
+ # obtaining a copy of this software and associated documentation
7
+ # files (the "Software"), to deal in the Software without
8
+ # restriction, including without limitation the rights to use,
9
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the
11
+ # Software is furnished to do so, subject to the following
12
+ # conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ # OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ # Work item to be executed in thread pool
27
+ class ThreadPoolJob
28
+ attr_accessor :handler
29
+ attr_accessor :params
30
+
31
+ def initialize(*params, &block)
32
+ @params = params
33
+ @handler = block
34
+ end
35
+ end
36
+
37
+
38
+ # Launches a specified number of threads on instantiation,
39
+ # assigning work to them as it arrives
40
+ class ThreadPool
41
+
42
+ # Encapsulate each thread pool thread in object
43
+ class ThreadPoolJobRunner
44
+ attr_accessor :time_started
45
+
46
+ def initialize(thread_pool)
47
+ @thread_pool = thread_pool
48
+ @timeout_lock = Mutex.new
49
+ @thread_lock = Mutex.new
50
+ end
51
+
52
+ def run
53
+ @thread_lock.synchronize {
54
+ @thread = Thread.new {
55
+ until @thread_pool.terminate
56
+ @timeout_lock.synchronize { @time_started = nil }
57
+ work = @thread_pool.next_job
58
+ @timeout_lock.synchronize { @time_started = Time.now }
59
+ unless work.nil?
60
+ begin
61
+ work.handler.call *work.params
62
+ rescue Exception => e
63
+ puts "Thread raised Fatal Exception #{e}"
64
+ puts "\n#{e.backtrace.join("\n")}"
65
+ end
66
+ end
67
+ end
68
+ }
69
+ }
70
+ end
71
+
72
+ def running?
73
+ res = nil
74
+ @thread_lock.synchronize{
75
+ res = (@thread.status != false)
76
+ }
77
+ res
78
+ end
79
+
80
+ # TODO should not invoke after stop is called
81
+ def check_timeout(timeout)
82
+ @timeout_lock.synchronize {
83
+ if !@time_started.nil? && Time.now - @time_started > timeout
84
+ stop
85
+ run
86
+ end
87
+ }
88
+ end
89
+
90
+ def stop
91
+ @thread_lock.synchronize {
92
+ if @thread.alive?
93
+ @thread.kill
94
+ @thread.join
95
+ end
96
+ }
97
+ end
98
+ end
99
+
100
+ # Create a thread pool with a specified number of threads
101
+ def initialize(num_threads, args = {})
102
+ @num_threads = num_threads
103
+ @timeout = args[:timeout]
104
+ @job_runners = []
105
+ @job_runners_lock = Mutex.new
106
+ @terminate = false
107
+ @terminate_lock = Mutex.new
108
+
109
+ @work_queue = Queue.new
110
+
111
+ 0.upto(@num_threads) { |i|
112
+ runner = ThreadPoolJobRunner.new(self)
113
+ @job_runners << runner
114
+ runner.run
115
+ }
116
+
117
+ # optional timeout thread
118
+ unless @timeout.nil?
119
+ @timeout_thread = Thread.new {
120
+ until terminate
121
+ sleep @timeout
122
+ @job_runners_lock.synchronize {
123
+ @job_runners.each { |jr|
124
+ jr.check_timeout(@timeout)
125
+ }
126
+ }
127
+ end
128
+ }
129
+ end
130
+ end
131
+
132
+ def running?
133
+ !terminate && (@timeout.nil? || @timeout_thread.status) &&
134
+ @job_runners.all? { |r| r.running? }
135
+ end
136
+
137
+ # terminate reader
138
+ def terminate
139
+ @terminate_lock.synchronize { @terminate }
140
+ end
141
+
142
+ # terminate setter
143
+ def terminate=(val)
144
+ @terminate_lock.synchronize { @terminate = val }
145
+ end
146
+
147
+ # Add work to the pool
148
+ def <<(work)
149
+ @work_queue.push work
150
+ end
151
+
152
+ # Return the next job queued up
153
+ def next_job
154
+ @work_queue.pop
155
+ end
156
+
157
+ # Terminate the thread pool
158
+ def stop
159
+ terminate = true
160
+ @timeout_thread.join unless @timout_thread.nil?
161
+ @work_queue.clear
162
+ @job_runners_lock.synchronize { @job_runners.each { |jr| jr.stop } }
163
+ end
164
+ end
165
+
@@ -0,0 +1 @@
1
+ # TODO
@@ -0,0 +1,112 @@
1
+ # RJR WWW Endpoint
2
+ #
3
+ # Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
5
+
6
+ # establish client connection w/ specified args and invoke block w/
7
+ # newly created client, returning it after block terminates
8
+
9
+ require 'curb'
10
+
11
+ require 'evma_httpserver'
12
+ #require 'em-http-request'
13
+
14
+ require 'rjr/node'
15
+ require 'rjr/message'
16
+
17
+ module RJR
18
+
19
+ # Web client node callback interface,
20
+ # currently does nothing as web connections aren't persistant
21
+ class WebNodeCallback
22
+ def initialize()
23
+ end
24
+
25
+ def invoke(callback_method, *data)
26
+ end
27
+ end
28
+
29
+ # Web node definition, listen for and invoke json-rpc requests via web requests
30
+ class WebRequestHandler < EventMachine::Connection
31
+ include EventMachine::HttpServer
32
+
33
+ RJR_NODE_TYPE = :web
34
+
35
+ def initialize(*args)
36
+ @web_node = args[0]
37
+ end
38
+
39
+ def handle_request(message)
40
+ msg = nil
41
+ result = nil
42
+ begin
43
+ msg = RequestMessage.new(:message => message, :headers => @web_node.message_headers)
44
+ headers = @web_node.message_headers.merge(msg.headers)
45
+ result = Dispatcher.dispatch_request(msg.jr_method,
46
+ :method_args => msg.jr_args,
47
+ :headers => headers,
48
+ :rjr_node_id => @web_node.node_id,
49
+ :rjr_node_type => RJR_NODE_TYPE,
50
+ :rjr_callback => WebNodeCallback.new())
51
+ rescue JSON::ParserError => e
52
+ result = Result.invalid_request
53
+ end
54
+
55
+ msg_id = msg.nil? ? nil : msg.msg_id
56
+ response = ResponseMessage.new(:id => msg_id, :result => result, :headers => headers)
57
+
58
+ resp = EventMachine::DelegatedHttpResponse.new(self)
59
+ #resp.status = response.result.success ? 200 : 500
60
+ resp.status = 200
61
+ resp.content = response.to_s
62
+ resp.content_type "application/json"
63
+ resp.send_response
64
+ end
65
+
66
+ def process_http_request
67
+ # TODO support http protocols other than POST
68
+ # TODO should delete handler threads as they complete & should handle timeout
69
+ msg = @http_post_content.nil? ? '' : @http_post_content
70
+ #@thread_pool << ThreadPoolJob.new { handle_request(msg) }
71
+ handle_request(msg)
72
+ end
73
+
74
+ #def receive_data(data)
75
+ # puts "~~~~ #{data}"
76
+ #end
77
+ end
78
+
79
+ class WebNode < RJR::Node
80
+ # initialize the node w/ the specified params
81
+ def initialize(args = {})
82
+ super(args)
83
+ @host = args[:host]
84
+ @port = args[:port]
85
+ end
86
+
87
+ # Initialize the web subsystem
88
+ def init_node
89
+ end
90
+
91
+ # Instruct Node to start listening for and dispatching rpc requests
92
+ def listen
93
+ em_run do
94
+ init_node
95
+ EventMachine::start_server(@host, @port, WebRequestHandler, self)
96
+ end
97
+ end
98
+
99
+ # Instructs node to send rpc request, and wait for / return response
100
+ def invoke_request(uri, rpc_method, *args)
101
+ init_node
102
+ message = RequestMessage.new :method => rpc_method,
103
+ :args => args,
104
+ :headers => @message_headers
105
+ res = Curl::Easy.http_post uri, message.to_s
106
+ msg = ResponseMessage.new(:message => res.body_str, :headers => @message_headers)
107
+ headers = @message_headers.merge(msg.headers)
108
+ return Dispatcher.handle_response(msg.result)
109
+ end
110
+ end
111
+
112
+ end # module RJR