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.
@@ -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