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.
- data/.gitignore +3 -0
- data/.yardopts +5 -0
- data/Gemfile.lock +25 -10
- data/bin/rpc_server +38 -33
- data/lib/protobuf.rb +22 -3
- data/lib/protobuf/common/logger.rb +6 -8
- data/lib/protobuf/compiler/visitors.rb +8 -9
- data/lib/protobuf/descriptor/descriptor_builder.rb +6 -6
- data/lib/protobuf/ext/eventmachine.rb +2 -4
- data/lib/protobuf/message/message.rb +1 -3
- data/lib/protobuf/rpc/buffer.rb +6 -6
- data/lib/protobuf/rpc/client.rb +59 -21
- data/lib/protobuf/rpc/connector.rb +10 -9
- data/lib/protobuf/rpc/connectors/base.rb +23 -8
- data/lib/protobuf/rpc/connectors/common.rb +155 -0
- data/lib/protobuf/rpc/connectors/em_client.rb +23 -192
- data/lib/protobuf/rpc/connectors/eventmachine.rb +36 -44
- data/lib/protobuf/rpc/connectors/socket.rb +58 -1
- data/lib/protobuf/rpc/error.rb +6 -14
- data/lib/protobuf/rpc/server.rb +72 -99
- data/lib/protobuf/rpc/servers/evented_runner.rb +32 -0
- data/lib/protobuf/rpc/servers/evented_server.rb +29 -0
- data/lib/protobuf/rpc/servers/socket_runner.rb +17 -0
- data/lib/protobuf/rpc/servers/socket_server.rb +145 -0
- data/lib/protobuf/rpc/service.rb +50 -51
- data/lib/protobuf/rpc/stat.rb +2 -2
- data/lib/protobuf/version.rb +1 -1
- data/protobuf.gemspec +9 -4
- data/spec/helper/all.rb +1 -7
- data/spec/helper/server.rb +45 -5
- data/spec/helper/silent_constants.rb +40 -0
- data/spec/proto/test_service.rb +0 -1
- data/spec/proto/test_service_impl.rb +4 -3
- data/spec/spec_helper.rb +19 -6
- data/spec/unit/enum_spec.rb +4 -4
- data/spec/unit/rpc/client_spec.rb +32 -42
- data/spec/unit/rpc/connector_spec.rb +11 -16
- data/spec/unit/rpc/connectors/base_spec.rb +14 -3
- data/spec/unit/rpc/connectors/common_spec.rb +132 -0
- data/spec/unit/rpc/connectors/{eventmachine/client_spec.rb → eventmachine_client_spec.rb} +0 -0
- data/spec/unit/rpc/connectors/socket_spec.rb +49 -0
- data/spec/unit/rpc/servers/evented_server_spec.rb +18 -0
- data/spec/unit/rpc/servers/socket_server_spec.rb +57 -0
- metadata +86 -16
- 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
|
-
|
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
|
-
|
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
|
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 ||=
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
54
|
-
|
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
|
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
|
data/lib/protobuf/rpc/error.rb
CHANGED
@@ -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
|
-
|
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'
|
data/lib/protobuf/rpc/server.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
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
|
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
|
-
|
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
|
89
|
-
@service.__send__
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
158
|
-
|
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
|
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
|