protobuf 1.0.1 → 1.1.0.beta0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.gitignore +3 -0
  2. data/.yardopts +5 -0
  3. data/Gemfile.lock +25 -10
  4. data/bin/rpc_server +38 -33
  5. data/lib/protobuf.rb +22 -3
  6. data/lib/protobuf/common/logger.rb +6 -8
  7. data/lib/protobuf/compiler/visitors.rb +8 -9
  8. data/lib/protobuf/descriptor/descriptor_builder.rb +6 -6
  9. data/lib/protobuf/ext/eventmachine.rb +2 -4
  10. data/lib/protobuf/message/message.rb +1 -3
  11. data/lib/protobuf/rpc/buffer.rb +6 -6
  12. data/lib/protobuf/rpc/client.rb +59 -21
  13. data/lib/protobuf/rpc/connector.rb +10 -9
  14. data/lib/protobuf/rpc/connectors/base.rb +23 -8
  15. data/lib/protobuf/rpc/connectors/common.rb +155 -0
  16. data/lib/protobuf/rpc/connectors/em_client.rb +23 -192
  17. data/lib/protobuf/rpc/connectors/eventmachine.rb +36 -44
  18. data/lib/protobuf/rpc/connectors/socket.rb +58 -1
  19. data/lib/protobuf/rpc/error.rb +6 -14
  20. data/lib/protobuf/rpc/server.rb +72 -99
  21. data/lib/protobuf/rpc/servers/evented_runner.rb +32 -0
  22. data/lib/protobuf/rpc/servers/evented_server.rb +29 -0
  23. data/lib/protobuf/rpc/servers/socket_runner.rb +17 -0
  24. data/lib/protobuf/rpc/servers/socket_server.rb +145 -0
  25. data/lib/protobuf/rpc/service.rb +50 -51
  26. data/lib/protobuf/rpc/stat.rb +2 -2
  27. data/lib/protobuf/version.rb +1 -1
  28. data/protobuf.gemspec +9 -4
  29. data/spec/helper/all.rb +1 -7
  30. data/spec/helper/server.rb +45 -5
  31. data/spec/helper/silent_constants.rb +40 -0
  32. data/spec/proto/test_service.rb +0 -1
  33. data/spec/proto/test_service_impl.rb +4 -3
  34. data/spec/spec_helper.rb +19 -6
  35. data/spec/unit/enum_spec.rb +4 -4
  36. data/spec/unit/rpc/client_spec.rb +32 -42
  37. data/spec/unit/rpc/connector_spec.rb +11 -16
  38. data/spec/unit/rpc/connectors/base_spec.rb +14 -3
  39. data/spec/unit/rpc/connectors/common_spec.rb +132 -0
  40. data/spec/unit/rpc/connectors/{eventmachine/client_spec.rb → eventmachine_client_spec.rb} +0 -0
  41. data/spec/unit/rpc/connectors/socket_spec.rb +49 -0
  42. data/spec/unit/rpc/servers/evented_server_spec.rb +18 -0
  43. data/spec/unit/rpc/servers/socket_server_spec.rb +57 -0
  44. metadata +86 -16
  45. data/spec/unit/rpc/server_spec.rb +0 -27
@@ -6,79 +6,71 @@ module Protobuf
6
6
  module Connectors
7
7
  class EventMachine < Base
8
8
 
9
- def initialize options
10
- super(EMClient::DEFAULT_OPTIONS.merge(options))
11
- end
12
-
13
9
  def send_request
14
- Thread.new { EM.run } unless EM.reactor_running?
10
+ ensure_em_running do
11
+ f = Fiber.current
12
+
13
+ EM.schedule do
14
+ log_debug "[#{log_signature}] Scheduling EventMachine client request to be created on next tick"
15
+ cnxn = EMClient.connect(options, &ensure_cb)
16
+ cnxn.on_success(&success_cb) if success_cb
17
+ cnxn.on_failure(&ensure_cb)
18
+ cnxn.on_complete { resume_fiber(f) } unless async?
19
+ log_debug "[#{log_signature}] Connection scheduled"
20
+ end
15
21
 
16
- f = Fiber.current
17
-
18
- EM.schedule do
19
- log_debug '[client] Scheduling EventMachine client request to be created on next tick'
20
- cnxn = EMClient.connect options, &ensure_cb
21
- cnxn.on_success &success_cb if success_cb
22
- cnxn.on_failure &ensure_cb
23
- cnxn.on_complete { resume_fiber f } unless async?
24
- log_debug '[client] Connection scheduled'
22
+ async? ? true : set_timeout_and_validate_fiber
25
23
  end
