protobuf 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (113) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +28 -0
  4. data/README.md +216 -0
  5. data/Rakefile +1 -0
  6. data/bin/rpc_server +117 -0
  7. data/bin/rprotoc +46 -0
  8. data/examples/addressbook.pb.rb +55 -0
  9. data/examples/addressbook.proto +24 -0
  10. data/examples/reading_a_message.rb +32 -0
  11. data/examples/writing_a_message.rb +46 -0
  12. data/lib/protobuf.rb +6 -0
  13. data/lib/protobuf/common/exceptions.rb +11 -0
  14. data/lib/protobuf/common/logger.rb +64 -0
  15. data/lib/protobuf/common/util.rb +59 -0
  16. data/lib/protobuf/common/wire_type.rb +10 -0
  17. data/lib/protobuf/compiler/compiler.rb +52 -0
  18. data/lib/protobuf/compiler/nodes.rb +323 -0
  19. data/lib/protobuf/compiler/proto.y +216 -0
  20. data/lib/protobuf/compiler/proto2.ebnf +79 -0
  21. data/lib/protobuf/compiler/proto_parser.rb +1425 -0
  22. data/lib/protobuf/compiler/template/rpc_bin.erb +4 -0
  23. data/lib/protobuf/compiler/template/rpc_client.erb +18 -0
  24. data/lib/protobuf/compiler/template/rpc_service.erb +25 -0
  25. data/lib/protobuf/compiler/template/rpc_service_implementation.erb +42 -0
  26. data/lib/protobuf/compiler/visitors.rb +302 -0
  27. data/lib/protobuf/descriptor/descriptor.proto +286 -0
  28. data/lib/protobuf/descriptor/descriptor.rb +55 -0
  29. data/lib/protobuf/descriptor/descriptor_builder.rb +143 -0
  30. data/lib/protobuf/descriptor/descriptor_proto.rb +138 -0
  31. data/lib/protobuf/descriptor/enum_descriptor.rb +33 -0
  32. data/lib/protobuf/descriptor/field_descriptor.rb +49 -0
  33. data/lib/protobuf/descriptor/file_descriptor.rb +37 -0
  34. data/lib/protobuf/message/decoder.rb +83 -0
  35. data/lib/protobuf/message/encoder.rb +46 -0
  36. data/lib/protobuf/message/enum.rb +62 -0
  37. data/lib/protobuf/message/extend.rb +8 -0
  38. data/lib/protobuf/message/field.rb +701 -0
  39. data/lib/protobuf/message/message.rb +402 -0
  40. data/lib/protobuf/message/protoable.rb +38 -0
  41. data/lib/protobuf/rpc/buffer.rb +74 -0
  42. data/lib/protobuf/rpc/client.rb +268 -0
  43. data/lib/protobuf/rpc/client_connection.rb +225 -0
  44. data/lib/protobuf/rpc/error.rb +34 -0
  45. data/lib/protobuf/rpc/error/client_error.rb +31 -0
  46. data/lib/protobuf/rpc/error/server_error.rb +43 -0
  47. data/lib/protobuf/rpc/rpc.pb.rb +107 -0
  48. data/lib/protobuf/rpc/server.rb +183 -0
  49. data/lib/protobuf/rpc/service.rb +244 -0
  50. data/lib/protobuf/rpc/stat.rb +70 -0
  51. data/lib/protobuf/version.rb +3 -0
  52. data/proto/rpc.proto +73 -0
  53. data/protobuf.gemspec +25 -0
  54. data/script/mk_parser +2 -0
  55. data/spec/functional/embedded_service_spec.rb +7 -0
  56. data/spec/proto/test.pb.rb +31 -0
  57. data/spec/proto/test.proto +31 -0
  58. data/spec/proto/test_service.rb +30 -0
  59. data/spec/proto/test_service_impl.rb +17 -0
  60. data/spec/spec_helper.rb +26 -0
  61. data/spec/unit/client_spec.rb +128 -0
  62. data/spec/unit/common/logger_spec.rb +121 -0
  63. data/spec/unit/enum_spec.rb +13 -0
  64. data/spec/unit/message_spec.rb +67 -0
  65. data/spec/unit/server_spec.rb +27 -0
  66. data/spec/unit/service_spec.rb +75 -0
  67. data/test/check_unbuild.rb +30 -0
  68. data/test/data/data.bin +3 -0
  69. data/test/data/data_source.py +14 -0
  70. data/test/data/types.bin +0 -0
  71. data/test/data/types_source.py +22 -0
  72. data/test/data/unk.png +0 -0
  73. data/test/proto/addressbook.pb.rb +66 -0
  74. data/test/proto/addressbook.proto +33 -0
  75. data/test/proto/addressbook_base.pb.rb +58 -0
  76. data/test/proto/addressbook_base.proto +26 -0
  77. data/test/proto/addressbook_ext.pb.rb +20 -0
  78. data/test/proto/addressbook_ext.proto +6 -0
  79. data/test/proto/collision.pb.rb +17 -0
  80. data/test/proto/collision.proto +5 -0
  81. data/test/proto/ext_collision.pb.rb +24 -0
  82. data/test/proto/ext_collision.proto +8 -0
  83. data/test/proto/ext_range.pb.rb +22 -0
  84. data/test/proto/ext_range.proto +7 -0
  85. data/test/proto/float_default.proto +10 -0
  86. data/test/proto/lowercase.pb.rb +30 -0
  87. data/test/proto/lowercase.proto +9 -0
  88. data/test/proto/merge.pb.rb +39 -0
  89. data/test/proto/merge.proto +15 -0
  90. data/test/proto/nested.pb.rb +30 -0
  91. data/test/proto/nested.proto +9 -0
  92. data/test/proto/optional_field.pb.rb +35 -0
  93. data/test/proto/optional_field.proto +12 -0
  94. data/test/proto/packed.pb.rb +22 -0
  95. data/test/proto/packed.proto +6 -0
  96. data/test/proto/rpc.proto +6 -0
  97. data/test/proto/types.pb.rb +84 -0
  98. data/test/proto/types.proto +37 -0
  99. data/test/test_addressbook.rb +56 -0
  100. data/test/test_compiler.rb +325 -0
  101. data/test/test_descriptor.rb +122 -0
  102. data/test/test_enum_value.rb +41 -0
  103. data/test/test_extension.rb +36 -0
  104. data/test/test_lowercase.rb +11 -0
  105. data/test/test_message.rb +128 -0
  106. data/test/test_optional_field.rb +103 -0
  107. data/test/test_packed_field.rb +40 -0
  108. data/test/test_parse.rb +15 -0
  109. data/test/test_repeated_types.rb +132 -0
  110. data/test/test_serialize.rb +61 -0
  111. data/test/test_standard_message.rb +96 -0
  112. data/test/test_types.rb +226 -0
  113. 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