protobuf 1.0.1 → 1.1.0.beta0

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