26
-
27
- return set_timeout_and_validate_fiber unless async?
28
- return true
29
24
  end
30
25
 
31
- # Returns a proc that ensures any errors will be returned to the client
26
+ # Returns a callable that ensures any errors will be returned to the client
32
27
  #
33
28
  # If a failure callback was set, just use that as a direct assignment
34
29
  # otherwise implement one here that simply throws an exception, since we
35
30
  # don't want to swallow the black holes.
36
31
  #
37
32
  def ensure_cb
38
- @ensure_cb ||= begin
39
- cbk = nil
40
- if @failure_cb
41
- cbk = @failure_cb
42
- else
43
- cbk = proc do |error|
44
- raise '%s: %s' % [error.code.name, error.message]
45
- end
46
- end
47
- cbk
48
- end
33
+ @ensure_cb ||= (@failure_cb || lambda { |error| raise '%s: %s' % [error.code.name, error.message] } )
34
+ end
35
+
36
+ def log_signature
37
+ @log_signature ||= "client-#{self.class}"
49
38
  end
50
39
 
51
40
  private
52
41
 
53
- def set_timeout_and_validate_fiber
54
- @timeout_timer = EM::add_timer(@options[:timeout]) do
55
- message = 'Client timeout of %d seconds expired' % @options[:timeout]
56
- err = ClientError.new(Protobuf::Socketrpc::ErrorReason::RPC_ERROR, message)
57
- ensure_cb.call(err)
58
- end
59
-
60
- Fiber.yield
61
- rescue FiberError
62
- message = "Synchronous calls must be in 'EM.fiber_run' block"
63
- err = ClientError.new(Protobuf::Socketrpc::ErrorReason::RPC_ERROR, message)
64
- ensure_cb.call(err)
42
+ def ensure_em_running(&blk)
43
+ EM.reactor_running? ? yield : EM.fiber_run { blk.call; EM.stop }
65
44
  end
66
45
 
67
46
  def resume_fiber(fib)
68
47
  EM::cancel_timer(@timeout_timer)
69
48
  fib.resume(true)
70
49
  rescue => ex
71
- log_error 'An exception occurred while waiting for server response:'
50
+ log_error "[#{log_signature}] An exception occurred while waiting for server response:"
72
51
  log_error ex.message
73
52
  log_error ex.backtrace.join("\n")
74
53
 
75
54
  message = 'Synchronous client failed: %s' % ex.message
76
- err = ClientError.new(Protobuf::Socketrpc::ErrorReason::RPC_ERROR, message)
55
+ err = Protobuf::Rpc::ClientError.new(Protobuf::Socketrpc::ErrorReason::RPC_ERROR, message)
77
56
  ensure_cb.call(err)
78
57
  end
79
58
 
59
+ def set_timeout_and_validate_fiber
60
+ @timeout_timer = EM::add_timer(@options[:timeout]) do
61
+ message = 'Client timeout of %d seconds expired' % @options[:timeout]
62
+ err = Protobuf::Rpc::ClientError.new(Protobuf::Socketrpc::ErrorReason::RPC_ERROR, message)
63
+ ensure_cb.call(err)
64
+ end
65
+
66
+ Fiber.yield
67
+ rescue FiberError
68
+ message = "Synchronous calls must be in 'EM.fiber_run' block"
69
+ err = Protobuf::Rpc::ClientError.new(Protobuf::Socketrpc::ErrorReason::RPC_ERROR, message)
70
+ ensure_cb.call(err)
71
+ end
80
72
 
81
73
  end
82
74
  end
83
75
  end
84
- end
76
+ end
@@ -4,10 +4,67 @@ module Protobuf
4
4
  module Rpc
5
5
  module Connectors
6
6
  class Socket < Base
7
+ include Protobuf::Rpc::Connectors::Common
8
+ include Protobuf::Logger::LogMethods
7
9
 
8
10
  def send_request
11
+ check_async
12
+ initialize_stats
13
+ connect_to_rpc_server
14
+ post_init # calls _send_request
15
+ read_response
9
16
  end
