protobuf 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|