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.
@@ -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 serializer if they so choose.
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.1.3</td>
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> protocol using EventMachine and provides both a server and a client. <br/>
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
- - Performing and handling a few thousand requests per second (depending on call size, network conditions and the like).
43
- - Configurable retry-on-fail for requests.
44
- - TLS encryption (with peer verification).
45
- - Asynchronous and synchronous requests.
46
- - Handling server-side asynchronous calls that require a block (or any method that passes its result to a block instead of returning it).
47
- - Token-based authentication.
48
- - Primary and secondary (fallback) serializers -- Server will expect the Client to use the primary serializer,
49
- if the Request cannot be parsed using the primary one, it will revert to using the fallback to parse the Request and serialize the Response.
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
- Check out the files in the <i>examples/</i> directory, they go through everything in great detail.<br/>
54
- The tests under <i>spec/arachni/rpc/</i> cover everything too so they can probably help you out.
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
- rake install
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 Github's issue system at
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 File.expand_path( File.dirname( __FILE__ ) ) + '/lib/arachni/rpc/em/version'
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 "Generate docs"
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 "yardoc --verbose --title \
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 "Build the arachni-rpc-em gem."
45
- task :build => [ :clean ] do
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 "Build and install the arachni gem."
50
- task :install => [ :build ] do
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 ]
@@ -14,7 +14,6 @@ require 'fiber'
14
14
  require 'arachni/rpc'
15
15
 
16
16
  require 'yaml'
17
- YAML::ENGINE.yamler = 'syck'
18
17
 
19
18
  dir = File.expand_path( File.dirname( __FILE__ ) )
20
19
  require File.join( dir, 'em', 'connection_utilities' )
@@ -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
- # @author: Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
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
- # Handles EventMachine's connection and RPC related stuff.
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
- @status = :idle
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
- # opts example:
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].to_i
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 which will be passed
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 do not pass a block, the value will be
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 in the form of <i>handler.method</i>
211
- # @param [Array] args collection of arguments to be passed to the method
212
- # @param [Proc] &block
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 connect
228
- ::EM.connect( @host, @port, Handler, @opts )
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
- ::EM.next_tick {
233
- req.callback = block if block_given?
233
+ req.callback = block if block_given?
234
234
 
235
- begin
236
- connect.send_request( req )
237
- rescue ::EM::ConnectionError => e
238
- exc = ConnectionError.new( e.to_s + " for '#{@host}:#{@port}'." )
239
- exc.set_backtrace( e.backtrace )
240
- req.callback.call exc
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
- 'block when your app is running inside the Reactor\'s thread)'
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( msg )
273
+ raise msg
270
274
  end
271
275
  end
272
276