10
-
17
+
18
+ private
19
+
20
+ def check_async
21
+ if async?
22
+ log_error "[client-#{self.class}] Cannot run in async mode"
23
+ raise "Cannot use Socket client in async mode"
24
+ else
25
+ log_debug "[client-#{self.class}] Async check passed"
26
+ end
27
+ end
28
+
29
+ def close_connection
30
+ @socket.close
31
+ log_debug "[client-#{self.class}] Connector closed"
32
+ end
33
+
34
+ def connect_to_rpc_server
35
+ @socket = TCPSocket.new(options[:host], options[:port])
36
+ log_debug "[client-#{self.class}] Connection established #{options[:host]}:#{options[:port]}"
37
+ end
38
+
39
+ # Method to determine error state, must be used with Connector api
40
+ def error?
41
+ log_debug "[client-#{self.class}] Error state : #{@socket.closed?}"
42
+ @socket.closed?
43
+ end
44
+
45
+ def read_data
46
+ size_io = StringIO.new
47
+
48
+ while((size_reader = @socket.getc) != "-")
49
+ size_io << size_reader
50
+ end
51
+ str_size_io = size_io.string
52
+
53
+ "#{str_size_io}-#{@socket.read(str_size_io.to_i)}"
54
+ end
55
+
56
+ def read_response
57
+ @buffer << read_data
58
+ parse_response if @buffer.flushed?
59
+ end
60
+
61
+ def send_data(data)
62
+ @socket.write(data)
63
+ @socket.flush
64
+ @socket.close_write
65
+ log_debug "[client-#{self.class}] write closed"
66
+ end
67
+
11
68
  end
12
69
  end
13
70
  end
@@ -2,20 +2,9 @@ require 'protobuf/rpc/rpc.pb'
2
2
 
3
3
  module Protobuf
4
4
  module Rpc
5
+ ClientError = Struct.new("ClientError", :code, :message)
5
6
 
6
- autoload :BadRequestData, 'protobuf/rpc/error/server_error'
7
- autoload :BadRequestProto, 'protobuf/rpc/error/server_error'
8
- autoload :ServiceNotFound, 'protobuf/rpc/error/server_error'
9
- autoload :MethodNotFound, 'protobuf/rpc/error/server_error'
10
- autoload :RpcError, 'protobuf/rpc/error/server_error'
11
- autoload :RpcFailed, 'protobuf/rpc/error/server_error'
12
-
13
- autoload :InvalidRequestProto, 'protobuf/rpc/error/client_error'
14
- autoload :BadResponseProto, 'protobuf/rpc/error/client_error'
15
- autoload :UnknownHost, 'protobuf/rpc/error/client_error'
16
- autoload :IOError, 'protobuf/rpc/error/client_error'
17
-
18
- # Base RpcError class for client and server errors
7
+ # Base PbError class for client and server errors
19
8
  class PbError < StandardError
20
9
  attr_reader :error_type
21
10
 
@@ -31,4 +20,7 @@ module Protobuf
31
20
  end
32
21
 
33
22
  end
34
- end
23
+ end
24
+
25
+ require 'protobuf/rpc/error/server_error'
26
+ require 'protobuf/rpc/error/client_error'
@@ -1,5 +1,3 @@
1
- require 'eventmachine'
2
- require 'socket'
3
1
  require 'protobuf/common/logger'
4
2
  require 'protobuf/rpc/rpc.pb'
5
3
  require 'protobuf/rpc/buffer'
@@ -8,26 +6,7 @@ require 'protobuf/rpc/stat'
8
6
 
9
7
  module Protobuf
10
8
  module Rpc
11
- class Server < EventMachine::Connection
12
- include Protobuf::Logger::LogMethods
13
-
14
- # Initialize a new read buffer for storing client request info
15
- def post_init
16
- log_debug '[server] Post init, new read buffer created'
17
-
18
- @stats = Protobuf::Rpc::Stat.new(:SERVER, true)
19
- @stats.client = Socket.unpack_sockaddr_in(get_peername)
20
-
21
- @buffer = Protobuf::Rpc::Buffer.new :read
22
- @did_respond = false
23
- end
24
-
25
- # Receive a chunk of data, potentially flushed to handle_client
26
- def receive_data data
27
- log_debug '[server] receive_data: %s' % data
28
- @buffer << data
29
- handle_client if @buffer.flushed?
30
- end
9
+ module Server
31
10
 
32
11
  # Invoke the service method dictated by the proto wrapper request object
33
12
  def handle_client
@@ -38,17 +17,16 @@ module Protobuf
38
17
  @response = Protobuf::Socketrpc::Response.new
39
18
 
40
19
  # Parse the protobuf request from the socket
