arachni-rpc-em 0.1.3 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +17 -1
- data/README.md +26 -15
- data/Rakefile +12 -31
- data/lib/arachni/rpc/em.rb +0 -1
- data/lib/arachni/rpc/em/client.rb +149 -145
- data/lib/arachni/rpc/em/client/handler.rb +180 -0
- data/lib/arachni/rpc/em/connection_utilities.rb +1 -3
- data/lib/arachni/rpc/em/em.rb +3 -9
- data/lib/arachni/rpc/em/protocol.rb +59 -70
- data/lib/arachni/rpc/em/server.rb +75 -185
- data/lib/arachni/rpc/em/server/handler.rb +161 -0
- data/lib/arachni/rpc/em/version.rb +1 -1
- data/spec/arachni/rpc/em/client_spec.rb +165 -55
- data/spec/arachni/rpc/em/em_spec.rb +1 -1
- data/spec/arachni/rpc/em/server_spec.rb +82 -30
- data/spec/arachni/rpc/em/ssl_spec.rb +1 -1
- data/spec/servers/server.rb +23 -14
- data/spec/servers/unix_socket.rb +11 -0
- data/spec/servers/with_fallback.rb +2 -3
- data/spec/servers/with_ssl_primitives.rb +3 -0
- data/spec/spec_helper.rb +6 -8
- metadata +45 -2
data/CHANGELOG.md
CHANGED
@@ -1,9 +1,25 @@
|
|
1
1
|
# ChangeLog
|
2
2
|
|
3
|
+
## Version 0.2 _(June 23, 2013)_
|
4
|
+
|
5
|
+
- YAML engine no longer forced to _Syck_.
|
6
|
+
- Added support for UNIX domain sockets.
|
7
|
+
- `Client`
|
8
|
+
- Moved connection handler to its own class file.
|
9
|
+
- Updated to reuse connections whenever possible.
|
10
|
+
- Maintains an adjustable-sized connection pool.
|
11
|
+
- Uses a single connection by default.
|
12
|
+
- `Server`
|
13
|
+
- Moved connection handler to its own class file.
|
14
|
+
- Removed connection inactivity timeout.
|
15
|
+
- Cleaned up RSpec tests.
|
16
|
+
- Added Bundler files.
|
17
|
+
|
3
18
|
## Version 0.1.3 _(April 15, 2013)_
|
4
19
|
|
5
20
|
- Stopped client callbacks from being deferred.
|
6
|
-
- Server now supports a fallback serializer to allow clients to use a secondary
|
21
|
+
- Server now supports a fallback serializer to allow clients to use a secondary
|
22
|
+
serializer if they so choose.
|
7
23
|
- Client request-retry strategy tweaked to be more resilient.
|
8
24
|
|
9
25
|
## Version 0.1.2
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
<table>
|
4
4
|
<tr>
|
5
5
|
<th>Version</th>
|
6
|
-
<td>0.
|
6
|
+
<td>0.2</td>
|
7
7
|
</tr>
|
8
8
|
<tr>
|
9
9
|
<th>Github page</th>
|
@@ -33,25 +33,33 @@
|
|
33
33
|
|
34
34
|
## Synopsis
|
35
35
|
|
36
|
-
Arachni-RPC EM is an implementation of the <a href="http://github.com/Arachni/arachni-rpc">Arachni-RPC</a>
|
36
|
+
Arachni-RPC EM is an implementation of the <a href="http://github.com/Arachni/arachni-rpc">Arachni-RPC</a>
|
37
|
+
protocol using EventMachine and provides both a server and a client. <br/>
|
37
38
|
|
38
39
|
## Features
|
39
40
|
|
40
41
|
It's capable of:
|
41
42
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
43
|
+
- Performing and handling a few thousand requests per second (depending on call
|
44
|
+
size, network conditions and the like).
|
45
|
+
- Operating over TCP/IP and UNIX domain sockets.
|
46
|
+
- Configurable retry-on-failure for requests.
|
47
|
+
- Keep-alive and connection re-use.
|
48
|
+
- TLS encryption (with peer verification).
|
49
|
+
- Asynchronous and synchronous requests.
|
50
|
+
- Handling server-side asynchronous calls that require a block (or any method
|
51
|
+
that passes its result to a block instead of returning it).
|
52
|
+
- Token-based authentication.
|
53
|
+
- Primary and secondary/fallback serializers
|
54
|
+
- Server will expect the Client to use the primary serializer, if the Request
|
55
|
+
cannot be parsed using the primary one, it will revert to using the
|
56
|
+
fallback to parse the Request and serialize the Response.
|
50
57
|
|
51
58
|
## Usage
|
52
59
|
|
53
|
-
|
54
|
-
|
60
|
+
The files in the `examples/` directory go through everything in great detail.
|
61
|
+
Also, the tests under `spec/arachni/rpc/` cover everything too so they can
|
62
|
+
provide you with hints.
|
55
63
|
|
56
64
|
## Installation
|
57
65
|
|
@@ -65,15 +73,18 @@ If you want to clone the repository and work with the source code:
|
|
65
73
|
|
66
74
|
git co git://github.com/arachni/arachni-rpc-em.git
|
67
75
|
cd arachni-rpc-em
|
68
|
-
|
76
|
+
bundle install
|
69
77
|
|
70
78
|
## Running the Specs
|
71
79
|
|
72
|
-
rake spec
|
80
|
+
bundle exec rake spec
|
81
|
+
|
82
|
+
**Warning**: Some of the test cases include stress-testing, don't be alarmed
|
83
|
+
when RAM usage hits 5GB and CPU utilization hits 100%.
|
73
84
|
|
74
85
|
## Bug reports/Feature requests
|
75
86
|
|
76
|
-
Please send your feedback using
|
87
|
+
Please send your feedback using GitHub's issue system at
|
77
88
|
[http://github.com/arachni/arachni-rpc-em/issues](http://github.com/arachni/arachni-rpc-em/issues).
|
78
89
|
|
79
90
|
|
data/Rakefile
CHANGED
@@ -7,7 +7,10 @@
|
|
7
7
|
=end
|
8
8
|
|
9
9
|
require 'rubygems'
|
10
|
-
require
|
10
|
+
require 'bundler'
|
11
|
+
require_relative 'lib/arachni/rpc/em/version'
|
12
|
+
|
13
|
+
Bundler::GemHelper.install_tasks
|
11
14
|
|
12
15
|
begin
|
13
16
|
require 'rspec'
|
@@ -21,39 +24,17 @@ end
|
|
21
24
|
|
22
25
|
task default: [ :build, :spec ]
|
23
26
|
|
24
|
-
desc
|
27
|
+
desc 'Generate docs'
|
25
28
|
task :docs do
|
29
|
+
outdir = '../arachni-rpc-em-docs'
|
26
30
|
|
27
|
-
outdir = "../arachni-rpc-pages"
|
28
31
|
sh "mkdir #{outdir}" if !File.directory?( outdir )
|
29
|
-
|
30
|
-
sh
|
31
|
-
\"Arachni-RPC\" \
|
32
|
-
lib/* -o #{outdir} \
|
33
|
-
- CHANGELOG.md LICENSE.md"
|
34
|
-
|
35
|
-
|
36
|
-
sh "rm -rf .yard*"
|
37
|
-
end
|
38
|
-
|
39
|
-
desc "Cleaning..."
|
40
|
-
task :clean do
|
41
|
-
sh "rm *.gem || true"
|
32
|
+
sh "yardoc -o #{outdir}"
|
33
|
+
sh 'rm -rf .yardoc'
|
42
34
|
end
|
43
35
|
|
44
|
-
desc
|
45
|
-
task :
|
46
|
-
sh "gem build arachni-rpc-em.gemspec"
|
47
|
-
end
|
36
|
+
desc 'Push a new version to RubyGems'
|
37
|
+
task :publish => [ :release ]
|
48
38
|
|
49
|
-
desc
|
50
|
-
task :
|
51
|
-
sh "gem install arachni-rpc-em-#{Arachni::RPC::EM::VERSION}.gem"
|
52
|
-
end
|
53
|
-
|
54
|
-
desc "Push a new version to Rubygems"
|
55
|
-
task :publish => [ :build ] do
|
56
|
-
sh "git tag -a v#{Arachni::RPC::EM::VERSION} -m 'Version #{Arachni::RPC::EM::VERSION}'"
|
57
|
-
sh "gem push arachni-rpc-em-#{Arachni::RPC::EM::VERSION}.gem"
|
58
|
-
end
|
59
|
-
task :release => [ :publish ]
|
39
|
+
desc 'Build Arachni and run all the tests.'
|
40
|
+
task :default => [ :build, :spec ]
|
data/lib/arachni/rpc/em.rb
CHANGED
@@ -10,143 +10,36 @@ module Arachni
|
|
10
10
|
module RPC
|
11
11
|
module EM
|
12
12
|
|
13
|
+
require_relative 'client/handler'
|
14
|
+
|
13
15
|
#
|
14
16
|
# Simple EventMachine-based RPC client.
|
15
17
|
#
|
16
18
|
# It's capable of:
|
17
|
-
# - performing and handling a few thousands requests per second (depending on
|
18
|
-
# call size, network conditions and the like)
|
19
|
-
# - TLS encryption
|
20
|
-
# - asynchronous and synchronous requests
|
21
|
-
# - handling remote asynchronous calls that require a block
|
22
19
|
#
|
23
|
-
#
|
20
|
+
# * Performing and handling a few thousands requests per second (depending on
|
21
|
+
# call size, network conditions and the like)
|
22
|
+
# * TLS encryption
|
23
|
+
# * Asynchronous and synchronous requests
|
24
|
+
# * Handling remote asynchronous calls that require a block
|
25
|
+
#
|
26
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
24
27
|
#
|
25
28
|
class Client
|
26
29
|
include ::Arachni::RPC::Exceptions
|
27
30
|
|
28
|
-
#
|
29
|
-
|
30
|
-
#
|
31
|
-
# It's responsible for TLS, storing and calling callbacks as well as
|
32
|
-
# serializing, transmitting and receiving objects.
|
33
|
-
#
|
34
|
-
# @author: Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
35
|
-
#
|
36
|
-
class Handler < EventMachine::Connection
|
37
|
-
include ::Arachni::RPC::EM::Protocol
|
38
|
-
include ::Arachni::RPC::EM::ConnectionUtilities
|
39
|
-
|
40
|
-
DEFAULT_TRIES = 9
|
41
|
-
|
42
|
-
def initialize( opts )
|
43
|
-
@opts = opts.dup
|
44
|
-
|
45
|
-
@max_retries = @opts[:max_retries] || DEFAULT_TRIES
|
46
|
-
|
47
|
-
@opts[:tries] ||= 0
|
48
|
-
@tries ||= @opts[:tries]
|
31
|
+
# Default amount of connections to maintain in the re-use pool.
|
32
|
+
DEFAULT_CONNECTION_POOL_SIZE = 1
|
49
33
|
|
50
|
-
|
51
|
-
|
52
|
-
@request = nil
|
53
|
-
assume_client_role!
|
54
|
-
end
|
55
|
-
|
56
|
-
def post_init
|
57
|
-
@status = :active
|
58
|
-
start_ssl
|
59
|
-
end
|
60
|
-
|
61
|
-
def unbind( reason )
|
62
|
-
end_ssl
|
63
|
-
|
64
|
-
if @request && @request.callback && !done?
|
65
|
-
if retry? #&& reason == Errno::ECONNREFUSED
|
66
|
-
retry_request
|
67
|
-
else
|
68
|
-
e = Arachni::RPC::Exceptions::ConnectionError.new( "Connection closed [#{reason}]" )
|
69
|
-
@request.callback.call( e )
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
@status = :closed
|
74
|
-
end
|
75
|
-
|
76
|
-
def connection_completed
|
77
|
-
@status = :established
|
78
|
-
end
|
79
|
-
|
80
|
-
def status
|
81
|
-
@status
|
82
|
-
end
|
83
|
-
|
84
|
-
def done?
|
85
|
-
!!@done
|
86
|
-
end
|
87
|
-
|
88
|
-
#
|
89
|
-
# Used to handle responses.
|
90
|
-
#
|
91
|
-
# @param [Arachni::RPC::EM::Response] res
|
92
|
-
#
|
93
|
-
def receive_response( res )
|
94
|
-
if exception?( res )
|
95
|
-
res.obj = Arachni::RPC::Exceptions.from_response( res )
|
96
|
-
end
|
97
|
-
|
98
|
-
@request.callback.call( res.obj ) if @request.callback
|
99
|
-
ensure
|
100
|
-
@done = true
|
101
|
-
@status = :done
|
102
|
-
close_connection
|
103
|
-
end
|
104
|
-
|
105
|
-
def retry_request
|
106
|
-
opts = @opts.dup
|
107
|
-
opts[:tries] += 1
|
108
|
-
|
109
|
-
@tries += 1
|
110
|
-
::EM.next_tick {
|
111
|
-
::EM::Timer.new( 0.2 ) {
|
112
|
-
::EM.connect( opts[:host], opts[:port], self.class, opts ).
|
113
|
-
send_request( @request )
|
114
|
-
}
|
115
|
-
}
|
116
|
-
end
|
117
|
-
|
118
|
-
def retry?
|
119
|
-
@tries < @max_retries
|
120
|
-
end
|
121
|
-
|
122
|
-
# @param [Arachni::RPC::EM::Response] res
|
123
|
-
def exception?( res )
|
124
|
-
res.obj.is_a?( Hash ) && res.obj['exception'] ? true : false
|
125
|
-
end
|
126
|
-
|
127
|
-
#
|
128
|
-
# Sends the request.
|
129
|
-
#
|
130
|
-
# @param [Arachni::RPC::EM::Request] req
|
131
|
-
#
|
132
|
-
def send_request( req )
|
133
|
-
@request = req
|
134
|
-
super( req )
|
135
|
-
@status = :pending
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
#
|
140
|
-
# Options hash
|
141
|
-
#
|
142
|
-
# @return [Hash]
|
143
|
-
#
|
34
|
+
# @return [Hash] Options hash.
|
144
35
|
attr_reader :opts
|
145
36
|
|
37
|
+
attr_reader :connection_count
|
38
|
+
|
146
39
|
#
|
147
40
|
# Starts EventMachine and connects to the remote server.
|
148
41
|
#
|
149
|
-
#
|
42
|
+
# @example Example options:
|
150
43
|
#
|
151
44
|
# {
|
152
45
|
# :host => 'localhost',
|
@@ -179,37 +72,143 @@ class Client
|
|
179
72
|
# }
|
180
73
|
#
|
181
74
|
# @param [Hash] opts
|
75
|
+
# @option opts [String] :host Hostname/IP address.
|
76
|
+
# @option opts [Integer] :port Port number.
|
77
|
+
# @option opts [String] :socket Path to UNIX domain socket.
|
78
|
+
# @option opts [Integer] :connection_pool_size (1)
|
79
|
+
# Amount of connections to keep open.
|
80
|
+
# @option opts [String] :token Optional authentication token.
|
81
|
+
# @option opts [.dump, .load] :serializer (YAML)
|
82
|
+
# Serializer to use for message transmission.
|
83
|
+
# @option opts [.dump, .load] :fallback_serializer
|
84
|
+
# Optional fallback serializer to be used when the primary one fails.
|
85
|
+
# @option opts [Integer] :max_retries
|
86
|
+
# How many times to retry failed requests.
|
87
|
+
# @option opts [String] :ssl_ca SSL CA certificate.
|
88
|
+
# @option opts [String] :ssl_pkey SSL private key.
|
89
|
+
# @option opts [String] :ssl_cert SSL certificate.
|
182
90
|
#
|
183
91
|
def initialize( opts )
|
184
92
|
@opts = opts.merge( role: :client )
|
185
93
|
@token = @opts[:token]
|
186
94
|
|
187
|
-
@host, @port = @opts[:host], @opts[:port]
|
95
|
+
@host, @port = @opts[:host], @opts[:port]
|
96
|
+
@socket = @opts[:socket]
|
97
|
+
|
98
|
+
if !@socket && !(@host || @port)
|
99
|
+
fail ArgumentError, 'Needs either a :socket or :host and :port options.'
|
100
|
+
end
|
101
|
+
|
102
|
+
@port = @port.to_i
|
103
|
+
|
104
|
+
if @host && @port <= 0
|
105
|
+
fail ArgumentError, "Invalid port: #{@port}"
|
106
|
+
end
|
107
|
+
|
108
|
+
if @socket && !File.exist?( @socket )
|
109
|
+
fail ArgumentError, "Socket path not valid: #{@socket}"
|
110
|
+
end
|
111
|
+
|
112
|
+
@pool_size = @opts[:connection_pool_size] || DEFAULT_CONNECTION_POOL_SIZE
|
113
|
+
|
114
|
+
@connections = ::EM::Queue.new
|
115
|
+
@connection_count = 0
|
188
116
|
|
189
117
|
Arachni::RPC::EM.ensure_em_running
|
190
118
|
end
|
191
119
|
|
120
|
+
# Connection factory, will re-use or create new connections as needed to
|
121
|
+
# accommodate the workload.
|
122
|
+
#
|
123
|
+
# @param [Block] block Block to be passed a {Handler connection}.
|
124
|
+
#
|
125
|
+
# @return [Boolean]
|
126
|
+
# `true` if a new connection had to be established, `false` if an existing
|
127
|
+
# one was re-used.
|
128
|
+
def connect( &block )
|
129
|
+
if @connections.empty? && @connection_count < @pool_size
|
130
|
+
#p 'NEW'
|
131
|
+
#p connection_count
|
132
|
+
begin
|
133
|
+
opts = @socket ? @socket : [@host, @port]
|
134
|
+
block.call ::EM.connect( *[opts, Handler, @opts.merge( client: self )].flatten )
|
135
|
+
increment_connection_counter
|
136
|
+
rescue => e
|
137
|
+
block.call e
|
138
|
+
end
|
139
|
+
return true
|
140
|
+
end
|
141
|
+
|
142
|
+
pop_block = proc do |conn|
|
143
|
+
# Some connections may have died while they were waiting in the
|
144
|
+
# queue, get rid of them and start all over in case the queue has
|
145
|
+
# been emptied.
|
146
|
+
if !conn.done?
|
147
|
+
#p 'NOT DONE'
|
148
|
+
#p connection_count
|
149
|
+
connection_failed conn
|
150
|
+
connect( &block )
|
151
|
+
next
|
152
|
+
end
|
153
|
+
|
154
|
+
block.call conn
|
155
|
+
end
|
156
|
+
|
157
|
+
#p 'REUSE'
|
158
|
+
#p connection_count
|
159
|
+
#p @connections.size
|
160
|
+
@connections.pop( &pop_block )
|
161
|
+
|
162
|
+
false
|
163
|
+
end
|
164
|
+
|
165
|
+
def increment_connection_counter
|
166
|
+
@connection_count += 1
|
167
|
+
end
|
168
|
+
|
169
|
+
# {Handler#done? Finished} {Handler}s push themselves here to be re-used.
|
170
|
+
#
|
171
|
+
# @param [Handler] connection
|
172
|
+
def push_connection( connection )
|
173
|
+
#p 'PUSHING BACK'
|
174
|
+
#p connection_count
|
175
|
+
#p @connections.size
|
176
|
+
#return if @pool_size <= 0 || @connections.size > @pool_size
|
177
|
+
@connections << connection
|
178
|
+
end
|
179
|
+
|
180
|
+
# Handles failed connections.
|
181
|
+
#
|
182
|
+
# @param [Handler] connection
|
183
|
+
def connection_failed( connection )
|
184
|
+
#p 'CON FAILED'
|
185
|
+
#p connection_count
|
186
|
+
#p @connections.size
|
187
|
+
@connection_count -= 1
|
188
|
+
connection.close_without_retry
|
189
|
+
end
|
190
|
+
|
192
191
|
#
|
193
192
|
# Calls a remote method and grabs the result.
|
194
193
|
#
|
195
194
|
# There are 2 ways to perform a call, async (non-blocking) and sync (blocking).
|
196
195
|
#
|
197
|
-
# To perform an async call you need to provide a block
|
198
|
-
# the return value once the method has finished executing.
|
196
|
+
# @example To perform an async call you need to provide a block to handle the result.
|
199
197
|
#
|
200
198
|
# server.call( 'handler.method', arg1, arg2 ) do |res|
|
201
199
|
# do_stuff( res )
|
202
200
|
# end
|
203
201
|
#
|
204
202
|
#
|
205
|
-
# To perform a sync (blocking) call
|
206
|
-
# returned as usual.
|
203
|
+
# @example To perform a sync (blocking), call without a block.
|
207
204
|
#
|
208
205
|
# res = server.call( 'handler.method', arg1, arg2 )
|
209
206
|
#
|
210
|
-
# @param [String] msg
|
211
|
-
#
|
212
|
-
# @param [
|
207
|
+
# @param [String] msg
|
208
|
+
# RPC message in the form of `handler.method`.
|
209
|
+
# @param [Array] args
|
210
|
+
# Collection of arguments to be passed to the method.
|
211
|
+
# @param [Block] block
|
213
212
|
#
|
214
213
|
def call( msg, *args, &block )
|
215
214
|
req = Request.new(
|
@@ -224,22 +223,26 @@ class Client
|
|
224
223
|
|
225
224
|
private
|
226
225
|
|
227
|
-
def
|
228
|
-
|
226
|
+
def set_exception( req, e )
|
227
|
+
exc = ConnectionError.new( e.to_s + " for '#{@host}:#{@port}'." )
|
228
|
+
exc.set_backtrace e.backtrace
|
229
|
+
req.callback.call exc
|
229
230
|
end
|
230
231
|
|
231
232
|
def call_async( req, &block )
|
232
|
-
|
233
|
-
req.callback = block if block_given?
|
233
|
+
req.callback = block if block_given?
|
234
234
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
235
|
+
begin
|
236
|
+
connect do |connection|
|
237
|
+
if connection.is_a? Exception
|
238
|
+
set_exception( req, connection )
|
239
|
+
next
|
240
|
+
end
|
241
|
+
connection.send_request( req )
|
241
242
|
end
|
242
|
-
|
243
|
+
rescue => e
|
244
|
+
set_exception( req, e )
|
245
|
+
end
|
243
246
|
end
|
244
247
|
|
245
248
|
def call_sync( req )
|
@@ -253,6 +256,7 @@ class Client
|
|
253
256
|
t.wakeup
|
254
257
|
ret = obj
|
255
258
|
end
|
259
|
+
raise ret if ret.is_a?( Exception )
|
256
260
|
sleep
|
257
261
|
else
|
258
262
|
f = Fiber.current
|
@@ -262,11 +266,11 @@ class Client
|
|
262
266
|
ret = Fiber.yield
|
263
267
|
rescue FiberError => e
|
264
268
|
msg = e.to_s + "\n"
|
265
|
-
msg += '(Consider wrapping your sync code in a'
|
266
|
-
' "::Arachni::RPC::EM::Synchrony.run" '
|
267
|
-
'
|
269
|
+
msg += '(Consider wrapping your sync code in a' <<
|
270
|
+
' "::Arachni::RPC::EM::Synchrony.run" block when your app ' <<
|
271
|
+
'is running inside the Reactor\'s thread)'
|
268
272
|
|
269
|
-
raise
|
273
|
+
raise msg
|
270
274
|
end
|
271
275
|
end
|
272
276
|
|