rjr 0.5.3

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,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