41
- log_debug '[server] Parsing request from client'
20
+ log_debug "[#{log_signature}] Parsing request from client"
42
21
  parse_request_from_buffer
43
22
 
44
23
  # Determine the service class and method name from the request
45
- log_debug '[server] Extracting procedure call info from request'
24
+ log_debug "[#{log_signature}] Extracting procedure call info from request"
46
25
  parse_service_info
47
26
 
48
27
  # Call the service method
49
- log_debug '[server] Dispatching client request to service'
28
+ log_debug "[#{log_signature}] Dispatching client request to service"
50
29
  invoke_rpc_method
51
-
52
30
  rescue => error
53
31
  # Ensure we're handling any errors that try to slip out the back door
54
32
  log_error error.message
@@ -56,8 +34,18 @@ module Protobuf
56
34
  handle_error(error)
57
35
  send_response
58
36
  end
59
-
60
- private
37
+
38
+ # Client error handler. Receives an exception object and writes it into the @response
39
+ def handle_error(error)
40
+ log_debug "[#{log_signature}] handle_error: %s" % error.inspect
41
+ if error.respond_to?(:to_response)
42
+ error.to_response(@response)
43
+ else
44
+ message = error.respond_to?(:message) ? error.message : error.to_s
45
+ code = error.respond_to?(:code) ? error.code.to_s : "RPC_ERROR"
46
+ PbError.new(message, code).to_response(@response)
47
+ end
48
+ end
61
49
 
62
50
  # Assuming all things check out, we can call the service method
63
51
  def invoke_rpc_method
@@ -85,99 +73,84 @@ module Protobuf
85
73
  end
86
74
 
87
75
  # Call the service method
88
- log_debug '[server] Invoking %s#%s with request %s' % [@klass.name, @method, @request.inspect]
89
- @service.__send__ @method, @request
76
+ log_debug "[#{log_signature}] Invoking %s#%s with request %s" % [@klass.name, @method, @request.inspect]
77
+ @service.__send__(@method, @request)
78
+ end
79
+
80
+ def log_signature
81
+ @log_signature ||= "server-#{self.class}"
90
82
  end
91
83
 
92
84
  # Parse the incoming request object into our expected request object
93
85
  def parse_request_from_buffer
94
- begin
95
- log_debug '[server] parsing request from buffer: %s' % @buffer.data.inspect
96
- @request.parse_from_string @buffer.data
97
- rescue => error
98
- exc = BadRequestData.new 'Unable to parse request: %s' % error.message
99
- log_error exc.message
100
- raise exc
101
- end
86
+ log_debug "[#{log_signature}] parsing request from buffer: %s" % @buffer.data.inspect
87
+ @request.parse_from_string(@buffer.data)
88
+ rescue => error
89
+ exc = BadRequestData.new 'Unable to parse request: %s' % error.message
90
+ log_error exc.message
91
+ raise exc
102
92
  end
103
-
93
+
104
94
  # Read out the response from the service method,
105
95
  # setting it on the pb request, and serializing the
106
96
  # response to the protobuf response wrapper
107
- def parse_response_from_service response
108
- begin
109
- expected = @klass.rpcs[@klass][@method].response_type
110
-
111
- # Cannibalize the response if it's a Hash
112
- response = expected.new(response) if response.is_a?(Hash)
113
- actual = response.class
114
-
115
- log_debug '[server] response (should/actual): %s/%s' % [expected.name, actual.name]
116
-
117
- # Determine if the service tried to change response types on us
118
- if expected == actual
119
- begin
120
- # Response types match, so go ahead and serialize
121
- log_debug '[server] serializing response: %s' % response.inspect
122
- @response.response_proto = response.serialize_to_string
123
- rescue
124
- raise BadResponseProto, $!.message
125
- end
126
- else
127
- # response types do not match, throw the appropriate error
128
- raise BadResponseProto, 'Response proto changed from %s to %s' % [expected.name, actual.name]
129
- end
130
- rescue => error
131
- log_error error.message
132
- log_error error.backtrace.join("\n")
133
- handle_error(error)
134
- end
135
- end
136
-
137
- # Write the response wrapper to the client
138
- def send_response
139
- raise 'Response already sent to client' if @did_respond
140
- log_debug '[server] Sending response to client: %s' % @response.inspect
141
- response_buffer = Protobuf::Rpc::Buffer.new(:write, @response)
142
- send_data(response_buffer.write)
143
- @stats.response_size = response_buffer.size
144
- @stats.end
145
- @stats.log_stats
146
- @did_respond = true
147
- end
148
-
149
- # Client error handler. Receives an exception object and writes it into the @response
150
- def handle_error error
151
- log_debug '[server] handle_error: %s' % error.inspect
152
- if error.is_a? PbError
153
- error.to_response @response
154
- elsif error.is_a? ClientError
155
- PbError.new(error.message, error.code.to_s).to_response @response
97
+ def parse_response_from_service(response)
98
+ expected = @klass.rpcs[@klass][@method].response_type
99
+
100
+ # Cannibalize the response if it's a Hash
101
+ response = expected.new(response) if response.is_a?(Hash)
102
+ actual = response.class
103
+ log_debug "[#{log_signature}] response (should/actual): %s/%s" % [expected.name, actual.name]
104
+
105
+ # Determine if the service tried to change response types on us
106
+ if expected == actual
107
+ serialize_response(response)
156
108
  else
