arachni-rpc-em 0.1.2 → 0.1.3dev1

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