protobuf 1.0.0
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/.gitignore +5 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +28 -0
- data/README.md +216 -0
- data/Rakefile +1 -0
- data/bin/rpc_server +117 -0
- data/bin/rprotoc +46 -0
- data/examples/addressbook.pb.rb +55 -0
- data/examples/addressbook.proto +24 -0
- data/examples/reading_a_message.rb +32 -0
- data/examples/writing_a_message.rb +46 -0
- data/lib/protobuf.rb +6 -0
- data/lib/protobuf/common/exceptions.rb +11 -0
- data/lib/protobuf/common/logger.rb +64 -0
- data/lib/protobuf/common/util.rb +59 -0
- data/lib/protobuf/common/wire_type.rb +10 -0
- data/lib/protobuf/compiler/compiler.rb +52 -0
- data/lib/protobuf/compiler/nodes.rb +323 -0
- data/lib/protobuf/compiler/proto.y +216 -0
- data/lib/protobuf/compiler/proto2.ebnf +79 -0
- data/lib/protobuf/compiler/proto_parser.rb +1425 -0
- data/lib/protobuf/compiler/template/rpc_bin.erb +4 -0
- data/lib/protobuf/compiler/template/rpc_client.erb +18 -0
- data/lib/protobuf/compiler/template/rpc_service.erb +25 -0
- data/lib/protobuf/compiler/template/rpc_service_implementation.erb +42 -0
- data/lib/protobuf/compiler/visitors.rb +302 -0
- data/lib/protobuf/descriptor/descriptor.proto +286 -0
- data/lib/protobuf/descriptor/descriptor.rb +55 -0
- data/lib/protobuf/descriptor/descriptor_builder.rb +143 -0
- data/lib/protobuf/descriptor/descriptor_proto.rb +138 -0
- data/lib/protobuf/descriptor/enum_descriptor.rb +33 -0
- data/lib/protobuf/descriptor/field_descriptor.rb +49 -0
- data/lib/protobuf/descriptor/file_descriptor.rb +37 -0
- data/lib/protobuf/message/decoder.rb +83 -0
- data/lib/protobuf/message/encoder.rb +46 -0
- data/lib/protobuf/message/enum.rb +62 -0
- data/lib/protobuf/message/extend.rb +8 -0
- data/lib/protobuf/message/field.rb +701 -0
- data/lib/protobuf/message/message.rb +402 -0
- data/lib/protobuf/message/protoable.rb +38 -0
- data/lib/protobuf/rpc/buffer.rb +74 -0
- data/lib/protobuf/rpc/client.rb +268 -0
- data/lib/protobuf/rpc/client_connection.rb +225 -0
- data/lib/protobuf/rpc/error.rb +34 -0
- data/lib/protobuf/rpc/error/client_error.rb +31 -0
- data/lib/protobuf/rpc/error/server_error.rb +43 -0
- data/lib/protobuf/rpc/rpc.pb.rb +107 -0
- data/lib/protobuf/rpc/server.rb +183 -0
- data/lib/protobuf/rpc/service.rb +244 -0
- data/lib/protobuf/rpc/stat.rb +70 -0
- data/lib/protobuf/version.rb +3 -0
- data/proto/rpc.proto +73 -0
- data/protobuf.gemspec +25 -0
- data/script/mk_parser +2 -0
- data/spec/functional/embedded_service_spec.rb +7 -0
- data/spec/proto/test.pb.rb +31 -0
- data/spec/proto/test.proto +31 -0
- data/spec/proto/test_service.rb +30 -0
- data/spec/proto/test_service_impl.rb +17 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/unit/client_spec.rb +128 -0
- data/spec/unit/common/logger_spec.rb +121 -0
- data/spec/unit/enum_spec.rb +13 -0
- data/spec/unit/message_spec.rb +67 -0
- data/spec/unit/server_spec.rb +27 -0
- data/spec/unit/service_spec.rb +75 -0
- data/test/check_unbuild.rb +30 -0
- data/test/data/data.bin +3 -0
- data/test/data/data_source.py +14 -0
- data/test/data/types.bin +0 -0
- data/test/data/types_source.py +22 -0
- data/test/data/unk.png +0 -0
- data/test/proto/addressbook.pb.rb +66 -0
- data/test/proto/addressbook.proto +33 -0
- data/test/proto/addressbook_base.pb.rb +58 -0
- data/test/proto/addressbook_base.proto +26 -0
- data/test/proto/addressbook_ext.pb.rb +20 -0
- data/test/proto/addressbook_ext.proto +6 -0
- data/test/proto/collision.pb.rb +17 -0
- data/test/proto/collision.proto +5 -0
- data/test/proto/ext_collision.pb.rb +24 -0
- data/test/proto/ext_collision.proto +8 -0
- data/test/proto/ext_range.pb.rb +22 -0
- data/test/proto/ext_range.proto +7 -0
- data/test/proto/float_default.proto +10 -0
- data/test/proto/lowercase.pb.rb +30 -0
- data/test/proto/lowercase.proto +9 -0
- data/test/proto/merge.pb.rb +39 -0
- data/test/proto/merge.proto +15 -0
- data/test/proto/nested.pb.rb +30 -0
- data/test/proto/nested.proto +9 -0
- data/test/proto/optional_field.pb.rb +35 -0
- data/test/proto/optional_field.proto +12 -0
- data/test/proto/packed.pb.rb +22 -0
- data/test/proto/packed.proto +6 -0
- data/test/proto/rpc.proto +6 -0
- data/test/proto/types.pb.rb +84 -0
- data/test/proto/types.proto +37 -0
- data/test/test_addressbook.rb +56 -0
- data/test/test_compiler.rb +325 -0
- data/test/test_descriptor.rb +122 -0
- data/test/test_enum_value.rb +41 -0
- data/test/test_extension.rb +36 -0
- data/test/test_lowercase.rb +11 -0
- data/test/test_message.rb +128 -0
- data/test/test_optional_field.rb +103 -0
- data/test/test_packed_field.rb +40 -0
- data/test/test_parse.rb +15 -0
- data/test/test_repeated_types.rb +132 -0
- data/test/test_serialize.rb +61 -0
- data/test/test_standard_message.rb +96 -0
- data/test/test_types.rb +226 -0
- metadata +261 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require 'protobuf/rpc/rpc.pb'
|
|
2
|
+
|
|
3
|
+
module Protobuf
|
|
4
|
+
module Rpc
|
|
5
|
+
|
|
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
|
|
19
|
+
class PbError < StandardError
|
|
20
|
+
attr_reader :error_type
|
|
21
|
+
|
|
22
|
+
def initialize message='An unknown RpcError occurred', error_type='RPC_ERROR'
|
|
23
|
+
@error_type = error_type.is_a?(String) ? Protobuf::Socketrpc::ErrorReason.const_get(error_type) : error_type
|
|
24
|
+
super message
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_response response
|
|
28
|
+
response.error = message
|
|
29
|
+
response.error_reason = @error_type
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require 'protobuf/rpc/error'
|
|
2
|
+
|
|
3
|
+
module Protobuf
|
|
4
|
+
module Rpc
|
|
5
|
+
|
|
6
|
+
class InvalidRequestProto < PbError
|
|
7
|
+
def initialize message='Invalid request type given'
|
|
8
|
+
super message, 'INVALID_REQUEST_PROTO'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class BadResponseProto < PbError
|
|
13
|
+
def initialize message='Bad response type from server'
|
|
14
|
+
super message, 'BAD_RESPONSE_PROTO'
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class UnkownHost < PbError
|
|
19
|
+
def initialize message='Unknown host or port'
|
|
20
|
+
super message, 'UNKNOWN_HOST'
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class IOError < PbError
|
|
25
|
+
def initialize message='IO Error occurred'
|
|
26
|
+
super message, 'IO_ERROR'
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'protobuf/rpc/rpc.pb'
|
|
2
|
+
|
|
3
|
+
module Protobuf
|
|
4
|
+
module Rpc
|
|
5
|
+
|
|
6
|
+
class BadRequestData < PbError
|
|
7
|
+
def initialize message='Unable to parse request'
|
|
8
|
+
super message, 'BAD_REQUEST_DATA'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class BadRequestProto < PbError
|
|
13
|
+
def initialize message='Request is of wrong type'
|
|
14
|
+
super message, 'BAD_REQUEST_PROTO'
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class ServiceNotFound < PbError
|
|
19
|
+
def initialize message='Service class not found'
|
|
20
|
+
super message, 'SERVICE_NOT_FOUND'
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class MethodNotFound < PbError
|
|
25
|
+
def initialize message='Service method not found'
|
|
26
|
+
super message, 'METHOD_NOT_FOUND'
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class RpcError < PbError
|
|
31
|
+
def initialize message='RPC exception occurred'
|
|
32
|
+
super message, 'RPC_ERROR'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class RpcFailed < PbError
|
|
37
|
+
def initialize message='RPC failed'
|
|
38
|
+
super message, 'RPC_FAILED'
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
### Generated by rprotoc. DO NOT EDIT!
|
|
2
|
+
### <proto file: rpc.proto>
|
|
3
|
+
# // Copyright (c) 2009 Shardul Deo
|
|
4
|
+
# //
|
|
5
|
+
# // Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# // of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# // in the Software without restriction, including without limitation the rights
|
|
8
|
+
# // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# // copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# // furnished to do so, subject to the following conditions:
|
|
11
|
+
# //
|
|
12
|
+
# // The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# // all copies or substantial portions of the Software.
|
|
14
|
+
# //
|
|
15
|
+
# // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
# // THE SOFTWARE.
|
|
22
|
+
#
|
|
23
|
+
# // Author: Shardul Deo
|
|
24
|
+
# //
|
|
25
|
+
# // Protobufs needed for socket rpcs.
|
|
26
|
+
#
|
|
27
|
+
# package protobuf.socketrpc;
|
|
28
|
+
#
|
|
29
|
+
# message Request {
|
|
30
|
+
#
|
|
31
|
+
# // RPC service full name
|
|
32
|
+
# required string service_name = 1;
|
|
33
|
+
#
|
|
34
|
+
# // RPC method name
|
|
35
|
+
# required string method_name = 2;
|
|
36
|
+
#
|
|
37
|
+
# // RPC request proto
|
|
38
|
+
# required bytes request_proto = 3;
|
|
39
|
+
# }
|
|
40
|
+
#
|
|
41
|
+
# message Response {
|
|
42
|
+
#
|
|
43
|
+
# // RPC response proto
|
|
44
|
+
# optional bytes response_proto = 1;
|
|
45
|
+
#
|
|
46
|
+
# // Error, if any
|
|
47
|
+
# optional string error = 2;
|
|
48
|
+
#
|
|
49
|
+
# // Was callback invoked
|
|
50
|
+
# optional bool callback = 3 [default = false];
|
|
51
|
+
#
|
|
52
|
+
# // Error Reason
|
|
53
|
+
# optional ErrorReason error_reason = 4;
|
|
54
|
+
# }
|
|
55
|
+
#
|
|
56
|
+
# // Possible error reasons
|
|
57
|
+
# // The server-side errors are returned in the response from the server.
|
|
58
|
+
# // The client-side errors are returned by the client-side code when it doesn't
|
|
59
|
+
# // have a response from the server.
|
|
60
|
+
# enum ErrorReason {
|
|
61
|
+
#
|
|
62
|
+
# // Server-side errors
|
|
63
|
+
# BAD_REQUEST_DATA = 0; // Server received bad request data
|
|
64
|
+
# BAD_REQUEST_PROTO = 1; // Server received bad request proto
|
|
65
|
+
# SERVICE_NOT_FOUND = 2; // Service not found on server
|
|
66
|
+
# METHOD_NOT_FOUND = 3; // Method not found on server
|
|
67
|
+
# RPC_ERROR = 4; // Rpc threw exception on server
|
|
68
|
+
# RPC_FAILED = 5; // Rpc failed on server
|
|
69
|
+
#
|
|
70
|
+
# // Client-side errors (these are returned by the client-side code)
|
|
71
|
+
# INVALID_REQUEST_PROTO = 6; // Rpc was called with invalid request proto
|
|
72
|
+
# BAD_RESPONSE_PROTO = 7; // Server returned a bad response proto
|
|
73
|
+
# UNKNOWN_HOST = 8; // Could not find supplied host
|
|
74
|
+
# IO_ERROR = 9; // I/O error while communicating with server
|
|
75
|
+
# }
|
|
76
|
+
|
|
77
|
+
require 'protobuf/message/message'
|
|
78
|
+
require 'protobuf/message/enum'
|
|
79
|
+
require 'protobuf/message/extend'
|
|
80
|
+
|
|
81
|
+
module Protobuf
|
|
82
|
+
module Socketrpc
|
|
83
|
+
class Request < ::Protobuf::Message
|
|
84
|
+
required :string, :service_name, 1
|
|
85
|
+
required :string, :method_name, 2
|
|
86
|
+
required :bytes, :request_proto, 3
|
|
87
|
+
end
|
|
88
|
+
class Response < ::Protobuf::Message
|
|
89
|
+
optional :bytes, :response_proto, 1
|
|
90
|
+
optional :string, :error, 2
|
|
91
|
+
optional :bool, :callback, 3, :default => false
|
|
92
|
+
optional :ErrorReason, :error_reason, 4
|
|
93
|
+
end
|
|
94
|
+
class ErrorReason < ::Protobuf::Enum
|
|
95
|
+
define :BAD_REQUEST_DATA, 0
|
|
96
|
+
define :BAD_REQUEST_PROTO, 1
|
|
97
|
+
define :SERVICE_NOT_FOUND, 2
|
|
98
|
+
define :METHOD_NOT_FOUND, 3
|
|
99
|
+
define :RPC_ERROR, 4
|
|
100
|
+
define :RPC_FAILED, 5
|
|
101
|
+
define :INVALID_REQUEST_PROTO, 6
|
|
102
|
+
define :BAD_RESPONSE_PROTO, 7
|
|
103
|
+
define :UNKNOWN_HOST, 8
|
|
104
|
+
define :IO_ERROR, 9
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
require 'eventmachine'
|
|
2
|
+
require 'socket'
|
|
3
|
+
require 'protobuf/common/logger'
|
|
4
|
+
require 'protobuf/rpc/rpc.pb'
|
|
5
|
+
require 'protobuf/rpc/buffer'
|
|
6
|
+
require 'protobuf/rpc/error'
|
|
7
|
+
require 'protobuf/rpc/stat'
|
|
8
|
+
|
|
9
|
+
module Protobuf
|
|
10
|
+
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
|
|
31
|
+
|
|
32
|
+
# Invoke the service method dictated by the proto wrapper request object
|
|
33
|
+
def handle_client
|
|
34
|
+
@stats.request_size = @buffer.size
|
|
35
|
+
|
|
36
|
+
# Setup the initial request and response
|
|
37
|
+
@request = Protobuf::Socketrpc::Request.new
|
|
38
|
+
@response = Protobuf::Socketrpc::Response.new
|
|
39
|
+
|
|
40
|
+
# Parse the protobuf request from the socket
|
|
41
|
+
log_debug '[server] Parsing request from client'
|
|
42
|
+
parse_request_from_buffer
|
|
43
|
+
|
|
44
|
+
# Determine the service class and method name from the request
|
|
45
|
+
log_debug '[server] Extracting procedure call info from request'
|
|
46
|
+
parse_service_info
|
|
47
|
+
|
|
48
|
+
# Call the service method
|
|
49
|
+
log_debug '[server] Dispatching client request to service'
|
|
50
|
+
invoke_rpc_method
|
|
51
|
+
|
|
52
|
+
rescue => error
|
|
53
|
+
# Ensure we're handling any errors that try to slip out the back door
|
|
54
|
+
log_error error.message
|
|
55
|
+
log_error error.backtrace.join("\n")
|
|
56
|
+
handle_error(error)
|
|
57
|
+
send_response
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# Assuming all things check out, we can call the service method
|
|
63
|
+
def invoke_rpc_method
|
|
64
|
+
# Get a new instance of the service
|
|
65
|
+
@service = @klass.new
|
|
66
|
+
|
|
67
|
+
# Define our response callback to perform the "successful" response to our client
|
|
68
|
+
# This decouples the service's rpc method from our response to the client,
|
|
69
|
+
# allowing the service to be the dictator for when the response should be sent back.
|
|
70
|
+
#
|
|
71
|
+
# In other words, we don't send the response once the service method finishes executing
|
|
72
|
+
# since the service may perform it's own operations asynchronously.
|
|
73
|
+
@service.on_send_response do |response|
|
|
74
|
+
unless @did_respond
|
|
75
|
+
parse_response_from_service(response)
|
|
76
|
+
send_response
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
@service.on_rpc_failed do |error|
|
|
81
|
+
unless @did_respond
|
|
82
|
+
handle_error(error)
|
|
83
|
+
send_response
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Call the service method
|
|
88
|
+
log_debug '[server] Invoking %s#%s with request %s' % [@klass.name, @method, @request.inspect]
|
|
89
|
+
@service.__send__ @method, @request
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Parse the incoming request object into our expected request object
|
|
93
|
+
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
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Read out the response from the service method,
|
|
105
|
+
# setting it on the pb request, and serializing the
|
|
106
|
+
# 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
|
|
156
|
+
else
|
|
157
|
+
message = error.is_a?(String) ? error : error.message
|
|
158
|
+
PbError.new(message, 'RPC_ERROR').to_response @response
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Parses and returns the service and method name from the request wrapper proto
|
|
163
|
+
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
|
+
|
|
172
|
+
@method = Util.underscore(@request.method_name).to_sym
|
|
173
|
+
unless @klass.instance_methods.include?(@method)
|
|
174
|
+
raise MethodNotFound, "Service method #{@request.method_name} is not defined by the service"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
@stats.service = @klass.name
|
|
178
|
+
@stats.method = @method
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
require 'protobuf/common/logger'
|
|
2
|
+
require 'protobuf/rpc/client'
|
|
3
|
+
require 'protobuf/rpc/error'
|
|
4
|
+
|
|
5
|
+
module Protobuf
|
|
6
|
+
module Rpc
|
|
7
|
+
|
|
8
|
+
# Object to encapsulate the request/response types for a given service method
|
|
9
|
+
#
|
|
10
|
+
RpcMethod = Struct.new "RpcMethod", :service, :method, :request_type, :response_type
|
|
11
|
+
|
|
12
|
+
class Service
|
|
13
|
+
include Protobuf::Logger::LogMethods
|
|
14
|
+
|
|
15
|
+
attr_reader :request
|
|
16
|
+
attr_accessor :response, :async_responder
|
|
17
|
+
private :request, :response, :response=
|
|
18
|
+
|
|
19
|
+
DEFAULT_LOCATION = {
|
|
20
|
+
:host => 'localhost',
|
|
21
|
+
:port => 9939
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Class methods are intended for use on the client-side.
|
|
25
|
+
#
|
|
26
|
+
class << self
|
|
27
|
+
|
|
28
|
+
# You MUST add the method name to this list if you are adding
|
|
29
|
+
# instance methods below, otherwise stuff will definitely break
|
|
30
|
+
NON_RPC_METHODS = %w( rpcs call_rpc on_rpc_failed rpc_failed request response method_missing async_responder on_send_response send_response )
|
|
31
|
+
|
|
32
|
+
# Override methods being added to the class
|
|
33
|
+
# If the method isn't already a private instance method, or it doesn't start with rpc_,
|
|
34
|
+
# or it isn't in the reserved method list (NON_RPC_METHODS),
|
|
35
|
+
# We want to remap the method such that we can wrap it in before and after behavior,
|
|
36
|
+
# most notably calling call_rpc against the method. See call_rpc for more info.
|
|
37
|
+
def method_added old
|
|
38
|
+
new_method = :"rpc_#{old}"
|
|
39
|
+
return if private_instance_methods.include?(new_method) or old =~ /^rpc_/ or NON_RPC_METHODS.include?(old.to_s)
|
|
40
|
+
|
|
41
|
+
alias_method new_method, old
|
|
42
|
+
private new_method
|
|
43
|
+
|
|
44
|
+
begin
|
|
45
|
+
define_method(old) do |pb_request|
|
|
46
|
+
call_rpc old.to_sym, pb_request
|
|
47
|
+
end
|
|
48
|
+
rescue ArgumentError => e
|
|
49
|
+
# Wrap a known issue where an instance method was defined in the class without
|
|
50
|
+
# it being ignored with NON_RPC_METHODS.
|
|
51
|
+
raise ArgumentError, "#{e.message} (Note: This could mean that you need to add the method #{old} to the NON_RPC_METHODS list)"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Generated service classes should call this method on themselves to add rpc methods
|
|
56
|
+
# to the stack with a given request and response type
|
|
57
|
+
def rpc method, request_type, response_type
|
|
58
|
+
rpcs[self] ||= {}
|
|
59
|
+
rpcs[self][method] = RpcMethod.new self, method, request_type, response_type
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Shorthand for @rpcs class instance var
|
|
63
|
+
def rpcs
|
|
64
|
+
@rpcs ||= {}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Create a new client for the given service.
|
|
68
|
+
# See Client#initialize and ClientConnection::DEFAULT_OPTIONS
|
|
69
|
+
# for all available options.
|
|
70
|
+
#
|
|
71
|
+
def client options={}
|
|
72
|
+
configure
|
|
73
|
+
Client.new({
|
|
74
|
+
:service => self,
|
|
75
|
+
:async => false,
|
|
76
|
+
:host => self.host,
|
|
77
|
+
:port => self.port
|
|
78
|
+
}.merge(options))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Allows service-level configuration of location.
|
|
82
|
+
# Useful for system-startup configuration of a service
|
|
83
|
+
# so that any Clients using the Service.client sugar
|
|
84
|
+
# will not have to configure the location each time.
|
|
85
|
+
#
|
|
86
|
+
def configure config={}
|
|
87
|
+
locations[self] ||= {}
|
|
88
|
+
locations[self][:host] = config[:host] if config.key? :host
|
|
89
|
+
locations[self][:port] = config[:port] if config.key? :port
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Shorthand call to configure, passing a string formatted as hostname:port
|
|
93
|
+
# e.g. 127.0.0.1:9933
|
|
94
|
+
# e.g. localhost:0
|
|
95
|
+
#
|
|
96
|
+
def located_at location
|
|
97
|
+
return if location.nil? or location.downcase.strip !~ /[a-z0-9.]+:\d+/
|
|
98
|
+
host, port = location.downcase.strip.split ':'
|
|
99
|
+
configure :host => host, :port => port.to_i
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The host location of the service
|
|
103
|
+
def host
|
|
104
|
+
configure
|
|
105
|
+
locations[self][:host] || DEFAULT_LOCATION[:host]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The port of the service on the destination server
|
|
109
|
+
def port
|
|
110
|
+
configure
|
|
111
|
+
locations[self][:port] || DEFAULT_LOCATION[:port]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Shorthand for @locations class instance var
|
|
115
|
+
def locations
|
|
116
|
+
@locations ||= {}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# If a method comes through that hasn't been found, and it
|
|
122
|
+
# is defined in the rpcs method list, we know that the rpc
|
|
123
|
+
# stub has been created, but no implementing method provides the
|
|
124
|
+
# functionality, so throw an appropriate error, otherwise go to super
|
|
125
|
+
#
|
|
126
|
+
def method_missing m, *params
|
|
127
|
+
if rpcs.key?(m)
|
|
128
|
+
exc = MethodNotFound.new "#{self}##{m} was defined as a valid rpc method, but was not implemented."
|
|
129
|
+
log_error exc.message
|
|
130
|
+
raise exc
|
|
131
|
+
else
|
|
132
|
+
log_error '-------------- [service] %s#%s not rpc method, passing to super' % [self.class.name, m.to_s]
|
|
133
|
+
super m, params
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Convenience wrapper around the rpc method list for a given class
|
|
138
|
+
def rpcs
|
|
139
|
+
self.class.rpcs[self.class]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Callback register for the server when a service
|
|
143
|
+
# method calls rpc_failed. Called by Service#rpc_failed.
|
|
144
|
+
def on_rpc_failed &rpc_failure_callback
|
|
145
|
+
@rpc_failure_callback = rpc_failure_callback
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Automatically fail a service method.
|
|
149
|
+
# NOTE: This shortcuts the @async_responder paradigm. There is
|
|
150
|
+
# not any way to get around this currently (and I'm not sure you should want to).
|
|
151
|
+
#
|
|
152
|
+
def rpc_failed message="RPC Failed while executing service method #{@current_method}"
|
|
153
|
+
if @rpc_failure_callback.nil?
|
|
154
|
+
exc = RuntimeError.new 'Unable to invoke rpc_failed, no failure callback is setup.'
|
|
155
|
+
log_error exc.message
|
|
156
|
+
raise exc
|
|
157
|
+
end
|
|
158
|
+
error = message.is_a?(String) ? RpcFailed.new(message) : message
|
|
159
|
+
log_warn '[service] RPC Failed: %s' % error.message
|
|
160
|
+
@rpc_failure_callback.call(error)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Callback register for the server to be notified
|
|
164
|
+
# when it is appropriate to generate a response to the client.
|
|
165
|
+
# Used in conjunciton with Service#send_response.
|
|
166
|
+
#
|
|
167
|
+
def on_send_response &responder
|
|
168
|
+
@responder = responder
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Tell the server to generate response and send it to the client.
|
|
172
|
+
#
|
|
173
|
+
# NOTE: If @async_responder is set to true, this MUST be called by
|
|
174
|
+
# the implementing service method, otherwise the connection
|
|
175
|
+
# will timeout since no data will be sent.
|
|
176
|
+
#
|
|
177
|
+
def send_response
|
|
178
|
+
if @responder.nil?
|
|
179
|
+
exc = RuntimeError.new "Unable to send response, responder is nil. It appears you aren't inside of an RPC request/response cycle."
|
|
180
|
+
log_error exc.message
|
|
181
|
+
raise exc
|
|
182
|
+
end
|
|
183
|
+
@responder.call @response
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
private
|
|
187
|
+
|
|
188
|
+
# Call the rpc method that was previously privatized.
|
|
189
|
+
# call_rpc allows us to wrap the normal method call with
|
|
190
|
+
# before and after behavior, most notably setting up the request
|
|
191
|
+
# and response instances.
|
|
192
|
+
#
|
|
193
|
+
# Implementing rpc methods should be aware
|
|
194
|
+
# that request and response are implicitly available, and
|
|
195
|
+
# that response should be manipulated during the rpc method,
|
|
196
|
+
# as there is no way to reliably determine the response like
|
|
197
|
+
# a normal (http-based) controller method would be able to.
|
|
198
|
+
#
|
|
199
|
+
# Async behavior of responding can be achieved in the rpc method
|
|
200
|
+
# by explicitly setting self.async_responder = true. It is then
|
|
201
|
+
# the responsibility of the service method to send the response,
|
|
202
|
+
# by calling self.send_response without any arguments. The rpc
|
|
203
|
+
# server is setup to handle synchronous and asynchronous responses.
|
|
204
|
+
#
|
|
205
|
+
def call_rpc method, pb_request
|
|
206
|
+
@current_method = method
|
|
207
|
+
|
|
208
|
+
# Allows the service to set whether or not
|
|
209
|
+
# it would like to asynchronously respond to the connected client(s)
|
|
210
|
+
@async_responder = false
|
|
211
|
+
|
|
212
|
+
begin
|
|
213
|
+
# Setup the request
|
|
214
|
+
@request = rpcs[method].request_type.new
|
|
215
|
+
@request.parse_from_string pb_request.request_proto
|
|
216
|
+
rescue
|
|
217
|
+
exc = BadRequestProto.new 'Unable to parse request: %s' % $!.message
|
|
218
|
+
log_error exc.message
|
|
219
|
+
log_error $!.backtrace.join("\n")
|
|
220
|
+
raise exc
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Setup the response
|
|
224
|
+
@response = rpcs[method].response_type.new
|
|
225
|
+
|
|
226
|
+
log_debug '[service] calling service method %s#%s' % [self.class.name, method]
|
|
227
|
+
# Call the aliased rpc method (e.g. :rpc_find for :find)
|
|
228
|
+
__send__("rpc_#{method}".to_sym)
|
|
229
|
+
log_debug '[service] completed service method %s#%s' % [self.class.name, method]
|
|
230
|
+
|
|
231
|
+
# Pass the populated response back to the server
|
|
232
|
+
# Note this will only get called if the rpc method didn't explode (by design)
|
|
233
|
+
if @async_responder
|
|
234
|
+
log_debug '[service] async request, not sending response (yet)'
|
|
235
|
+
else
|
|
236
|
+
log_debug '[service] trigger server send_response'
|
|
237
|
+
send_response
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
end
|
|
244
|
+
end
|