arachni-rpc-em 0.1.3 → 0.2
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.
- 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
|
|