diff options
Diffstat (limited to 'src/ruby')
22 files changed, 757 insertions, 82 deletions
diff --git a/src/ruby/ext/grpc/rb_byte_buffer.c b/src/ruby/ext/grpc/rb_byte_buffer.c index f97890e4a2..47fd6d9120 100644 --- a/src/ruby/ext/grpc/rb_byte_buffer.c +++ b/src/ruby/ext/grpc/rb_byte_buffer.c @@ -65,5 +65,6 @@ VALUE grpc_rb_byte_buffer_to_s(grpc_byte_buffer *buffer) { GRPC_SLICE_LENGTH(next)); grpc_slice_unref(next); } + grpc_byte_buffer_reader_destroy(&reader); return rb_string; } diff --git a/src/ruby/ext/grpc/rb_grpc_imports.generated.c b/src/ruby/ext/grpc/rb_grpc_imports.generated.c index e214216ece..6c36df9113 100644 --- a/src/ruby/ext/grpc/rb_grpc_imports.generated.c +++ b/src/ruby/ext/grpc/rb_grpc_imports.generated.c @@ -189,6 +189,7 @@ grpc_slice_split_head_type grpc_slice_split_head_import; gpr_empty_slice_type gpr_empty_slice_import; grpc_slice_cmp_type grpc_slice_cmp_import; grpc_slice_str_cmp_type grpc_slice_str_cmp_import; +grpc_slice_is_equivalent_type grpc_slice_is_equivalent_import; grpc_slice_buffer_init_type grpc_slice_buffer_init_import; grpc_slice_buffer_destroy_type grpc_slice_buffer_destroy_import; grpc_slice_buffer_add_type grpc_slice_buffer_add_import; @@ -464,6 +465,7 @@ void grpc_rb_load_imports(HMODULE library) { gpr_empty_slice_import = (gpr_empty_slice_type) GetProcAddress(library, "gpr_empty_slice"); grpc_slice_cmp_import = (grpc_slice_cmp_type) GetProcAddress(library, "grpc_slice_cmp"); grpc_slice_str_cmp_import = (grpc_slice_str_cmp_type) GetProcAddress(library, "grpc_slice_str_cmp"); + grpc_slice_is_equivalent_import = (grpc_slice_is_equivalent_type) GetProcAddress(library, "grpc_slice_is_equivalent"); grpc_slice_buffer_init_import = (grpc_slice_buffer_init_type) GetProcAddress(library, "grpc_slice_buffer_init"); grpc_slice_buffer_destroy_import = (grpc_slice_buffer_destroy_type) GetProcAddress(library, "grpc_slice_buffer_destroy"); grpc_slice_buffer_add_import = (grpc_slice_buffer_add_type) GetProcAddress(library, "grpc_slice_buffer_add"); diff --git a/src/ruby/ext/grpc/rb_grpc_imports.generated.h b/src/ruby/ext/grpc/rb_grpc_imports.generated.h index b137700aa6..5745686adf 100644 --- a/src/ruby/ext/grpc/rb_grpc_imports.generated.h +++ b/src/ruby/ext/grpc/rb_grpc_imports.generated.h @@ -518,6 +518,9 @@ extern grpc_slice_cmp_type grpc_slice_cmp_import; typedef int(*grpc_slice_str_cmp_type)(grpc_slice a, const char *b); extern grpc_slice_str_cmp_type grpc_slice_str_cmp_import; #define grpc_slice_str_cmp grpc_slice_str_cmp_import +typedef int(*grpc_slice_is_equivalent_type)(grpc_slice a, grpc_slice b); +extern grpc_slice_is_equivalent_type grpc_slice_is_equivalent_import; +#define grpc_slice_is_equivalent grpc_slice_is_equivalent_import typedef void(*grpc_slice_buffer_init_type)(grpc_slice_buffer *sb); extern grpc_slice_buffer_init_type grpc_slice_buffer_init_import; #define grpc_slice_buffer_init grpc_slice_buffer_init_import diff --git a/src/ruby/ext/grpc/rb_server.c b/src/ruby/ext/grpc/rb_server.c index 2a6a246e67..c7b112c94b 100644 --- a/src/ruby/ext/grpc/rb_server.c +++ b/src/ruby/ext/grpc/rb_server.c @@ -37,6 +37,7 @@ #include "rb_server.h" #include <grpc/grpc.h> +#include <grpc/support/atm.h> #include <grpc/grpc_security.h> #include <grpc/support/log.h> #include "rb_call.h" @@ -59,22 +60,26 @@ typedef struct grpc_rb_server { /* The actual server */ grpc_server *wrapped; grpc_completion_queue *queue; + gpr_atm shutdown_started; } grpc_rb_server; static void destroy_server(grpc_rb_server *server, gpr_timespec deadline) { grpc_event ev; - if (server->wrapped != NULL) { - grpc_server_shutdown_and_notify(server->wrapped, server->queue, NULL); - ev = rb_completion_queue_pluck(server->queue, NULL, deadline, NULL); - if (ev.type == GRPC_QUEUE_TIMEOUT) { - grpc_server_cancel_all_calls(server->wrapped); - rb_completion_queue_pluck(server->queue, NULL, - gpr_inf_future(GPR_CLOCK_REALTIME), NULL); + // This can be started by app or implicitly by GC. Avoid a race between these. + if (gpr_atm_full_fetch_add(&server->shutdown_started, (gpr_atm)1) == 0) { + if (server->wrapped != NULL) { + grpc_server_shutdown_and_notify(server->wrapped, server->queue, NULL); + ev = rb_completion_queue_pluck(server->queue, NULL, deadline, NULL); + if (ev.type == GRPC_QUEUE_TIMEOUT) { + grpc_server_cancel_all_calls(server->wrapped); + rb_completion_queue_pluck(server->queue, NULL, + gpr_inf_future(GPR_CLOCK_REALTIME), NULL); + } + grpc_server_destroy(server->wrapped); + grpc_rb_completion_queue_destroy(server->queue); + server->wrapped = NULL; + server->queue = NULL; } - grpc_server_destroy(server->wrapped); - grpc_rb_completion_queue_destroy(server->queue); - server->wrapped = NULL; - server->queue = NULL; } } @@ -115,6 +120,7 @@ static const rb_data_type_t grpc_rb_server_data_type = { static VALUE grpc_rb_server_alloc(VALUE cls) { grpc_rb_server *wrapper = ALLOC(grpc_rb_server); wrapper->wrapped = NULL; + wrapper->shutdown_started = (gpr_atm)0; return TypedData_Wrap_Struct(cls, &grpc_rb_server_data_type, wrapper); } diff --git a/src/ruby/lib/grpc/errors.rb b/src/ruby/lib/grpc/errors.rb index 23b2bb7e12..f6998e17c4 100644 --- a/src/ruby/lib/grpc/errors.rb +++ b/src/ruby/lib/grpc/errors.rb @@ -35,9 +35,18 @@ module GRPC # either end of a GRPC connection. When raised, it indicates that a status # error should be returned to the other end of a GRPC connection; when # caught it means that this end received a status error. + # + # There is also subclass of BadStatus in this module for each GRPC status. + # E.g., the GRPC::Cancelled class corresponds to status CANCELLED. + # + # See + # https://github.com/grpc/grpc/blob/master/include/grpc/impl/codegen/status.h + # for detailed descriptions of each status code. class BadStatus < StandardError attr_reader :code, :details, :metadata + include GRPC::Core::StatusCodes + # @param code [Numeric] the status code # @param details [String] the details of the exception # @param metadata [Hash] the error's metadata @@ -55,9 +64,152 @@ module GRPC def to_status Struct::Status.new(code, details, @metadata) end + + def self.new_status_exception(code, details = 'unkown cause', metadata = {}) + codes = {} + codes[OK] = Ok + codes[CANCELLED] = Cancelled + codes[UNKNOWN] = Unknown + codes[INVALID_ARGUMENT] = InvalidArgument + codes[DEADLINE_EXCEEDED] = DeadlineExceeded + codes[NOT_FOUND] = NotFound + codes[ALREADY_EXISTS] = AlreadyExists + codes[PERMISSION_DENIED] = PermissionDenied + codes[UNAUTHENTICATED] = Unauthenticated + codes[RESOURCE_EXHAUSTED] = ResourceExhausted + codes[FAILED_PRECONDITION] = FailedPrecondition + codes[ABORTED] = Aborted + codes[OUT_OF_RANGE] = OutOfRange + codes[UNIMPLEMENTED] = Unimplemented + codes[INTERNAL] = Internal + codes[UNIMPLEMENTED] = Unimplemented + codes[UNAVAILABLE] = Unavailable + codes[DATA_LOSS] = DataLoss + + if codes[code].nil? + BadStatus.new(code, details, metadata) + else + codes[code].new(details, metadata) + end + end + end + + # GRPC status code corresponding to status OK + class Ok < BadStatus + def initialize(details = 'unknown cause', metadata = {}) + super(Core::StatusCodes::OK, details, metadata) + end end - # Cancelled is an exception class that indicates that an rpc was cancelled. - class Cancelled < StandardError + # GRPC status code corresponding to status CANCELLED + class Cancelled < BadStatus + def initialize(details = 'unknown cause', metadata = {}) + super(Core::StatusCodes::CANCELLED, details, metadata) + end + end + + # GRPC status code corresponding to status UNKNOWN + class Unknown < BadStatus + def initialize(details = 'unknown cause', metadata = {}) + super(Core::StatusCodes::UNKNOWN, details, metadata) + end + end + + # GRPC status code corresponding to status INVALID_ARGUMENT + class InvalidArgument < BadStatus + def initialize(details = 'unknown cause', metadata = {}) + super(Core::StatusCodes::INVALID_ARGUMENT, details, metadata) + end + end + + # GRPC status code corresponding to status DEADLINE_EXCEEDED + class DeadlineExceeded < BadStatus + def initialize(details = 'unknown cause', metadata = {}) + super(Core::StatusCodes::DEADLINE_EXCEEDED, details, metadata) + end + end + + # GRPC status code corresponding to status NOT_FOUND + class NotFound < BadStatus + def initialize(details = 'unknown cause', metadata = {}) + super(Core::StatusCodes::NOT_FOUND, details, metadata) + end + end + + # GRPC status code corresponding to status ALREADY_EXISTS + class AlreadyExists < BadStatus + def initialize(details = 'unknown cause', metadata = {}) + super(Core::StatusCodes::ALREADY_EXISTS, details, metadata) + end + end + + # GRPC status code corresponding to status PERMISSION_DENIED + class PermissionDenied < BadStatus + def initialize(details = 'unknown cause', metadata = {}) + super(Core::StatusCodes::PERMISSION_DENIED, details, metadata) + end + end + + # GRPC status code corresponding to status UNAUTHENTICATED + class Unauthenticated < BadStatus + def initialize(details = 'unknown cause', metadata = {}) + super(Core::StatusCodes::UNAUTHENTICATED, details, metadata) + end + end + + # GRPC status code corresponding to status RESOURCE_EXHAUSTED + class ResourceExhausted < BadStatus + def initialize(details = 'unknown cause', metadata = {}) + super(Core::StatusCodes::RESOURCE_EXHAUSTED, details, metadata) + end + end + + # GRPC status code corresponding to status FAILED_PRECONDITION + class FailedPrecondition < BadStatus + def initialize(details = 'unknown cause', metadata = {}) + super(Core::StatusCodes::FAILED_PRECONDITION, details, metadata) + end + end + + # GRPC status code corresponding to status ABORTED + class Aborted < BadStatus + def initialize(details = 'unknown cause', metadata = {}) + super(Core::StatusCodes::ABORTED, details, metadata) + end + end + + # GRPC status code corresponding to status OUT_OF_RANGE + class OutOfRange < BadStatus + def initialize(details = 'unknown cause', metadata = {}) + super(Core::StatusCodes::OUT_OF_RANGE, details, metadata) + end + end + + # GRPC status code corresponding to status UNIMPLEMENTED + class Unimplemented < BadStatus + def initialize(details = 'unknown cause', metadata = {}) + super(Core::StatusCodes::UNIMPLEMENTED, details, metadata) + end + end + + # GRPC status code corresponding to status INTERNAL + class Internal < BadStatus + def initialize(details = 'unknown cause', metadata = {}) + super(Core::StatusCodes::INTERNAL, details, metadata) + end + end + + # GRPC status code corresponding to status UNAVAILABLE + class Unavailable < BadStatus + def initialize(details = 'unknown cause', metadata = {}) + super(Core::StatusCodes::UNAVAILABLE, details, metadata) + end + end + + # GRPC status code corresponding to status DATA_LOSS + class DataLoss < BadStatus + def initialize(details = 'unknown cause', metadata = {}) + super(Core::StatusCodes::DATA_LOSS, details, metadata) + end end end diff --git a/src/ruby/lib/grpc/generic/active_call.rb b/src/ruby/lib/grpc/generic/active_call.rb index f5c426ebfc..3b31f77ec0 100644 --- a/src/ruby/lib/grpc/generic/active_call.rb +++ b/src/ruby/lib/grpc/generic/active_call.rb @@ -43,8 +43,8 @@ class Struct GRPC.logger.debug("Failing with status #{status}") # raise BadStatus, propagating the metadata if present. md = status.metadata - fail GRPC::BadStatus.new(status.code, status.details, md), - "status code: #{status.code}, details: #{status.details}" + fail GRPC::BadStatus.new_status_exception( + status.code, status.details, md) end status end diff --git a/src/ruby/lib/grpc/generic/bidi_call.rb b/src/ruby/lib/grpc/generic/bidi_call.rb index d7cd9e6df2..adc77bbf5d 100644 --- a/src/ruby/lib/grpc/generic/bidi_call.rb +++ b/src/ruby/lib/grpc/generic/bidi_call.rb @@ -200,6 +200,7 @@ module GRPC if is_client batch_result = @call.run_batch(RECV_STATUS_ON_CLIENT => nil) @call.status = batch_result.status + @call.trailing_metadata = @call.status.metadata if @call.status batch_result.check_status GRPC.logger.debug("bidi-read-loop: done status #{@call.status}") end @@ -219,6 +220,10 @@ module GRPC GRPC.logger.debug('bidi-read-loop: finished') @reads_complete = true finished + # Make sure that the write loop is done done before finishing the call. + # Note that blocking is ok at this point because we've already received + # a status + @enq_th.join if is_client end end end diff --git a/src/ruby/lib/grpc/generic/rpc_desc.rb b/src/ruby/lib/grpc/generic/rpc_desc.rb index cd17aed8e7..d46c4a1b5c 100644 --- a/src/ruby/lib/grpc/generic/rpc_desc.rb +++ b/src/ruby/lib/grpc/generic/rpc_desc.rb @@ -119,7 +119,7 @@ module GRPC # Send back a UNKNOWN status to the client GRPC.logger.warn("failed handler: #{active_call}; sending status:UNKNOWN") GRPC.logger.warn(e) - send_status(active_call, UNKNOWN, 'unkown error handling call on server') + send_status(active_call, UNKNOWN, "#{e.class}: #{e.message}") end def assert_arity_matches(mth) diff --git a/src/ruby/lib/grpc/generic/rpc_server.rb b/src/ruby/lib/grpc/generic/rpc_server.rb index 57f99c8ce6..00e0db71be 100644 --- a/src/ruby/lib/grpc/generic/rpc_server.rb +++ b/src/ruby/lib/grpc/generic/rpc_server.rb @@ -31,10 +31,133 @@ require_relative '../grpc' require_relative 'active_call' require_relative 'service' require 'thread' -require 'concurrent' # GRPC contains the General RPC module. module GRPC + # Pool is a simple thread pool. + class Pool + # Default keep alive period is 1s + DEFAULT_KEEP_ALIVE = 1 + + def initialize(size, keep_alive: DEFAULT_KEEP_ALIVE) + fail 'pool size must be positive' unless size > 0 + @jobs = Queue.new + @size = size + @stopped = false + @stop_mutex = Mutex.new # needs to be held when accessing @stopped + @stop_cond = ConditionVariable.new + @workers = [] + @keep_alive = keep_alive + + # Each worker thread has its own queue to push and pull jobs + # these queues are put into @ready_queues when that worker is idle + @ready_workers = Queue.new + end + + # Returns the number of jobs waiting + def jobs_waiting + @jobs.size + end + + def ready_for_work? + # Busy worker threads are either doing work, or have a single job + # waiting on them. Workers that are idle with no jobs waiting + # have their "queues" in @ready_workers + !@ready_workers.empty? + end + + # Runs the given block on the queue with the provided args. + # + # @param args the args passed blk when it is called + # @param blk the block to call + def schedule(*args, &blk) + return if blk.nil? + @stop_mutex.synchronize do + if @stopped + GRPC.logger.warn('did not schedule job, already stopped') + return + end + GRPC.logger.info('schedule another job') + fail 'No worker threads available' if @ready_workers.empty? + worker_queue = @ready_workers.pop + + fail 'worker already has a task waiting' unless worker_queue.empty? + worker_queue << [blk, args] + end + end + + # Starts running the jobs in the thread pool. + def start + @stop_mutex.synchronize do + fail 'already stopped' if @stopped + end + until @workers.size == @size.to_i + new_worker_queue = Queue.new + @ready_workers << new_worker_queue + next_thread = Thread.new(new_worker_queue) do |jobs| + catch(:exit) do # allows { throw :exit } to kill a thread + loop_execute_jobs(jobs) + end + remove_current_thread + end + @workers << next_thread + end + end + + # Stops the jobs in the pool + def stop + GRPC.logger.info('stopping, will wait for all the workers to exit') + schedule { throw :exit } while ready_for_work? + @stop_mutex.synchronize do # wait @keep_alive for works to stop + @stopped = true + @stop_cond.wait(@stop_mutex, @keep_alive) if @workers.size > 0 + end + forcibly_stop_workers + GRPC.logger.info('stopped, all workers are shutdown') + end + + protected + + # Forcibly shutdown any threads that are still alive. + def forcibly_stop_workers + return unless @workers.size > 0 + GRPC.logger.info("forcibly terminating #{@workers.size} worker(s)") + @workers.each do |t| + next unless t.alive? + begin + t.exit + rescue StandardError => e + GRPC.logger.warn('error while terminating a worker') + GRPC.logger.warn(e) + end + end + end + + # removes the threads from workers, and signal when all the + # threads are complete. + def remove_current_thread + @stop_mutex.synchronize do + @workers.delete(Thread.current) + @stop_cond.signal if @workers.size.zero? + end + end + + def loop_execute_jobs(worker_queue) + loop do + begin + blk, args = worker_queue.pop + blk.call(*args) + rescue StandardError => e + GRPC.logger.warn('Error in worker thread') + GRPC.logger.warn(e) + end + # there shouldn't be any work given to this thread while its busy + fail('received a task while busy') unless worker_queue.empty? + @ready_workers << worker_queue + end + end + end + # RpcServer hosts a number of services and makes them available on the # network. class RpcServer @@ -44,17 +167,13 @@ module GRPC def_delegators :@server, :add_http2_port - # Default max size of the thread pool size is 100 - DEFAULT_MAX_POOL_SIZE = 100 - - # Default minimum size of the thread pool is 5 - DEFAULT_MIN_POOL_SIZE = 5 + # Default thread pool size is 30 + DEFAULT_POOL_SIZE = 30 - # Default max_waiting_requests size is 60 - DEFAULT_MAX_WAITING_REQUESTS = 60 + # Deprecated due to internal changes to the thread pool + DEFAULT_MAX_WAITING_REQUESTS = 20 # Default poll period is 1s - # Used for grpc server shutdown and thread pool shutdown timeouts DEFAULT_POLL_PERIOD = 1 # Signal check period is 0.25s @@ -75,12 +194,12 @@ module GRPC # There are some specific keyword args used to configure the RpcServer # instance. # - # * pool_size: the maximum size of the thread pool that the server's - # thread pool can reach. + # * pool_size: the size of the thread pool the server uses to run its + # threads. No more concurrent requests can be made than the size + # of the thread pool # - # * max_waiting_requests: the maximum number of requests that are not - # being handled to allow. When this limit is exceeded, the server responds - # with not available to new requests + # * max_waiting_requests: Deprecated due to internal changes to the thread + # pool. This is still an argument for compatibility but is ignored. # # * poll_period: when present, the server polls for new events with this # period @@ -92,8 +211,7 @@ module GRPC # # * server_args: # A server arguments hash to be passed down to the underlying core server - def initialize(pool_size:DEFAULT_MAX_POOL_SIZE, - min_pool_size:DEFAULT_MIN_POOL_SIZE, + def initialize(pool_size:DEFAULT_POOL_SIZE, max_waiting_requests:DEFAULT_MAX_WAITING_REQUESTS, poll_period:DEFAULT_POLL_PERIOD, connect_md_proc:nil, @@ -101,12 +219,8 @@ module GRPC @connect_md_proc = RpcServer.setup_connect_md_proc(connect_md_proc) @max_waiting_requests = max_waiting_requests @poll_period = poll_period - - @pool = Concurrent::ThreadPoolExecutor.new( - min_threads: [min_pool_size, pool_size].min, - max_threads: pool_size, - max_queue: max_waiting_requests, - fallback_policy: :discard) + @pool_size = pool_size + @pool = Pool.new(@pool_size) @run_cond = ConditionVariable.new @run_mutex = Mutex.new # running_state can take 4 values: :not_started, :running, :stopping, and @@ -127,8 +241,7 @@ module GRPC end deadline = from_relative_time(@poll_period) @server.close(deadline) - @pool.shutdown - @pool.wait_for_termination(@poll_period) + @pool.stop end def running_state @@ -225,6 +338,7 @@ module GRPC def run @run_mutex.synchronize do fail 'cannot run without registering services' if rpc_descs.size.zero? + @pool.start @server.start transition_running_state(:running) @run_cond.broadcast @@ -236,12 +350,8 @@ module GRPC # Sends RESOURCE_EXHAUSTED if there are too many unprocessed jobs def available?(an_rpc) - jobs_count, max = @pool.queue_length, @pool.max_queue - GRPC.logger.info("waiting: #{jobs_count}, max: #{max}") - - # remaining capacity for ThreadPoolExecutors is -1 if unbounded - return an_rpc if @pool.remaining_capacity != 0 - GRPC.logger.warn("NOT AVAILABLE: too many jobs_waiting: #{an_rpc}") + return an_rpc if @pool.ready_for_work? + GRPC.logger.warn('no free worker threads currently') noop = proc { |x| x } # Create a new active call that knows that metadata hasn't been @@ -276,7 +386,7 @@ module GRPC break if (!an_rpc.nil?) && an_rpc.call.nil? active_call = new_active_server_call(an_rpc) unless active_call.nil? - @pool.post(active_call) do |ac| + @pool.schedule(active_call) do |ac| c, mth = ac begin rpc_descs[mth].run_server_method(c, rpc_handlers[mth]) diff --git a/src/ruby/lib/grpc/generic/service.rb b/src/ruby/lib/grpc/generic/service.rb index 7cb9f1cc99..f5a6b49eb4 100644 --- a/src/ruby/lib/grpc/generic/service.rb +++ b/src/ruby/lib/grpc/generic/service.rb @@ -110,8 +110,9 @@ module GRPC rpc_descs[name] = RpcDesc.new(name, input, output, marshal_class_method, unmarshal_class_method) - define_method(name) do - fail GRPC::BadStatus, GRPC::Core::StatusCodes::UNIMPLEMENTED + define_method(GenericService.underscore(name.to_s).to_sym) do |_, _| + fail GRPC::BadStatus.new_status_exception( + GRPC::Core::StatusCodes::UNIMPLEMENTED) end end diff --git a/src/ruby/pb/grpc/health/checker.rb b/src/ruby/pb/grpc/health/checker.rb index 4bce1744c4..6b2d852ebf 100644 --- a/src/ruby/pb/grpc/health/checker.rb +++ b/src/ruby/pb/grpc/health/checker.rb @@ -52,7 +52,9 @@ module Grpc @status_mutex.synchronize do status = @statuses["#{req.service}"] end - fail GRPC::BadStatus, StatusCodes::NOT_FOUND if status.nil? + if status.nil? + fail GRPC::BadStatus.new_status_exception(StatusCodes::NOT_FOUND) + end HealthCheckResponse.new(status: status) end diff --git a/src/ruby/pb/src/proto/grpc/testing/test_services_pb.rb b/src/ruby/pb/src/proto/grpc/testing/test_services_pb.rb index fde328e4c5..7d1072e512 100644 --- a/src/ruby/pb/src/proto/grpc/testing/test_services_pb.rb +++ b/src/ruby/pb/src/proto/grpc/testing/test_services_pb.rb @@ -54,6 +54,10 @@ module Grpc rpc :EmptyCall, Empty, Empty # One request followed by one response. rpc :UnaryCall, SimpleRequest, SimpleResponse + # One request followed by one response. Response has cache control + # headers set such that a caching HTTP proxy (such as GFE) can + # satisfy subsequent requests. + rpc :CacheableUnaryCall, SimpleRequest, SimpleResponse # One request followed by a sequence of responses (streamed download). # The server returns the payload with client desired type and sizes. rpc :StreamingOutputCall, StreamingOutputCallRequest, stream(StreamingOutputCallResponse) @@ -69,6 +73,9 @@ module Grpc # stream of responses are returned to the client when the server starts with # first request. rpc :HalfDuplexCall, stream(StreamingOutputCallRequest), stream(StreamingOutputCallResponse) + # The test server will not implement this method. It will be used + # to test the behavior when clients call unimplemented methods. + rpc :UnimplementedCall, Empty, Empty end Stub = Service.rpc_stub_class diff --git a/src/ruby/pb/test/client.rb b/src/ruby/pb/test/client.rb index 1e3ae65630..9c4ee9c6f2 100755 --- a/src/ruby/pb/test/client.rb +++ b/src/ruby/pb/test/client.rb @@ -158,14 +158,26 @@ def create_stub(opts) GRPC.logger.info("... connecting securely to #{address}") stub_opts[:channel_args].merge!(compression_channel_args) - Grpc::Testing::TestService::Stub.new(address, creds, **stub_opts) + if opts.test_case == "unimplemented_service" + Grpc::Testing::UnimplementedService::Stub.new(address, creds, **stub_opts) + else + Grpc::Testing::TestService::Stub.new(address, creds, **stub_opts) + end else GRPC.logger.info("... connecting insecurely to #{address}") - Grpc::Testing::TestService::Stub.new( - address, - :this_channel_is_insecure, - channel_args: compression_channel_args - ) + if opts.test_case == "unimplemented_service" + Grpc::Testing::UnimplementedService::Stub.new( + address, + :this_channel_is_insecure, + channel_args: compression_channel_args + ) + else + Grpc::Testing::TestService::Stub.new( + address, + :this_channel_is_insecure, + channel_args: compression_channel_args + ) + end end end @@ -459,11 +471,8 @@ class NamedTests deadline = GRPC::Core::TimeConsts::from_relative_time(1) resps = @stub.full_duplex_call(enum.each_item, deadline: deadline) resps.each { } # wait to receive each request (or timeout) - fail 'Should have raised GRPC::BadStatus(DEADLINE_EXCEEDED)' - rescue GRPC::BadStatus => e - assert("#{__callee__}: status was wrong") do - e.code == GRPC::Core::StatusCodes::DEADLINE_EXCEEDED - end + fail 'Should have raised GRPC::DeadlineExceeded' + rescue GRPC::DeadlineExceeded end def empty_stream @@ -505,6 +514,153 @@ class NamedTests op.wait end + def unimplemented_method + begin + resp = @stub.unimplemented_call(Empty.new) + rescue GRPC::Unimplemented => e + return + rescue Exception => e + fail AssertionError, "Expected BadStatus. Received: #{e.inspect}" + end + fail AssertionError, "GRPC::Unimplemented should have been raised. Was not." + end + + def unimplemented_service + begin + resp = @stub.unimplemented_call(Empty.new) + rescue GRPC::Unimplemented => e + return + rescue Exception => e + fail AssertionError, "Expected BadStatus. Received: #{e.inspect}" + end + fail AssertionError, "GRPC::Unimplemented should have been raised. Was not." + end + + def status_code_and_message + + # Function wide constants. + message = "test status method" + code = GRPC::Core::StatusCodes::UNKNOWN + + # Testing with UnaryCall. + payload = Payload.new(type: :COMPRESSABLE, body: nulls(1)) + echo_status = EchoStatus.new(code: code, message: message) + req = SimpleRequest.new(response_type: :COMPRESSABLE, + response_size: 1, + payload: payload, + response_status: echo_status) + seen_correct_exception = false + begin + resp = @stub.unary_call(req) + rescue GRPC::Unknown => e + if e.details != message + fail AssertionError, + "Expected message #{message}. Received: #{e.details}" + end + seen_correct_exception = true + rescue Exception => e + fail AssertionError, "Expected BadStatus. Received: #{e.inspect}" + end + + if not seen_correct_exception + fail AssertionError, "Did not see expected status from UnaryCall" + end + + # testing with FullDuplex + req_cls, p_cls = StreamingOutputCallRequest, ResponseParameters + duplex_req = req_cls.new(payload: Payload.new(body: nulls(1)), + response_type: :COMPRESSABLE, + response_parameters: [p_cls.new(size: 1)], + response_status: echo_status) + seen_correct_exception = false + begin + resp = @stub.full_duplex_call([duplex_req]) + resp.each { |r| } + rescue GRPC::Unknown => e + if e.details != message + fail AssertionError, + "Expected message #{message}. Received: #{e.details}" + end + seen_correct_exception = true + rescue Exception => e + fail AssertionError, "Expected BadStatus. Received: #{e.inspect}" + end + + if not seen_correct_exception + fail AssertionError, "Did not see expected status from FullDuplexCall" + end + + end + + + def custom_metadata + + # Function wide constants + req_size, wanted_response_size = 271_828, 314_159 + initial_metadata_key = "x-grpc-test-echo-initial" + initial_metadata_value = "test_initial_metadata_value" + trailing_metadata_key = "x-grpc-test-echo-trailing-bin" + trailing_metadata_value = "\x0a\x0b\x0a\x0b\x0a\x0b" + + metadata = { + initial_metadata_key => initial_metadata_value, + trailing_metadata_key => trailing_metadata_value + } + + # Testing with UnaryCall + payload = Payload.new(type: :COMPRESSABLE, body: nulls(req_size)) + req = SimpleRequest.new(response_type: :COMPRESSABLE, + response_size: wanted_response_size, + payload: payload) + + op = @stub.unary_call(req, metadata: metadata, return_op: true) + op.execute + if not op.metadata.has_key?(initial_metadata_key) + fail AssertionError, "Expected initial metadata. None received" + elsif op.metadata[initial_metadata_key] != metadata[initial_metadata_key] + fail AssertionError, + "Expected initial metadata: #{metadata[initial_metadata_key]}. "\ + "Received: #{op.metadata[initial_metadata_key]}" + end + if not op.trailing_metadata.has_key?(trailing_metadata_key) + fail AssertionError, "Expected trailing metadata. None received" + elsif op.trailing_metadata[trailing_metadata_key] != + metadata[trailing_metadata_key] + fail AssertionError, + "Expected trailing metadata: #{metadata[trailing_metadata_key]}. "\ + "Received: #{op.trailing_metadata[trailing_metadata_key]}" + end + + # Testing with FullDuplex + req_cls, p_cls = StreamingOutputCallRequest, ResponseParameters + duplex_req = req_cls.new(payload: Payload.new(body: nulls(req_size)), + response_type: :COMPRESSABLE, + response_parameters: [p_cls.new(size: wanted_response_size)]) + + duplex_op = @stub.full_duplex_call([duplex_req], metadata: metadata, + return_op: true) + resp = duplex_op.execute + resp.each { |r| } # ensures that the server sends trailing data + duplex_op.wait + if not duplex_op.metadata.has_key?(initial_metadata_key) + fail AssertionError, "Expected initial metadata. None received" + elsif duplex_op.metadata[initial_metadata_key] != + metadata[initial_metadata_key] + fail AssertionError, + "Expected initial metadata: #{metadata[initial_metadata_key]}. "\ + "Received: #{duplex_op.metadata[initial_metadata_key]}" + end + if not duplex_op.trailing_metadata[trailing_metadata_key] + fail AssertionError, "Expected trailing metadata. None received" + elsif duplex_op.trailing_metadata[trailing_metadata_key] != + metadata[trailing_metadata_key] + fail AssertionError, + "Expected trailing metadata: #{metadata[trailing_metadata_key]}. "\ + "Received: #{duplex_op.trailing_metadata[trailing_metadata_key]}" + end + + end + def all all_methods = NamedTests.instance_methods(false).map(&:to_s) all_methods.each do |m| diff --git a/src/ruby/pb/test/server.rb b/src/ruby/pb/test/server.rb index 3f1e0a1ccf..cc9b872648 100755 --- a/src/ruby/pb/test/server.rb +++ b/src/ruby/pb/test/server.rb @@ -129,6 +129,27 @@ def nulls(l) [].pack('x' * l).force_encoding('ascii-8bit') end +def maybe_echo_metadata(_call) + + # these are consistent for all interop tests + initial_metadata_key = "x-grpc-test-echo-initial" + trailing_metadata_key = "x-grpc-test-echo-trailing-bin" + + if _call.metadata.has_key?(initial_metadata_key) + _call.metadata_to_send[initial_metadata_key] = _call.metadata[initial_metadata_key] + end + if _call.metadata.has_key?(trailing_metadata_key) + _call.output_metadata[trailing_metadata_key] = _call.metadata[trailing_metadata_key] + end +end + +def maybe_echo_status_and_message(req) + unless req.response_status.nil? + fail GRPC::BadStatus.new_status_exception( + req.response_status.code, req.response_status.message) + end +end + # A FullDuplexEnumerator passes requests to a block and yields generated responses class FullDuplexEnumerator include Grpc::Testing @@ -143,6 +164,7 @@ class FullDuplexEnumerator begin cls = StreamingOutputCallResponse @requests.each do |req| + maybe_echo_status_and_message(req) req.response_parameters.each do |params| resp_size = params.size GRPC.logger.info("read a req, response size is #{resp_size}") @@ -170,6 +192,8 @@ class TestTarget < Grpc::Testing::TestService::Service end def unary_call(simple_req, _call) + maybe_echo_metadata(_call) + maybe_echo_status_and_message(simple_req) req_size = simple_req.response_size SimpleResponse.new(payload: Payload.new(type: :COMPRESSABLE, body: nulls(req_size))) @@ -189,7 +213,8 @@ class TestTarget < Grpc::Testing::TestService::Service end end - def full_duplex_call(reqs) + def full_duplex_call(reqs, _call) + maybe_echo_metadata(_call) # reqs is a lazy Enumerator of the requests sent by the client. FullDuplexEnumerator.new(reqs).each_item end diff --git a/src/ruby/qps/client.rb b/src/ruby/qps/client.rb index 8aed866da5..817192626b 100644 --- a/src/ruby/qps/client.rb +++ b/src/ruby/qps/client.rb @@ -134,6 +134,7 @@ class BenchmarkClient resp = stub.streaming_call(q.each_item) start = Time.now q.push(req) + pushed_sentinal = false resp.each do |r| @histogram.add((Time.now-start)*1e9) if !@done @@ -141,8 +142,9 @@ class BenchmarkClient start = Time.now q.push(req) else - q.push(self) - break + q.push(self) unless pushed_sentinal + # Continue polling on the responses to consume and release resources + pushed_sentinal = true end end end diff --git a/src/ruby/spec/error_sanity_spec.rb b/src/ruby/spec/error_sanity_spec.rb new file mode 100644 index 0000000000..77e94a8816 --- /dev/null +++ b/src/ruby/spec/error_sanity_spec.rb @@ -0,0 +1,64 @@ +# Copyright 2015, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +require 'grpc' + +StatusCodes = GRPC::Core::StatusCodes + +describe StatusCodes do + # convert upper snake-case to camel case. + # e.g., DEADLINE_EXCEEDED -> DeadlineExceeded + def upper_snake_to_camel(name) + name.to_s.split('_').map(&:downcase).map(&:capitalize).join('') + end + + StatusCodes.constants.each do |status_name| + it 'there is a subclass of BadStatus corresponding to StatusCode: ' \ + "#{status_name} that has code: #{StatusCodes.const_get(status_name)}" do + camel_case = upper_snake_to_camel(status_name) + error_class = GRPC.const_get(camel_case) + # expect the error class to be a subclass of BadStatus + expect(error_class < GRPC::BadStatus) + + error_object = error_class.new + # check that the code matches the int value of the error's constant + status_code = StatusCodes.const_get(status_name) + expect(error_object.code).to eq(status_code) + + # check default parameters + expect(error_object.details).to eq('unknown cause') + expect(error_object.metadata).to eq({}) + + # check that the BadStatus factory for creates the correct + # exception too + from_factory = GRPC::BadStatus.new_status_exception(status_code) + expect(from_factory.is_a?(error_class)).to be(true) + end + end +end diff --git a/src/ruby/spec/generic/client_stub_spec.rb b/src/ruby/spec/generic/client_stub_spec.rb index 607a4a3c5d..b51b291cbd 100644 --- a/src/ruby/spec/generic/client_stub_spec.rb +++ b/src/ruby/spec/generic/client_stub_spec.rb @@ -190,15 +190,14 @@ describe 'ClientStub' do end creds = GRPC::Core::CallCredentials.new(failing_auth) - error_occured = false + unauth_error_occured = false begin get_response(stub, credentials: creds) - rescue GRPC::BadStatus => e - error_occured = true - expect(e.code).to eq(GRPC::Core::StatusCodes::UNAUTHENTICATED) + rescue GRPC::Unauthenticated => e + unauth_error_occured = true expect(e.details.include?(error_message)).to be true end - expect(error_occured).to eq(true) + expect(unauth_error_occured).to eq(true) # Kill the server thread so tests can complete th.kill diff --git a/src/ruby/spec/generic/rpc_desc_spec.rb b/src/ruby/spec/generic/rpc_desc_spec.rb index a3f0efa603..1ace7211e9 100644 --- a/src/ruby/spec/generic/rpc_desc_spec.rb +++ b/src/ruby/spec/generic/rpc_desc_spec.rb @@ -48,7 +48,6 @@ describe GRPC::RpcDesc do @bidi_streamer = RpcDesc.new('ss', Stream.new(Object.new), Stream.new(Object.new), 'encode', 'decode') @bs_code = INTERNAL - @no_reason = 'unkown error handling call on server' @ok_response = Object.new end @@ -62,8 +61,9 @@ describe GRPC::RpcDesc do it 'sends status UNKNOWN if other StandardErrors are raised' do expect(@call).to receive(:remote_read).once.and_return(Object.new) - expect(@call).to receive(:send_status) .once.with(UNKNOWN, @no_reason, - false, metadata: {}) + expect(@call).to receive(:send_status).once.with(UNKNOWN, + arg_error_msg, + false, metadata: {}) this_desc.run_server_method(@call, method(:other_error)) end @@ -112,7 +112,7 @@ describe GRPC::RpcDesc do end it 'sends status UNKNOWN if other StandardErrors are raised' do - expect(@call).to receive(:send_status).once.with(UNKNOWN, @no_reason, + expect(@call).to receive(:send_status).once.with(UNKNOWN, arg_error_msg, false, metadata: {}) @client_streamer.run_server_method(@call, method(:other_error_alt)) end @@ -174,8 +174,9 @@ describe GRPC::RpcDesc do end it 'sends status UNKNOWN if other StandardErrors are raised' do + error_msg = arg_error_msg(StandardError.new) expect(@call).to receive(:run_server_bidi).and_raise(StandardError) - expect(@call).to receive(:send_status).once.with(UNKNOWN, @no_reason, + expect(@call).to receive(:send_status).once.with(UNKNOWN, error_msg, false, metadata: {}) @bidi_streamer.run_server_method(@call, method(:other_error_alt)) end @@ -342,4 +343,9 @@ describe GRPC::RpcDesc do def other_error_alt(_call) fail(ArgumentError, 'other error') end + + def arg_error_msg(error = nil) + error ||= ArgumentError.new('other error') + "#{error.class}: #{error.message}" + end end diff --git a/src/ruby/spec/generic/rpc_server_pool_spec.rb b/src/ruby/spec/generic/rpc_server_pool_spec.rb new file mode 100644 index 0000000000..69e8222cb9 --- /dev/null +++ b/src/ruby/spec/generic/rpc_server_pool_spec.rb @@ -0,0 +1,132 @@ +# Copyright 2015, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +require 'grpc' + +Thread.abort_on_exception = true + +describe GRPC::Pool do + Pool = GRPC::Pool + + describe '#new' do + it 'raises if a non-positive size is used' do + expect { Pool.new(0) }.to raise_error + expect { Pool.new(-1) }.to raise_error + expect { Pool.new(Object.new) }.to raise_error + end + + it 'is constructed OK with a positive size' do + expect { Pool.new(1) }.not_to raise_error + end + end + + describe '#ready_for_work?' do + it 'before start it is not ready' do + p = Pool.new(1) + expect(p.ready_for_work?).to be(false) + end + + it 'it stops being ready after all workers jobs waiting or running' do + p = Pool.new(5) + p.start + job = proc { sleep(3) } # sleep so workers busy when done scheduling + 5.times do + expect(p.ready_for_work?).to be(true) + p.schedule(&job) + end + expect(p.ready_for_work?).to be(false) + end + + it 'it becomes ready again after jobs complete' do + p = Pool.new(5) + p.start + job = proc {} + 5.times do + expect(p.ready_for_work?).to be(true) + p.schedule(&job) + end + expect(p.ready_for_work?).to be(false) + sleep 5 # give the pool time do get at least one task done + expect(p.ready_for_work?).to be(true) + end + end + + describe '#schedule' do + it 'return if the pool is already stopped' do + p = Pool.new(1) + p.stop + job = proc {} + expect { p.schedule(&job) }.to_not raise_error + end + + it 'adds jobs that get run by the pool' do + p = Pool.new(1) + p.start + o, q = Object.new, Queue.new + job = proc { q.push(o) } + p.schedule(&job) + expect(q.pop).to be(o) + p.stop + end + end + + describe '#stop' do + it 'works when there are no scheduled tasks' do + p = Pool.new(1) + expect { p.stop }.not_to raise_error + end + + it 'stops jobs when there are long running jobs' do + p = Pool.new(1) + p.start + o, q = Object.new, Queue.new + job = proc do + sleep(5) # long running + q.push(o) + end + p.schedule(&job) + sleep(1) # should ensure the long job gets scheduled + expect { p.stop }.not_to raise_error + end + end + + describe '#start' do + it 'runs jobs as they are scheduled' do + p = Pool.new(5) + o, q = Object.new, Queue.new + p.start + n = 5 # arbitrary + n.times do + p.schedule(o, &q.method(:push)) + expect(q.pop).to be(o) + end + p.stop + end + end +end diff --git a/src/ruby/spec/generic/rpc_server_spec.rb b/src/ruby/spec/generic/rpc_server_spec.rb index c5694790fd..806ea8ce9f 100644 --- a/src/ruby/spec/generic/rpc_server_spec.rb +++ b/src/ruby/spec/generic/rpc_server_spec.rb @@ -408,21 +408,21 @@ describe GRPC::RpcServer do req = EchoMsg.new n = 20 # arbitrary, use as many to ensure the server pool is exceeded threads = [] - bad_status_code = nil + 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::BadStatus => e - bad_status_code = e.code + rescue GRPC::ResourceExhausted + one_failed_as_unavailable = true end end end threads.each(&:join) alt_srv.stop t.join - expect(bad_status_code).to be(StatusCodes::RESOURCE_EXHAUSTED) + expect(one_failed_as_unavailable).to be(true) end end diff --git a/src/ruby/spec/pb/health/checker_spec.rb b/src/ruby/spec/pb/health/checker_spec.rb index 4711e09e88..719510001c 100644 --- a/src/ruby/spec/pb/health/checker_spec.rb +++ b/src/ruby/spec/pb/health/checker_spec.rb @@ -122,7 +122,7 @@ describe Grpc::Health::Checker do checker.check(HCReq.new(service: t[:service]), nil) end expected_msg = /#{StatusCodes::NOT_FOUND}/ - expect(&blk).to raise_error GRPC::BadStatus, expected_msg + expect(&blk).to raise_error GRPC::NotFound, expected_msg end end end @@ -141,7 +141,7 @@ describe Grpc::Health::Checker do checker.check(HCReq.new(service: t[:service]), nil) end expected_msg = /#{StatusCodes::NOT_FOUND}/ - expect(&blk).to raise_error GRPC::BadStatus, expected_msg + expect(&blk).to raise_error GRPC::NotFound, expected_msg end end end @@ -163,7 +163,7 @@ describe Grpc::Health::Checker do checker.check(HCReq.new(service: t[:service]), nil) end expected_msg = /#{StatusCodes::NOT_FOUND}/ - expect(&blk).to raise_error GRPC::BadStatus, expected_msg + expect(&blk).to raise_error GRPC::NotFound, expected_msg end end end @@ -214,7 +214,7 @@ describe Grpc::Health::Checker do stub.check(HCReq.new(service: 'unknown')) end expected_msg = /#{StatusCodes::NOT_FOUND}/ - expect(&blk).to raise_error GRPC::BadStatus, expected_msg + expect(&blk).to raise_error GRPC::NotFound, expected_msg @srv.stop t.join end diff --git a/src/ruby/spec/spec_helper.rb b/src/ruby/spec/spec_helper.rb index c891c1bf5e..c2be0afa72 100644 --- a/src/ruby/spec/spec_helper.rb +++ b/src/ruby/spec/spec_helper.rb @@ -67,3 +67,5 @@ RSpec.configure do |config| end RSpec::Expectations.configuration.warn_about_potential_false_positives = false + +Thread.abort_on_exception = true |