157
- message = error.is_a?(String) ? error : error.message
158
- PbError.new(message, 'RPC_ERROR').to_response @response
109
+ # response types do not match, throw the appropriate error
110
+ raise BadResponseProto, 'Response proto changed from %s to %s' % [expected.name, actual.name]
159
111
  end
112
+ rescue => error
113
+ log_error error.message
114
+ log_error error.backtrace.join("\n")
115
+ handle_error(error)
160
116
  end
161
-
117
+
162
118
  # Parses and returns the service and method name from the request wrapper proto
163
119
  def parse_service_info
164
- @klass, @method = nil, nil
165
-
166
- begin
167
- @klass = Util.constantize(@request.service_name)
168
- rescue
169
- raise ServiceNotFound, "Service class #{@request.service_name} is not found"
170
- end
171
-
120
+ @klass = Util.constantize(@request.service_name)
172
121
  @method = Util.underscore(@request.method_name).to_sym
122
+
173
123
  unless @klass.instance_methods.include?(@method)
174
124
  raise MethodNotFound, "Service method #{@request.method_name} is not defined by the service"
175
125
  end
176
126
 
177
127
  @stats.service = @klass.name
178
128
  @stats.method = @method
129
+ rescue NameError
130
+ raise ServiceNotFound, "Service class #{@request.service_name} is not found"
131
+ end
132
+
133
+ # Write the response wrapper to the client
134
+ def send_response
135
+ raise 'Response already sent to client' if @did_respond
136
+ log_debug "[#{log_signature}] Sending response to client: %s" % @response.inspect
137
+ response_buffer = Protobuf::Rpc::Buffer.new(:write, @response)
138
+ send_data(response_buffer.write)
139
+ @stats.response_size = response_buffer.size
140
+ @stats.end
141
+ @stats.log_stats
142
+ @did_respond = true
143
+ end
144
+
145
+ def serialize_response(response)
146
+ log_debug "[#{log_signature}] serializing response: %s" % response.inspect
147
+ @response.response_proto = response.serialize_to_string
148
+ rescue
149
+ raise BadResponseProto, $!.message
179
150
  end
180
151
 
181
152
  end
153
+
182
154
  end
155
+
183
156
  end
@@ -0,0 +1,32 @@
1
+ module Protobuf
2
+ module Rpc
3
+ class EventedRunner
4
+
5
+ def self.stop
6
+ EventMachine.stop_event_loop if EventMachine.reactor_running?
7
+ Protobuf::Logger.info 'Shutdown complete'
8
+ end
9
+
10
+ def self.run(server)
11
+ # Ensure errors thrown within EM are caught and logged appropriately
12
+ EventMachine.error_handler do |error|
13
+ if error.message == 'no acceptor'
14
+ raise 'Failed binding to %s:%d (%s)' % [server.host, server.port, error.message]
15
+ else
16
+ Protobuf::Logger.error error.message
17
+ Protobuf::Logger.error error.backtrace.join("\n")
18
+ end
19
+ end
20
+
21
+ # Startup and run the rpc server
22
+ EM.schedule do
23
+ EventMachine.start_server(server.host, server.port, Protobuf::Rpc::EventedServer) && \
24
+ Protobuf::Logger.info('RPC Server listening at %s:%d in %s' % [server.host, server.port, server.env])
25
+ end
26
+
27
+ # Join or start the reactor
28
+ EM.reactor_running? ? EM.reactor_thread.join : EM.run
29
+ end
30
+ end
31
+ end
32
+ end