arachni-rpc-em 0.1.2 → 0.1.3dev1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # ChangeLog
2
2
 
3
- ## Version 0.1.2 _(Under development)_
3
+ ## Version 0.1.3 (_Under development_)
4
4
 
5
- - Code cleanup
6
- - Client retries on Errno::ECONNREFUSED
5
+ - 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.
7
+ - Client request-retry strategy tweaked to be more resilient.
8
+
9
+ ## Version 0.1.2
10
+
11
+ - Code cleanup.
12
+ - Client retries on Errno::ECONNREFUSED.
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <table>
3
3
  <tr>
4
4
  <th>Version</th>
5
- <td>0.1.2</td>
5
+ <td>0.1.3dev</td>
6
6
  </tr>
7
7
  <tr>
8
8
  <th>Github page</th>
@@ -38,11 +38,14 @@ Arachni-RPC EM is an implementation of the <a href="http://github.com/Arachni/ar
38
38
 
39
39
  It's capable of:
40
40
 
41
- - performing and handling a few thousand requests per second (depending on call size, network conditions and the like)
42
- - TLS encryption (with peer verification)
43
- - asynchronous and synchronous requests
44
- - handling server-side asynchronous calls that require a block (or any method that requires a block in general)
45
- - token-based authentication
41
+ - Performing and handling a few thousand requests per second (depending on call size, network conditions and the like).
42
+ - Configurable retry-on-fail for requests.
43
+ - TLS encryption (with peer verification).
44
+ - Asynchronous and synchronous requests.
45
+ - Handling server-side asynchronous calls that require a block (or any method that passes its result to a block instead of returning it).
46
+ - Token-based authentication.
47
+ - Primary and secondary (fallback) serializers -- Server will expect the Client to use the primary serializer,
48
+ 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.
46
49
 
47
50
  ## Usage
48
51
 
data/Rakefile CHANGED
@@ -19,7 +19,7 @@ rescue LoadError => e
19
19
  puts ' gem install rspec'
20
20
  end
21
21
 
22
- task default: [ :spec ]
22
+ task default: [ :build, :spec ]
23
23
 
24
24
  desc "Generate docs"
25
25
  task :docs do
@@ -48,12 +48,12 @@ end
48
48
 
49
49
  desc "Build and install the arachni gem."
50
50
  task :install => [ :build ] do
51
-
52
51
  sh "gem install arachni-rpc-em-#{Arachni::RPC::EM::VERSION}.gem"
53
52
  end
54
53
 
55
- desc "Push a new version to Gemcutter"
54
+ desc "Push a new version to Rubygems"
56
55
  task :publish => [ :build ] do
57
-
56
+ sh "git tag -a v#{Arachni::RPC::EM::VERSION} -m 'Version #{Arachni::RPC::EM::VERSION}'"
58
57
  sh "gem push arachni-rpc-em-#{Arachni::RPC::EM::VERSION}.gem"
59
58
  end
