# Copyright 2015 gRPC authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. require 'spec_helper' def load_test_certs test_root = File.join(File.dirname(File.dirname(__FILE__)), 'testdata') files = ['ca.pem', 'server1.key', 'server1.pem'] files.map { |f| File.open(File.join(test_root, f)).read } end def check_md(wanted_md, received_md) wanted_md.zip(received_md).each do |w, r| w.each do |key, value| expect(r[key]).to eq(value) end end end # A test service with no methods. class EmptyService include GRPC::GenericService end # A test service without an implementation. class NoRpcImplementation include GRPC::GenericService rpc :an_rpc, EchoMsg, EchoMsg end # A test service with an implementation that fails with BadStatus class FailingService include GRPC::GenericService rpc :an_rpc, EchoMsg, EchoMsg attr_reader :details, :code, :md def initialize(_default_var = 'ignored') @details = 'app error' @code = 101 @md = { 'failed_method' => 'an_rpc' } end def an_rpc(_req, _call) fail GRPC::BadStatus.new(@code, @details, @md) end end FailingStub = FailingService.rpc_stub_class # A slow test service. class SlowService include GRPC::GenericService rpc :an_rpc, EchoMsg, EchoMsg attr_reader :received_md, :delay def initialize(_default_var = 'ignored') @delay = 0.25 @received_md = [] end def an_rpc(req, call) GRPC.logger.info("starting a slow #{@delay} rpc") sleep @delay @received_md << call.metadata unless call.metadata.nil? req # send back the req as the response end end SlowStub = SlowService.rpc_stub_class # A test service that allows a synchronized RPC cancellation class SynchronizedCancellationService include GRPC::GenericService rpc :an_rpc, EchoMsg, EchoMsg attr_reader :received_md, :delay # notify_request_received and wait_until_rpc_cancelled are # callbacks to synchronously allow the client to proceed with # cancellation (after the unary request has been received), # and to synchronously wait until the client has cancelled the # current RPC. def initialize(notify_request_received, wait_until_rpc_cancelled) @notify_request_received = notify_request_received @wait_until_rpc_cancelled = wait_until_rpc_cancelled end def an_rpc(req, _call) GRPC.logger.info('starting a synchronusly cancelled rpc') @notify_request_received.call(req) @wait_until_rpc_cancelled.call req # send back the req as the response end end SynchronizedCancellationStub = SynchronizedCancellationService.rpc_stub_class # a test service that hangs onto call objects # and uses them after the server-side call has been # finished class CheckCallAfterFinishedService include GRPC::GenericService rpc :an_rpc, EchoMsg, EchoMsg rpc :a_client_streaming_rpc, stream(EchoMsg), EchoMsg rpc :a_server_streaming_rpc, EchoMsg, stream(EchoMsg) rpc :a_bidi_rpc, stream(EchoMsg), stream(EchoMsg) attr_reader :server_side_call def an_rpc(req, call) fail 'shouldnt reuse service' unless @server_side_call.nil? @server_side_call = call req end def a_client_streaming_rpc(call) fail 'shouldnt reuse service' unless @server_side_call.nil? @server_side_call = call # iterate through requests so call can complete call.each_remote_read.each { |r| GRPC.logger.info(r) } EchoMsg.new end def a_server_streaming_rpc(_, call) fail 'shouldnt reuse service' unless @server_side_call.nil? @server_side_call = call [EchoMsg.new, EchoMsg.new] end def a_bidi_rpc(requests, call) fail 'shouldnt reuse service' unless @server_side_call.nil? @server_side_call = call requests.each { |r| GRPC.logger.info(r) } [EchoMsg.new, EchoMsg.new] end end CheckCallAfterFinishedServiceStub = CheckCallAfterFinishedService.rpc_stub_class # A service with a bidi streaming method. class BidiService include GRPC::GenericService rpc :server_sends_bad_input, stream(EchoMsg), stream(EchoMsg) def server_sends_bad_input(_, _) 'bad response. (not an enumerable, client sees an error)' end end BidiStub = BidiService.rpc_stub_class describe GRPC::RpcServer do RpcServer = GRPC::RpcServer StatusCodes = GRPC::Core::StatusCodes before(:each) do @method = 'an_rpc_method' @pass = 0 @fail = 1 @noop = proc { |x| x } end describe '#new' do it 'can be created with just some args' do opts = { server_args: { a_channel_arg: 'an_arg' } } blk = proc do new_rpc_server_for_testing(**opts) end expect(&blk).not_to raise_error end it 'cannot be created with invalid ServerCredentials' do blk = proc do opts = { server_args: { a_channel_arg: 'an_arg' }, creds: Object.new } new_rpc_server_for_testing(**opts) end expect(&blk).to raise_error end end describe '#stopped?' do before(:each) do opts = { server_args: { a_channel_arg: 'an_arg' }, poll_period: 1.5 } @srv = new_rpc_server_for_testing(**opts) @srv.add_http2_port('0.0.0.0:0', :this_port_is_insecure) end it 'starts out false' do expect(@srv.stopped?).to be(false) end it 'stays false after the server starts running', server: true do @srv.handle(EchoService) t = Thread.new { @srv.run } @srv.wait_till_running expect(@srv.stopped?).to be(false) @srv.stop t.join end it 'is true after a running server is stopped', server: true do @srv.handle(EchoService) t = Thread.new { @srv.run } @srv.wait_till_running @srv.stop t.join expect(@srv.stopped?).to be(true) end end describe '#running?' do it 'starts out false' do opts = { server_args: { a_channel_arg: 'an_arg' } } r = new_rpc_server_for_testing(**opts) expect(r.running?).to be(false) end it 'is false if run is called with no services registered', server: true do opts = { server_args: { a_channel_arg: 'an_arg' }, poll_period: 2 } r = new_rpc_server_for_testing(**opts) r.add_http2_port('0.0.0.0:0', :this_port_is_insecure) expect { r.run }.to raise_error(RuntimeError) end it 'is true after run is called with a registered service' do opts = { server_args: { a_channel_arg: 'an_arg' }, poll_period: 2.5 } r = new_rpc_server_for_testing(**opts) r.add_http2_port('0.0.0.0:0', :this_port_is_insecure) r.handle(EchoService) t = Thread.new { r.run } r.wait_till_running expect(r.running?).to be(true) r.stop t.join end end describe '#handle' do before(:each) do @opts = { server_args: { a_channel_arg: 'an_arg' }, poll_period: 1 } @srv = new_rpc_server_for_testing(**@opts) @srv.add_http2_port('0.0.0.0:0', :this_port_is_insecure) end it 'raises if #run has already been called' do @srv.handle(EchoService) t = Thread.new { @srv.run } @srv.wait_till_running expect { @srv.handle(EchoService) }.to raise_error @srv.stop t.join end it 'raises if the server has been run and stopped' do @srv.handle(EchoService) t = Thread.new { @srv.run } @srv.wait_till_running @srv.stop t.join expect { @srv.handle(EchoService) }.to raise_error end it 'raises if the service does not include GenericService ' do expect { @srv.handle(Object) }.to raise_error end it 'raises if the service does not declare any rpc methods' do expect { @srv.handle(EmptyService) }.to raise_error end it 'raises if a handler method is already registered' do @srv.handle(EchoService) expect { r.handle(EchoService) }.to raise_error end end describe '#run' do let(:client_opts) { { channel_override: @ch } } let(:marshal) { EchoService.rpc_descs[:an_rpc].marshal_proc } let(:unmarshal) { EchoService.rpc_descs[:an_rpc].unmarshal_proc(:output) } context 'with no connect_metadata' do before(:each) do server_opts = { poll_period: 1 } @srv = new_rpc_server_for_testing(**server_opts) server_port = @srv.add_http2_port('0.0.0.0:0', :this_port_is_insecure) @host = "localhost:#{server_port}" @ch = GRPC::Core::Channel.new(@host, nil, :this_channel_is_insecure) end it 'should return NOT_FOUND status on unknown methods', server: true do @srv.handle(EchoService) t = Thread.new { @srv.run } @srv.wait_till_running req = EchoMsg.new blk = proc do stub = GRPC::ClientStub.new(@host, :this_channel_is_insecure, **client_opts) stub.request_response('/unknown', req, marshal, unmarshal) end expect(&blk).to raise_error GRPC::BadStatus @srv.stop t.join end it 'should return UNIMPLEMENTED on unimplemented methods', server: true do @srv.handle(NoRpcImplementation) t = Thread.new { @srv.run } @srv.wait_till_running req = EchoMsg.new blk = proc do stub = GRPC::ClientStub.new(@host, :this_channel_is_insecure, **client_opts) stub.request_response('/an_rpc', req, marshal, unmarshal) end expect(&blk).to raise_error do |error| expect(error).to be_a(GRPC::BadStatus) expect(error.code).to be(GRPC::Core::StatusCodes::UNIMPLEMENTED) end @srv.stop t.join end it 'should return UNIMPLEMENTED on unimplemented ' \ 'methods for client_streamer', server: true do @srv.handle(EchoService) t = Thread.new { @srv.run } @srv.wait_till_running blk = proc do stub = EchoStub.new(@host, :this_channel_is_insecure, **client_opts) requests = [EchoMsg.new, EchoMsg.new] stub.a_client_streaming_rpc_unimplemented(requests) end begin expect(&blk).to raise_error do |error| expect(error).to be_a(GRPC::BadStatus) expect(error.code).to eq(GRPC::Core::StatusCodes::UNIMPLEMENTED) end ensure @srv.stop # should be call not to crash t.join end end it 'should handle multiple sequential requests', server: true do @srv.handle(EchoService) t = Thread.new { @srv.run } @srv.wait_till_running req = EchoMsg.new n = 5 # arbitrary stub = EchoStub.new(@host, :this_channel_is_insecure, **client_opts) n.times { expect(stub.an_rpc(req)).to be_a(EchoMsg) } @srv.stop t.join end it 'should receive metadata sent as rpc keyword args', server: true do service = EchoService.new @srv.handle(service) t = Thread.new { @srv.run } @srv.wait_till_running req = EchoMsg.new stub = EchoStub.new(@host, :this_channel_is_insecure, **client_opts) expect(stub.an_rpc(req, metadata: { k1: 'v1', k2: 'v2' })) .to be_a(EchoMsg) wanted_md = [{ 'k1' => 'v1', 'k2' => 'v2' }] check_md(wanted_md, service.received_md) @srv.stop t.join end it 'should receive metadata if a deadline is specified', server: true do service = SlowService.new @srv.handle(service) t = Thread.new { @srv.run } @srv.wait_till_running req = EchoMsg.new stub = SlowStub.new(@host, :this_channel_is_insecure, **client_opts) timeout = service.delay + 1.0 deadline = GRPC::Core::TimeConsts.from_relative_time(timeout) resp = stub.an_rpc(req, deadline: deadline, metadata: { k1: 'v1', k2: 'v2' }) expect(resp).to be_a(EchoMsg) wanted_md = [{ 'k1' => 'v1', 'k2' => 'v2' }] check_md(wanted_md, service.received_md) @srv.stop t.join end it 'should handle cancellation correctly', server: true do request_received = false request_received_mu = Mutex.new request_received_cv = ConditionVariable.new notify_request_received = proc do |req| request_received_mu.synchronize do fail 'req is nil' if req.nil? expect(req.is_a?(EchoMsg)).to be true fail 'test bug - already set' if request_received request_received = true request_received_cv.signal end end rpc_cancelled = false rpc_cancelled_mu = Mutex.new rpc_cancelled_cv = ConditionVariable.new wait_until_rpc_cancelled = proc do rpc_cancelled_mu.synchronize do loop do break if rpc_cancelled rpc_cancelled_cv.wait(rpc_cancelled_mu) end end end service = SynchronizedCancellationService.new(notify_request_received, wait_until_rpc_cancelled) @srv.handle(service) srv_thd = Thread.new { @srv.run } @srv.wait_till_running req = EchoMsg.new stub = SynchronizedCancellationStub.new(@host, :this_channel_is_insecure, **client_opts) op = stub.an_rpc(req, return_op: true) client_thd = Thread.new do expect { op.execute }.to raise_error GRPC::Cancelled end request_received_mu.synchronize do loop do break if request_received request_received_cv.wait(request_received_mu) end end op.cancel rpc_cancelled_mu.synchronize do fail 'test bug - already set' if rpc_cancelled rpc_cancelled = true rpc_cancelled_cv.signal end client_thd.join @srv.stop srv_thd.join end it 'should handle multiple parallel requests', server: true do @srv.handle(EchoService) t = Thread.new { @srv.run } @srv.wait_till_running req, q = EchoMsg.new, Queue.new n = 5 # arbitrary threads = [t] n.times do threads << Thread.new do stub = EchoStub.new(@host, :this_channel_is_insecure, **client_opts) q << stub.an_rpc(req) end end n.times { expect(q.pop).to be_a(EchoMsg) } @srv.stop threads.each(&:join) end it 'should return RESOURCE_EXHAUSTED on too many jobs', server: true do opts = { server_args: { a_channel_arg: 'an_arg' }, pool_size: 2, poll_period: 1, max_waiting_requests: 1 } alt_srv = new_rpc_server_for_testing(**opts) alt_srv.handle(SlowService) alt_port = alt_srv.add_http2_port('0.0.0.0:0', :this_port_is_insecure) alt_host = "0.0.0.0:#{alt_port}" t = Thread.new { alt_srv.run } alt_srv.wait_till_running req = EchoMsg.new n = 20 # arbitrary, use as many to ensure the server pool is exceeded threads = [] one_failed_as_unavailable = false n.times do threads << Thread.new do stub = SlowStub.new(alt_host, :this_channel_is_insecure) begin stub.an_rpc(req) rescue GRPC::ResourceExhausted one_failed_as_unavailable = true end end end threads.each(&:join) alt_srv.stop t.join expect(one_failed_as_unavailable).to be(true) end it 'should send a status UNKNOWN with a relevant message when the' \ 'servers response stream is not an enumerable' do @srv.handle(BidiService) t = Thread.new { @srv.run } @srv.wait_till_running stub = BidiStub.new(@host, :this_channel_is_insecure, **client_opts) responses = stub.server_sends_bad_input([]) exception = nil begin responses.each { |r| r } rescue GRPC::Unknown => e exception = e end # Erroneous responses sent from the server handler should cause an # exception on the client with relevant info. expected_details = 'NoMethodError: undefined method `each\' for '\ '"bad response. (not an enumerable, client sees an error)"' expect(exception.inspect.include?(expected_details)).to be true @srv.stop t.join end end context 'with connect metadata' do let(:test_md_proc) do proc do |mth, md| res = md.clone res['method'] = mth res['connect_k1'] = 'connect_v1' res end end before(:each) do server_opts = { poll_period: 1, connect_md_proc: test_md_proc } @srv = new_rpc_server_for_testing(**server_opts) alt_port = @srv.add_http2_port('0.0.0.0:0', :this_port_is_insecure) @alt_host = "0.0.0.0:#{alt_port}" end it 'should send connect metadata to the client', server: true do service = EchoService.new @srv.handle(service) t = Thread.new { @srv.run } @srv.wait_till_running req = EchoMsg.new stub = EchoStub.new(@alt_host, :this_channel_is_insecure) op = stub.an_rpc(req, metadata: { k1: 'v1', k2: 'v2' }, return_op: true) expect(op.metadata).to be nil expect(op.execute).to be_a(EchoMsg) wanted_md = { 'k1' => 'v1', 'k2' => 'v2', 'method' => '/EchoService/an_rpc', 'connect_k1' => 'connect_v1' } wanted_md.each do |key, value| GRPC.logger.info("key: #{key}") expect(op.metadata[key]).to eq(value) end @srv.stop t.join end end context 'with trailing metadata' do before(:each) do server_opts = { poll_period: 1 } @srv = new_rpc_server_for_testing(**server_opts) alt_port = @srv.add_http2_port('0.0.0.0:0', :this_port_is_insecure) @alt_host = "0.0.0.0:#{alt_port}" end it 'should be added to BadStatus when requests fail', server: true do service = FailingService.new @srv.handle(service) t = Thread.new { @srv.run } @srv.wait_till_running req = EchoMsg.new stub = FailingStub.new(@alt_host, :this_channel_is_insecure) blk = proc { stub.an_rpc(req) } # confirm it raise the expected error expect(&blk).to raise_error GRPC::BadStatus # call again and confirm exception contained the trailing metadata. begin blk.call rescue GRPC::BadStatus => e expect(e.code).to eq(service.code) expect(e.details).to eq(service.details) expect(e.metadata).to eq(service.md) end @srv.stop t.join end it 'should be received by the client', server: true do wanted_trailers = { 'k1' => 'out_v1', 'k2' => 'out_v2' } service = EchoService.new(k1: 'out_v1', k2: 'out_v2') @srv.handle(service) t = Thread.new { @srv.run } @srv.wait_till_running req = EchoMsg.new stub = EchoStub.new(@alt_host, :this_channel_is_insecure) op = stub.an_rpc(req, return_op: true, metadata: { k1: 'v1', k2: 'v2' }) expect(op.metadata).to be nil expect(op.execute).to be_a(EchoMsg) expect(op.trailing_metadata).to eq(wanted_trailers) @srv.stop t.join end end context 'when call objects are used after calls have completed' do before(:each) do server_opts = { poll_period: 1 } @srv = new_rpc_server_for_testing(**server_opts) alt_port = @srv.add_http2_port('0.0.0.0:0', :this_port_is_insecure) @alt_host = "0.0.0.0:#{alt_port}" @service = CheckCallAfterFinishedService.new @srv.handle(@service) @srv_thd = Thread.new { @srv.run } @srv.wait_till_running end # check that the server-side call is still in a usable state even # after it has finished def check_single_req_view_of_finished_call(call) common_check_of_finished_server_call(call) expect(call.peer).to be_a(String) expect(call.peer_cert).to be(nil) end def check_multi_req_view_of_finished_call(call) common_check_of_finished_server_call(call) expect do call.each_remote_read.each { |r| p r } end.to raise_error(GRPC::Core::CallError) end def common_check_of_finished_server_call(call) expect do call.merge_metadata_to_send({}) end.to raise_error(RuntimeError) expect do call.send_initial_metadata end.to_not raise_error expect(call.cancelled?).to be(false) expect(call.metadata).to be_a(Hash) expect(call.metadata['user-agent']).to be_a(String) expect(call.metadata_sent).to be(true) expect(call.output_metadata).to eq({}) expect(call.metadata_to_send).to eq({}) expect(call.deadline.is_a?(Time)).to be(true) end it 'should not crash when call used after an unary call is finished' do req = EchoMsg.new stub = CheckCallAfterFinishedServiceStub.new(@alt_host, :this_channel_is_insecure) resp = stub.an_rpc(req) expect(resp).to be_a(EchoMsg) @srv.stop @srv_thd.join check_single_req_view_of_finished_call(@service.server_side_call) end it 'should not crash when call used after client streaming finished' do requests = [EchoMsg.new, EchoMsg.new] stub = CheckCallAfterFinishedServiceStub.new(@alt_host, :this_channel_is_insecure) resp = stub.a_client_streaming_rpc(requests) expect(resp).to be_a(EchoMsg) @srv.stop @srv_thd.join check_multi_req_view_of_finished_call(@service.server_side_call) end it 'should not crash when call used after server streaming finished' do req = EchoMsg.new stub = CheckCallAfterFinishedServiceStub.new(@alt_host, :this_channel_is_insecure) responses = stub.a_server_streaming_rpc(req) responses.each do |r| expect(r).to be_a(EchoMsg) end @srv.stop @srv_thd.join check_single_req_view_of_finished_call(@service.server_side_call) end it 'should not crash when call used after a bidi call is finished' do requests = [EchoMsg.new, EchoMsg.new] stub = CheckCallAfterFinishedServiceStub.new(@alt_host, :this_channel_is_insecure) responses = stub.a_bidi_rpc(requests) responses.each do |r| expect(r).to be_a(EchoMsg) end @srv.stop @srv_thd.join check_multi_req_view_of_finished_call(@service.server_side_call) end end end end