59
+ task :release => [ :publish ]
@@ -19,19 +19,17 @@ require File.join( File.expand_path( File.dirname( __FILE__ ) ), '../lib/arachni
19
19
 
20
20
  # connect to the server
21
21
  client = Arachni::RPC::EM::Client.new(
22
- :host => 'localhost',
23
- :port => 7332,
22
+ host: 'localhost',
23
+ port: 7332,
24
24
 
25
25
  # optional authentication token, if it doesn't match the one
26
26
  # set on the server-side you'll be getting exceptions.
27
- :token => 'superdupersecret',
28
-
29
- # :keep_alive => false,
27
+ token: 'superdupersecret',
30
28
 
31
29
  # optional serializer (defaults to YAML)
32
30
  # see the 'serializer' method at:
33
31
  # http://eventmachine.rubyforge.org/EventMachine/Protocols/ObjectProtocol.html#M000369
34
- :serializer => Marshal
32
+ serializer: Marshal
35
33
  )
36
34
 
37
35
  bench = Arachni::RPC::RemoteObjectMapper.new( client, 'bench' )
@@ -65,49 +63,14 @@ require File.join( File.expand_path( File.dirname( __FILE__ ) ), '../lib/arachni
65
63
  end
66
64
 
67
65
  # async calls are the same
68
- bench.foo( 'This is an async call... business as usual. :)' ) {
69
- |res|
66
+ bench.foo( 'This is an async call... business as usual. :)' ) do |res|
70
67
  p res
71
68
  # => "This is an async call... business as usual. :)"
72
- }
69
+ end
73
70
 
74
- bench.async_foo( 'This is an async call to an async remote method.' ) {
75
- |res|
71
+ bench.async_foo( 'This is an async call to an async remote method.' ) do |res|
76
72
  p res
77
73
  # => "This is an async call to an async remote method."
78
- }
79
-
80
-
81
- #
82
- # The system uses 2 methods to make calls appear sync:
83
- # - Threads (when the code is *not* run directly inside the Reactor's thread, see example.rb)
84
- # - Fibers (when the code *is* run inside the Reactor's thread, like right here)
85
- #
86
- # For performance reasons callbacks are ::EM.defer'ed.
87
- #
88
- # This means that they're already in their own thread so you don't need
89
- # ::Arachni::RPC::EM::EM::Synchrony.run to perform sync calls from inside callbacks.
90
- #
91
- bench.async_foo( 'Coo-coo' ) {
92
- |res|
93
-
94
- p res
95
- # => "Coo-coo"
96
-
97
- p bench.async_foo( 'Coo-coo 2' )
98
- # => "Coo-coo 2"
99
-
100
- bench.async_foo( 'Coo-coo 3' ) {
101
- |res|
102
-
103
- p res
104
- # => "Coo-coo 3"
105
-
106
- p bench.foo( 'Coo-coo 4' )
107
- # => "Coo-coo 4"
108
- }
109
-
110
- }
111
-
74
+ end
112
75
 
113
76
  end
data/examples/client.rb CHANGED
@@ -14,45 +14,30 @@ require File.join( cwd, '../lib/arachni/rpc/', 'em' )
14
14
 
15
15
  # connect to the server
16
16
  client = Arachni::RPC::EM::Client.new(
17
- :host => 'localhost',
18
- :port => 7332,
17
+ host: 'localhost',
18
+ port: 7332,
19
19
 
20
20
  # optional authentication token, if it doesn't match the one
21
21
  # set on the server-side you'll be getting exceptions.
22
- :token => 'superdupersecret',
22
+ token: 'superdupersecret',
23
23
 
24
24
  # optional serializer (defaults to YAML)
25
25
  # see the 'serializer' method at:
26
26
  # http://eventmachine.rubyforge.org/EventMachine/Protocols/ObjectProtocol.html#M000369
27
- :serializer => Marshal,
28
-
29
- #
30
- # Connection keep alive is set to true by default, this means that
31
- # a single connection will be maintained and all calls will pass
32
- # through it.
33
- # This bypasses a bug in EventMachine and allows you to perform thousands
34
- # of calls without issue.
35
- #
36
- # However, you are responsible for closing the connection when you're done.
37
- #
38
- # If keep alive is set to false then each call will go through its own connection
39
- # and the responsibility for closing that connection falls on Arachni-RPC.
40
- #
41
- # Unfortunately, if you try to make a greater number of calls than your system's
42
- # maximum open file descriptors limit EventMachine will freak-out.
43
27
  #
44
- :keep_alive => false,
28
+ # Will trigger the Server's fallback serializer.
29
+ serializer: YAML,
45
30
 
46
31
  #
47
32
  # In order to enable peer verification one must first provide
48
33
  # the following:
49
34
  #
50
35
  # SSL CA certificate
51
- # :ssl_ca => cwd + '/../spec/pems/cacert.pem',
36
+ # ssl_ca: cwd + '/../spec/pems/cacert.pem',
52
37
  # SSL private key
53
- # :ssl_pkey => cwd + '/../spec/pems/client/key.pem',
38
+ # ssl_pkey: cwd + '/../spec/pems/client/key.pem',
54
39
  # SSL certificate
55
- # :ssl_cert => cwd + '/../spec/pems/client/cert.pem'
40
+ # ssl_cert: cwd + '/../spec/pems/client/cert.pem'
56
41
  )
57
42
 
58
43
  # Make things easy on the eyes using the mapper, it allows you to do this:
@@ -69,9 +54,7 @@ bench = Arachni::RPC::RemoteObjectMapper.new( client, 'bench' )
69
54
  # In order to perform an asynchronous call you will need to provide a block,
70
55
  # even if it is an empty one.
71
56
  #
72
- bench.foo( 'This is an async call to "bench.foo".' ) {
73
- |res|
74
-
57
+ bench.foo( 'This is an async call to "bench.foo".' ) do |res|
75
58
  p res
76
59
  # => "This is an async call to \"bench.foo\"."
77
60
 
@@ -98,7 +81,7 @@ bench.foo( 'This is an async call to "bench.foo".' ) {
98
81
  # was there an authentication token mismatch?
99
82
  # p res.rpc_invalid_token_error?
100
83
  # => false
101
- }
84
+ end
102
85
 
103
86
 
104
87
 
@@ -108,11 +91,10 @@ bench.foo( 'This is an async call to "bench.foo".' ) {
108
91
  # You'll need to kind-of specify the async methods on the server-side,
109
92
  # check the server example file for more info.
110
93
  #
111
- bench.async_foo( 'This is an async call to "bench.async_foo".' ) {
112
- |res|
94
+ bench.async_foo( 'This is an async call to "bench.async_foo".' ) do |res|
113
95
  p res
114
96
  # => "This is an async call to \"bench.async_foo\"."
115
- }
97
+ end
116
98
 
117
99
  p bench.async_foo( 'This is a sync call to "bench.async_foo".' )
118
100
  # => "This is a sync call to \"bench.async_foo\"."
@@ -141,7 +123,7 @@ p bench.foo( 'This is a sync call to "bench.foo".' )
141
123
  blah = Arachni::RPC::RemoteObjectMapper.new( client, 'blah' )
142
124
  begin
143
125
  p blah.something
144
- rescue Exception => e
126
+ rescue => e
145
127
  p e # => #<Arachni::RPC::EM::Exceptions::InvalidObject: Trying to access non-existent object 'blah'.>
146
128
  end
147
129
 
@@ -150,7 +132,7 @@ end
150
132
  #
151
133
  begin
152
134
  p bench.fdoo
153
- rescue Exception => e
135
+ rescue => e
154
136
  p e # => #<Arachni::RPC::EM::Exceptions::InvalidMethod: Trying to access non-public method 'fdoo'.>
155
137
  end
156
138
 
@@ -161,8 +143,7 @@ end
161
143
  # It will *NOT* be thrown!
162
144
  # It will be *RETURNED*!
163
145
  #
164
- blah.something {
165
- |res|
146
+ blah.something do |res|
166
147
  p res # => #<Arachni::RPC::EM::Exceptions::InvalidObject: Trying to access non-existent object 'blah'.>
167
148
 
168
149
  # RPC Exception helper methods have been added to all Ruby objects (except BasicObject)
@@ -170,12 +151,12 @@ blah.something {
170
151
 
171
152
  # p res.rpc_exception? # => true
172
153
  # p res.rpc_invalid_object_error? # => true
173
- }
154
+ end
174
155
 
175
156
  #
176
157
  # We don't know when async calls will return so we wait forever.
177
158
  #
178
159
  # Call ::EM.stop to break-out.
179
160
  #
180
- Arachni::RPC::EM.block!
161
+ Arachni::RPC::EM.block
181
162
 
data/examples/server.rb CHANGED
@@ -13,7 +13,7 @@ require File.join( cwd, '../lib/arachni/rpc/', 'em' )
13
13
 
14
14
  class Parent
15
15
  def foo( arg )
16
- return arg
16
+ arg
17
17
  end
18
18
  end
19
19
 
@@ -38,22 +38,27 @@ class Bench < Parent
38
38
  end
39
39
 
40
40
  server = Arachni::RPC::EM::Server.new(
41
- :host => 'localhost',
42
- :port => 7332,
41
+ host: 'localhost',
42
+ port: 7332,
43
43
 
44
44
  # optional authentication token, if it doesn't match the one
45
45
  # set on the client-side the client won't be able to do anything
46
46
  # and keep getting exceptions.
47
- :token => 'superdupersecret',
47
+ token: 'superdupersecret',
48
48
 
49
49
  # optional serializer (defaults to YAML)
50
50
  # see the 'serializer' method at:
51
51
  # http://eventmachine.rubyforge.org/EventMachine/Protocols/ObjectProtocol.html#M000369
52
- :serializer => Marshal,
52
+ #
53
+ # Use Marshal for performance as the primary serializer.
54
+ serializer: Marshal,
55
+
56
+ # Fallback to YAML for interoperability -- used for requests that can't be parsed by Marshal.load.
57
+ fallback_serializer: YAML,
53
58
 
54
- # :ssl_ca => cwd + '/../spec/pems/cacert.pem',
55
- # :ssl_pkey => cwd + '/../spec/pems/server/key.pem',
56
- # :ssl_cert => cwd + '/../spec/pems/server/cert.pem'
59
+ # ssl_ca: cwd + '/../spec/pems/cacert.pem',
60
+ # ssl_pkey: cwd + '/../spec/pems/server/key.pem',
61
+ # ssl_cert: cwd + '/../spec/pems/server/cert.pem'
57
62
  )
58
63
 
59
64
  #
@@ -64,15 +69,14 @@ server = Arachni::RPC::EM::Server.new(
64
69
  # you can just decide dynamically based on a plethora of data which Ruby provides
65
70
  # by its 'Method' class.
66
71
  #
67
- server.add_async_check {
68
- |method|
72
+ server.add_async_check do |method|
69
73
  #
70
74
  # Must return 'true' for async and 'false' for sync.
71
75
  #
72
76
  # Very simple check here...
73
77
  #
74
- 'async' == method.name.to_s.split( '_' )[0]
75
- }
78
+ method.name.to_s.start_with? 'async_'
79
+ end
76
80
 
77
81
  server.add_handler( 'bench', Bench.new )
78
82
 
@@ -14,7 +14,8 @@ module EM
14
14
  # Simple EventMachine-based RPC client.
15
15
  #
16
16
  # It's capable of:
17
- # - performing and handling a few thousands requests per second (depending on call size, network conditions and the like)
17
+ # - performing and handling a few thousands requests per second (depending on
18
+ # call size, network conditions and the like)
18
19
  # - TLS encryption
19
20
  # - asynchronous and synchronous requests
20
21
  # - handling remote asynchronous calls that require a block
@@ -39,10 +40,12 @@ class Client
39
40
  DEFAULT_TRIES = 9
40
41
 
41
42
  def initialize( opts )
42
- @opts = opts
43
+ @opts = opts.dup
44
+
43
45
  @max_retries = @opts[:max_retries] || DEFAULT_TRIES
46
+
44
47
  @opts[:tries] ||= 0
45
- @tries = @opts[:tries]
48
+ @tries ||= @opts[:tries]
46
49
 
47
50
  @status = :idle
48
51
 
@@ -58,14 +61,15 @@ class Client
58
61
  def unbind( reason )
59
62
  end_ssl
60
63
 
61
- if @request && @request.callback && status != :done
62
- if reason == Errno::ECONNREFUSED && retry?
64
+ if @request && @request.callback && !done?
65
+ if retry? #&& reason == Errno::ECONNREFUSED
63
66
  retry_request
64
67
  else
65
68
  e = Arachni::RPC::Exceptions::ConnectionError.new( "Connection closed [#{reason}]" )
66
69
  @request.callback.call( e )
67
70
  end
68
71
  end
72
+
69
73
  @status = :closed
70
74
  end
71
75
 
@@ -77,42 +81,37 @@ class Client
77
81
  @status
78
82
  end
79
83
 
84
+ def done?
85
+ !!@done
86
+ end
87
+
80
88
  #
81
89
  # Used to handle responses.
82
90
  #
83
91
  # @param [Arachni::RPC::EM::Response] res
84
92
  #
85
93
  def receive_response( res )
86
- @status = :done
87
-
88
94
  if exception?( res )
89
95
  res.obj = Arachni::RPC::Exceptions.from_response( res )
90
96
  end
91
97
 
92
-
93
- if cb = @request.callback
94
-
95
- callback = Proc.new do |obj|
96
- cb.call( obj )
97
- close_connection
98
- end
99
-
100
- if @request.defer?
101
- # the callback might block a bit so tell EM to put it in a thread
102
- ::EM.defer { callback.call( res.obj ) }
103
- else
104
- callback.call( res.obj )
105
- end
106
- end
98
+ @request.callback.call( res.obj ) if @request.callback
99
+ ensure
100
+ @done = true
101
+ @status = :done
102
+ close_connection
107
103
  end
108
104
 
109
105
  def retry_request
110
106
  opts = @opts.dup
111
107
  opts[:tries] += 1
112
- EventMachine::Timer.new( 0.1 ){
113
- sleep( 0.1 )
114
- close_connection
115
- ::EM.connect( opts[:host], opts[:port], self.class, opts ).send_request( @request )
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
+ }
116
115
  }
117
116
  end
118
117
 
@@ -131,9 +130,9 @@ class Client
131
130
  # @param [Arachni::RPC::EM::Request] req
132
131
  #
133
132
  def send_request( req )
134
- @status = :pending
135
133
  @request = req
136
134
  super( req )
135
+ @status = :pending
137
136
  end
138
137
  end
139
138
 
@@ -162,6 +161,9 @@ class Client
162
161
  # # http://eventmachine.rubyforge.org/EventMachine/Protocols/ObjectProtocol.html#M000369
163
162
  # :serializer => Marshal,
164
163
  #
164
+ # # serializer to use if the first choice fails
165
+ # :fallback_serializer => YAML,
166
+ #
165
167
  # :max_retries => 0,
166
168
  #
167
169
  # #
@@ -179,18 +181,12 @@ class Client
179
181
  # @param [Hash] opts
180
182
  #
181
183
  def initialize( opts )
182
- begin
183
- @opts = opts.merge( role: :client )
184
- @token = @opts[:token]
184
+ @opts = opts.merge( role: :client )
185
+ @token = @opts[:token]
185
186
 
186
- @host, @port = @opts[:host], @opts[:port]
187
+ @host, @port = @opts[:host], @opts[:port].to_i
187
188
 
188
- Arachni::RPC::EM.ensure_em_running
189
- rescue EventMachine::ConnectionError => e
190
- exc = ConnectionError.new( e.to_s + " for '#{@k}'." )
191
- exc.set_backtrace( e.backtrace )
192
- raise exc
193
- end
189
+ Arachni::RPC::EM.ensure_em_running
194
190
  end
195
191
 
196
192
  #
@@ -201,10 +197,9 @@ class Client
201
197
  # To perform an async call you need to provide a block which will be passed
202
198
  # the return value once the method has finished executing.
203
199
  #
204
- # server.call( 'handler.method', arg1, arg2 ){
205
- # |res|
200
+ # server.call( 'handler.method', arg1, arg2 ) do |res|
206
201
  # do_stuff( res )
207
- # }
202
+ # end
208
203
  #
209
204
  #
210
205
  # To perform a sync (blocking) call do not pass a block, the value will be
@@ -224,11 +219,7 @@ class Client
224
219
  token: @token
225
220
  )
226
221
 
227
- if block_given?
228
- call_async( req )
229
- else
230
- call_sync( req )
231
- end
222
+ block_given? ? call_async( req ) : call_sync( req )
232
223
  end
233
224
 
234
225
  private
@@ -238,7 +229,7 @@ class Client
238
229
  end
239
230
 
240
231
  def call_async( req, &block )
241
- ::EM.schedule {
232
+ ::EM.next_tick {
242
233
  req.callback = block if block_given?
243
234
  connect.send_request( req )
244
235
  }
@@ -257,10 +248,6 @@ class Client
257
248
  end
258
249
  sleep
259
250
  else
260
- # Fibers do not work across threads so don't defer the callback
261
- # once the Handler gets to it
262
- req.do_not_defer
263
-
264
251
  f = Fiber.current
265
252
  call_async( req ) { |obj| f.resume( obj ) }
266
253