diff options
author | nnoble <nnoble@google.com> | 2014-12-01 17:06:10 -0800 |
---|---|---|
committer | Nicolas Noble <nnoble@google.com> | 2014-12-01 17:45:43 -0800 |
commit | 097ef9b7d9a8e000a5654432cb2fd35816777068 (patch) | |
tree | 62e853673a6ddf69666d531f8fe6597a6fdc1a69 | |
parent | 8ac074ba20ed23f597eddf0a2d07293ec80d88a8 (diff) |
Incorporating ruby into the master grpc repository.
Change on 2014/12/01 by nnoble <nnoble@google.com>
-------------
Created by MOE: http://code.google.com/p/moe-java
MOE_MIGRATED_REVID=81111468
64 files changed, 9812 insertions, 0 deletions
diff --git a/src/ruby/.gitignore b/src/ruby/.gitignore new file mode 100755 index 0000000000..62fcb4fa94 --- /dev/null +++ b/src/ruby/.gitignore @@ -0,0 +1,15 @@ +/.bundle/ +/.yardoc +/Gemfile.lock +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +*.bundle +*.so +*.o +*.a +mkmf.log +vendor diff --git a/src/ruby/.rspec b/src/ruby/.rspec new file mode 100755 index 0000000000..60a4aad5a2 --- /dev/null +++ b/src/ruby/.rspec @@ -0,0 +1 @@ +-I. diff --git a/src/ruby/Gemfile b/src/ruby/Gemfile new file mode 100755 index 0000000000..4d41544ce9 --- /dev/null +++ b/src/ruby/Gemfile @@ -0,0 +1,13 @@ +source 'https://rubygems.org' + +# Modify this when working locally, see README.md +# e.g, +# gem 'beefcake', path: "/usr/local/google/repos/beefcake" +# +# The default value is what's used for gRPC ruby's GCE configuration +# +# gem 'beefcake', path: "/var/local/git/beefcake" +gem 'beefcake', path: "/usr/local/google/repos/beefcake" + +# Specify your gem's dependencies in grpc.gemspec +gemspec diff --git a/src/ruby/README.md b/src/ruby/README.md new file mode 100755 index 0000000000..8377866344 --- /dev/null +++ b/src/ruby/README.md @@ -0,0 +1,93 @@ +Ruby for GRPC +============= + +LAYOUT +------ + +Directory structure is the recommended layout for [ruby extensions](http://guides.rubygems.org/gems-with-extensions/) + + * ext: the extension code + * lib: the entrypoint grpc ruby library to be used in a 'require' statement + * test: tests + + +DEPENDENCIES +------------ + + +* Extension + +The extension can be built and tested using +[rake](https://rubygems.org/gems/rake). However, the rake-extensiontask rule +is not supported on older versions of rubygems, and the necessary version of +rubygems is not available on the latest version of Goobuntu. + +This is resolved by using [RVM](https://rvm.io/) instead; install a single-user +ruby environment, and develop on the latest stable version of ruby (2.1.2). + + +* Proto code generation + +To build generate service stubs and skeletons, it's currently necessary to use +a patched version of a beefcake, a simple third-party proto2 library. This is +feature compatible with proto3 and will be replaced by official proto3 support +in protoc. + +* Patched protoc + +The patched version of beefcake in turn depends on a patched version of protoc. +This is an update of the latest open source release of protoc with some forward +looking proto3 patches. + + +INSTALLATION PREREQUISITES +-------------------------- + +Install the patched protoc + +$ cd <git_repo_dir> +$ git clone sso://team/one-platform-grpc-team/protobuf +$ cd protobuf +$ ./configure --prefix=/usr +$ make +$ sudo make install + +Install RVM + +$ \curl -sSL https://get.rvm.io | bash -s stable --ruby +$ # follow the instructions to ensure that your're using the latest stable version of Ruby +$ +$ gem install bundler # install bundler, the standard ruby package manager + +Install the patched beefcake, and update the Gemfile to reference + +$ cd <git_repo_dir> +$ git clone sso://team/one-platform-grpc-team/grpc-ruby-beefcake beefcake +$ cd beefcake +$ bundle install +$ + +HACKING +------- + +The extension can be built and tested using the Rakefile. + +$ # create a workspace +$ git5 start <your-git5-branch> net/grpc +$ +$ # build the C library and install it in $HOME/grpc_dev +$ <google3>/net/grpc/c/build_gyp/build_grpc_dev.sh +$ +$ # build the ruby extension and test it. +$ cd google3_dir/net/grpc/ruby +$ rake + +Finally, install grpc ruby locally. + +$ cd <this_dir> +$ +$ # update the Gemfile, modify the line beginning # gem 'beefcake' to refer to +$ # the patched beefcake dir +$ +$ bundle install + diff --git a/src/ruby/Rakefile b/src/ruby/Rakefile new file mode 100755 index 0000000000..11b3d04f3f --- /dev/null +++ b/src/ruby/Rakefile @@ -0,0 +1,38 @@ +# -*- ruby -*- +require 'rake/extensiontask' +require 'rspec/core/rake_task' + + +Rake::ExtensionTask.new 'grpc' do |ext| + ext.lib_dir = File.join('lib', 'grpc') +end + +SPEC_SUITES = [ + { :id => :wrapper, :title => 'wrapper layer', :files => %w(spec/*.rb) }, + { :id => :idiomatic, :title => 'idiomatic layer', :dir => %w(spec/generic) } +] + +desc "Run all RSpec tests" +namespace :spec do + namespace :suite do + SPEC_SUITES.each do |suite| + desc "Run all specs in #{suite[:title]} spec suite" + RSpec::Core::RakeTask.new(suite[:id]) do |t| + spec_files = [] + if suite[:files] + suite[:files].each { |f| spec_files += Dir[f] } + end + + if suite[:dirs] + suite[:dirs].each { |f| spec_files += Dir["#{f}/**/*_spec.rb"] } + end + + t.pattern = spec_files + end + end + end +end + +desc "Run tests" +task :default => [ "spec:suite:wrapper", "spec:suite:idiomatic"] +task :spec => :compile diff --git a/src/ruby/bin/math.pb.rb b/src/ruby/bin/math.pb.rb new file mode 100755 index 0000000000..9278a84382 --- /dev/null +++ b/src/ruby/bin/math.pb.rb @@ -0,0 +1,65 @@ +## Generated from bin/math.proto for math +require "beefcake" +require "grpc" + +module Math + + class DivArgs + include Beefcake::Message + end + + class DivReply + include Beefcake::Message + end + + class FibArgs + include Beefcake::Message + end + + class Num + include Beefcake::Message + end + + class FibReply + include Beefcake::Message + end + + class DivArgs + required :dividend, :int64, 1 + required :divisor, :int64, 2 + end + + class DivReply + required :quotient, :int64, 1 + required :remainder, :int64, 2 + end + + class FibArgs + optional :limit, :int64, 1 + end + + class Num + required :num, :int64, 1 + end + + class FibReply + required :count, :int64, 1 + end + + module Math + + class Service + include GRPC::GenericService + + self.marshal_instance_method = :encode + self.unmarshal_class_method = :decode + + rpc :Div, DivArgs, DivReply + rpc :DivMany, stream(DivArgs), stream(DivReply) + rpc :Fib, FibArgs, stream(Num) + rpc :Sum, stream(Num), Num + end + Stub = Service.rpc_stub_class + + end +end diff --git a/src/ruby/bin/math.proto b/src/ruby/bin/math.proto new file mode 100755 index 0000000000..de18a50260 --- /dev/null +++ b/src/ruby/bin/math.proto @@ -0,0 +1,50 @@ +syntax = "proto2"; + +package math; + +message DivArgs { + required int64 dividend = 1; + required int64 divisor = 2; +} + +message DivReply { + required int64 quotient = 1; + required int64 remainder = 2; +} + +message FibArgs { + optional int64 limit = 1; +} + +message Num { + required int64 num = 1; +} + +message FibReply { + required int64 count = 1; +} + +service Math { + // Div divides args.dividend by args.divisor and returns the quotient and + // remainder. + rpc Div (DivArgs) returns (DivReply) { + } + + // DivMany accepts an arbitrary number of division args from the client stream + // and sends back the results in the reply stream. The stream continues until + // the client closes its end; the server does the same after sending all the + // replies. The stream ends immediately if either end aborts. + rpc DivMany (stream DivArgs) returns (stream DivReply) { + } + + // Fib generates numbers in the Fibonacci sequence. If args.limit > 0, Fib + // generates up to limit numbers; otherwise it continues until the call is + // canceled. Unlike Fib above, Fib has no final FibReply. + rpc Fib (FibArgs) returns (stream Num) { + } + + // Sum sums a stream of numbers, returning the final result once the stream + // is closed. + rpc Sum (stream Num) returns (Num) { + } +} diff --git a/src/ruby/bin/math_client.rb b/src/ruby/bin/math_client.rb new file mode 100644 index 0000000000..f8cf8580e8 --- /dev/null +++ b/src/ruby/bin/math_client.rb @@ -0,0 +1,110 @@ +# Copyright 2014, 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. + +#!/usr/bin/env ruby +# +# Sample app that accesses a Calc service running on a Ruby gRPC server and +# helps validate RpcServer as a gRPC server using proto2 serialization. +# +# Usage: $ path/to/math_client.rb + +this_dir = File.expand_path(File.dirname(__FILE__)) +lib_dir = File.join(File.dirname(this_dir), 'lib') +$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) +$LOAD_PATH.unshift(this_dir) unless $LOAD_PATH.include?(this_dir) + +require 'grpc' +require 'grpc/generic/client_stub' +require 'grpc/generic/service' +require 'math.pb' + +def do_div(stub) + logger.info('request_response') + logger.info('----------------') + req = Math::DivArgs.new(:dividend => 7, :divisor => 3) + logger.info("div(7/3): req=#{req.inspect}") + resp = stub.div(req, deadline=GRPC::TimeConsts::INFINITE_FUTURE) + logger.info("Answer: #{resp.inspect}") + logger.info('----------------') +end + +def do_sum(stub) + # to make client streaming requests, pass an enumerable of the inputs + logger.info('client_streamer') + logger.info('---------------') + reqs = [1, 2, 3, 4, 5].map { |x| Math::Num.new(:num => x) } + logger.info("sum(1, 2, 3, 4, 5): reqs=#{reqs.inspect}") + resp = stub.sum(reqs) # reqs.is_a?(Enumerable) + logger.info("Answer: #{resp.inspect}") + logger.info('---------------') +end + +def do_fib(stub) + logger.info('server_streamer') + logger.info('----------------') + req = Math::FibArgs.new(:limit => 11) + logger.info("fib(11): req=#{req.inspect}") + resp = stub.fib(req, deadline=GRPC::TimeConsts::INFINITE_FUTURE) + resp.each do |r| + logger.info("Answer: #{r.inspect}") + end + logger.info('----------------') +end + +def do_div_many(stub) + logger.info('bidi_streamer') + logger.info('-------------') + reqs = [] + reqs << Math::DivArgs.new(:dividend => 7, :divisor => 3) + reqs << Math::DivArgs.new(:dividend => 5, :divisor => 2) + reqs << Math::DivArgs.new(:dividend => 7, :divisor => 2) + logger.info("div(7/3), div(5/2), div(7/2): reqs=#{reqs.inspect}") + resp = stub.div_many(reqs, deadline=10) + resp.each do |r| + logger.info("Answer: #{r.inspect}") + end + logger.info('----------------') +end + + +def main + host_port = 'localhost:7070' + if ARGV.size > 0 + host_port = ARGV[0] + end + # The Math::Math:: module occurs because the service has the same name as its + # package. That practice should be avoided by defining real services. + stub = Math::Math::Stub.new(host_port) + do_div(stub) + do_sum(stub) + do_fib(stub) + do_div_many(stub) +end + +main diff --git a/src/ruby/bin/math_server.rb b/src/ruby/bin/math_server.rb new file mode 100644 index 0000000000..72a1f6b398 --- /dev/null +++ b/src/ruby/bin/math_server.rb @@ -0,0 +1,166 @@ +# Copyright 2014, 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. + +#!/usr/bin/env ruby +# +# Sample gRPC Ruby server that implements the Math::Calc service and helps +# validate GRPC::RpcServer as GRPC implementation using proto2 serialization. +# +# Usage: $ path/to/math_server.rb + +this_dir = File.expand_path(File.dirname(__FILE__)) +lib_dir = File.join(File.dirname(this_dir), 'lib') +$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) +$LOAD_PATH.unshift(this_dir) unless $LOAD_PATH.include?(this_dir) + +require 'forwardable' +require 'grpc' +require 'grpc/generic/service' +require 'grpc/generic/rpc_server' +require 'math.pb' + +# Holds state for a fibonacci series +class Fibber + + def initialize(limit) + raise "bad limit: got #{limit}, want limit > 0" if limit < 1 + @limit = limit + end + + def generator + return enum_for(:generator) unless block_given? + idx, current, previous = 0, 1, 1 + until idx == @limit + if idx == 0 || idx == 1 + yield Math::Num.new(:num => 1) + idx += 1 + next + end + tmp = current + current = previous + current + previous = tmp + yield Math::Num.new(:num => current) + idx += 1 + end + end +end + +# A EnumeratorQueue wraps a Queue to yield the items added to it. +class EnumeratorQueue + extend Forwardable + def_delegators :@q, :push + + def initialize(sentinel) + @q = Queue.new + @sentinel = sentinel + end + + def each_item + return enum_for(:each_item) unless block_given? + loop do + r = @q.pop + break if r.equal?(@sentinel) + raise r if r.is_a?Exception + yield r + end + end + +end + +# The Math::Math:: module occurs because the service has the same name as its +# package. That practice should be avoided by defining real services. +class Calculator < Math::Math::Service + + def div(div_args, call) + if div_args.divisor == 0 + # To send non-OK status handlers raise a StatusError with the code and + # and detail they want sent as a Status. + raise GRPC::StatusError.new(GRPC::Status::INVALID_ARGUMENT, + 'divisor cannot be 0') + end + + Math::DivReply.new(:quotient => div_args.dividend/div_args.divisor, + :remainder => div_args.dividend % div_args.divisor) + end + + def sum(call) + # the requests are accesible as the Enumerator call#each_request + nums = call.each_remote_read.collect { |x| x.num } + sum = nums.inject { |sum,x| sum + x } + Math::Num.new(:num => sum) + end + + def fib(fib_args, call) + if fib_args.limit < 1 + raise StatusError.new(Status::INVALID_ARGUMENT, 'limit must be >= 0') + end + + # return an Enumerator of Nums + Fibber.new(fib_args.limit).generator() + # just return the generator, GRPC::GenericServer sends each actual response + end + + def div_many(requests) + # requests is an lazy Enumerator of the requests sent by the client. + q = EnumeratorQueue.new(self) + t = Thread.new do + begin + requests.each do |req| + logger.info("read #{req.inspect}") + resp = Math::DivReply.new(:quotient => req.dividend/req.divisor, + :remainder => req.dividend % req.divisor) + q.push(resp) + Thread::pass # let the internal Bidi threads run + end + logger.info('finished reads') + q.push(self) + rescue StandardError => e + q.push(e) # share the exception with the enumerator + raise e + end + end + t.priority = -2 # hint that the div_many thread should not be favoured + q.each_item + end + +end + +def main + host_port = 'localhost:7070' + if ARGV.size > 0 + host_port = ARGV[0] + end + + s = GRPC::RpcServer.new() + s.add_http2_port(host_port) + s.handle(Calculator) + s.run +end + +main diff --git a/src/ruby/bin/noproto_client.rb b/src/ruby/bin/noproto_client.rb new file mode 100644 index 0000000000..fbd10a06b5 --- /dev/null +++ b/src/ruby/bin/noproto_client.rb @@ -0,0 +1,75 @@ +# Copyright 2014, 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. + +#!/usr/bin/env ruby +# Sample app that helps validate RpcServer without protobuf serialization. +# +# Usage: $ ruby -S path/to/noproto_client.rb + +this_dir = File.expand_path(File.dirname(__FILE__)) +lib_dir = File.join(File.dirname(this_dir), 'lib') +$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) + +require 'grpc' +require 'grpc/generic/client_stub' +require 'grpc/generic/service' + +class EchoMsg + def marshal + '' + end + + def self.unmarshal(o) + EchoMsg.new + end +end + +class EchoService + include GRPC::GenericService + rpc :AnRPC, EchoMsg, EchoMsg + + def initialize(default_var='ignored') + end + + def an_rpc(req, call) + logger.info('echo service received a request') + req + end +end + +EchoStub = EchoService.rpc_stub_class + +def main + stub = EchoStub.new('localhost:9090') + logger.info('sending an rpc') + resp = stub.an_rpc(EchoMsg.new) + logger.info("got a response: #{resp}") +end + +main diff --git a/src/ruby/bin/noproto_server.rb b/src/ruby/bin/noproto_server.rb new file mode 100644 index 0000000000..c5b7c192eb --- /dev/null +++ b/src/ruby/bin/noproto_server.rb @@ -0,0 +1,75 @@ +# Copyright 2014, 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. + +#!/usr/bin/env ruby +# Sample app that helps validate RpcServer without protobuf serialization. +# +# Usage: $ path/to/noproto_server.rb + +this_dir = File.expand_path(File.dirname(__FILE__)) +lib_dir = File.join(File.dirname(this_dir), 'lib') +$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) + +require 'grpc' +require 'grpc/generic/rpc_server' +require 'grpc/generic/service' + +class EchoMsg + def marshal + '' + end + + def self.unmarshal(o) + EchoMsg.new + end +end + +class EchoService + include GRPC::GenericService + rpc :AnRPC, EchoMsg, EchoMsg +end + +class Echo < EchoService + def initialize(default_var='ignored') + end + + def an_rpc(req, call) + logger.info('echo service received a request') + req + end +end + +def main + s = GRPC::RpcServer.new() + s.add_http2_port('localhost:9090') + s.handle(Echo) + s.run +end + +main diff --git a/src/ruby/ext/grpc/extconf.rb b/src/ruby/ext/grpc/extconf.rb new file mode 100644 index 0000000000..06bfad9e6c --- /dev/null +++ b/src/ruby/ext/grpc/extconf.rb @@ -0,0 +1,92 @@ +# Copyright 2014, 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 'mkmf' + +LIBDIR = RbConfig::CONFIG['libdir'] +INCLUDEDIR = RbConfig::CONFIG['includedir'] + +HEADER_DIRS = [ + # First search the local development dir + ENV['HOME'] + '/grpc_dev/include', + + # Then search /opt/local (Mac) + '/opt/local/include', + + # Then search /usr/local (Source install) + '/usr/local/include', + + # Check the ruby install locations + INCLUDEDIR, + + # Finally fall back to /usr + '/usr/include' +] + +LIB_DIRS = [ + # First search the local development dir + ENV['HOME'] + '/grpc_dev/lib', + + # Then search /opt/local for (Mac) + '/opt/local/lib', + + # Then search /usr/local (Source install) + '/usr/local/lib', + + # Check the ruby install locations + LIBDIR, + + # Finally fall back to /usr + '/usr/lib' +] + +def crash(msg) + print(" extconf failure: %s\n" % msg) + exit 1 +end + +dir_config('grpc', HEADER_DIRS, LIB_DIRS) + +$CFLAGS << ' -std=c89 ' +$CFLAGS << ' -Wno-implicit-function-declaration ' +$CFLAGS << ' -Wno-pointer-sign ' +$CFLAGS << ' -Wno-return-type ' +$CFLAGS << ' -Wall ' +$CFLAGS << ' -pedantic ' + +$LDFLAGS << ' -lgrpc -lgpr -levent -levent_pthreads -levent_core' + +# crash('need grpc lib') unless have_library('grpc', 'grpc_channel_destroy') +# +# TODO(temiola): figure out why this stopped working, but the so is built OK +# and the tests pass + +have_library('grpc', 'grpc_channel_destroy') +crash('need gpr lib') unless have_library('gpr', 'gpr_now') +create_makefile('grpc/grpc') diff --git a/src/ruby/ext/grpc/rb_byte_buffer.c b/src/ruby/ext/grpc/rb_byte_buffer.c new file mode 100644 index 0000000000..a520ca44dd --- /dev/null +++ b/src/ruby/ext/grpc/rb_byte_buffer.c @@ -0,0 +1,243 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#include "rb_byte_buffer.h" + +#include <ruby.h> + +#include <grpc/grpc.h> +#include <grpc/support/slice.h> +#include "rb_grpc.h" + +/* grpc_rb_byte_buffer wraps a grpc_byte_buffer. It provides a peer ruby + * object, 'mark' to minimize copying when a byte_buffer is created from + * ruby. */ +typedef struct grpc_rb_byte_buffer { + /* Holder of ruby objects involved in constructing the status */ + VALUE mark; + /* The actual status */ + grpc_byte_buffer *wrapped; +} grpc_rb_byte_buffer; + + +/* Destroys ByteBuffer instances. */ +static void grpc_rb_byte_buffer_free(void *p) { + grpc_rb_byte_buffer *bb = NULL; + if (p == NULL) { + return; + }; + bb = (grpc_rb_byte_buffer *)p; + + /* Deletes the wrapped object if the mark object is Qnil, which indicates + * that no other object is the actual owner. */ + if (bb->wrapped != NULL && bb->mark == Qnil) { + grpc_byte_buffer_destroy(bb->wrapped); + } + + xfree(p); +} + +/* Protects the mark object from GC */ +static void grpc_rb_byte_buffer_mark(void *p) { + grpc_rb_byte_buffer *bb = NULL; + if (p == NULL) { + return; + } + bb = (grpc_rb_byte_buffer *)p; + + /* If it's not already cleaned up, mark the mark object */ + if (bb->mark != Qnil && BUILTIN_TYPE(bb->mark) != T_NONE) { + rb_gc_mark(bb->mark); + } +} + +/* id_source is the name of the hidden ivar the preserves the original + * byte_buffer source string */ +static ID id_source; + +/* Allocates ByteBuffer instances. + + Provides safe default values for the byte_buffer fields. */ +static VALUE grpc_rb_byte_buffer_alloc(VALUE cls) { + grpc_rb_byte_buffer *wrapper = ALLOC(grpc_rb_byte_buffer); + wrapper->wrapped = NULL; + wrapper->mark = Qnil; + return Data_Wrap_Struct(cls, grpc_rb_byte_buffer_mark, + grpc_rb_byte_buffer_free, wrapper); +} + +/* Clones ByteBuffer instances. + + Gives ByteBuffer a consistent implementation of Ruby's object copy/dup + protocol. */ +static VALUE grpc_rb_byte_buffer_init_copy(VALUE copy, VALUE orig) { + grpc_rb_byte_buffer *orig_bb = NULL; + grpc_rb_byte_buffer *copy_bb = NULL; + + if (copy == orig) { + return copy; + } + + /* Raise an error if orig is not a metadata object or a subclass. */ + if (TYPE(orig) != T_DATA || + RDATA(orig)->dfree != (RUBY_DATA_FUNC)grpc_rb_byte_buffer_free) { + rb_raise(rb_eTypeError, "not a %s", rb_obj_classname(rb_cByteBuffer)); + } + + Data_Get_Struct(orig, grpc_rb_byte_buffer, orig_bb); + Data_Get_Struct(copy, grpc_rb_byte_buffer, copy_bb); + + /* use ruby's MEMCPY to make a byte-for-byte copy of the metadata wrapper + * object. */ + MEMCPY(copy_bb, orig_bb, grpc_rb_byte_buffer, 1); + return copy; +} + +/* id_empty is used to return the empty string from to_s when necessary. */ +static ID id_empty; + +static VALUE grpc_rb_byte_buffer_to_s(VALUE self) { + grpc_rb_byte_buffer *wrapper = NULL; + grpc_byte_buffer *bb = NULL; + grpc_byte_buffer_reader *reader = NULL; + char *output = NULL; + size_t length = 0; + size_t offset = 0; + VALUE output_obj = Qnil; + gpr_slice next; + + Data_Get_Struct(self, grpc_rb_byte_buffer, wrapper); + output_obj = rb_ivar_get(wrapper->mark, id_source); + if (output_obj != Qnil) { + /* From ruby, ByteBuffers are immutable so if a source is set, return that + * as the to_s value */ + return output_obj; + } + + /* Read the bytes. */ + bb = wrapper->wrapped; + if (bb == NULL) { + return rb_id2str(id_empty); + } + length = grpc_byte_buffer_length(bb); + if (length == 0) { + return rb_id2str(id_empty); + } + reader = grpc_byte_buffer_reader_create(bb); + output = xmalloc(length); + while (grpc_byte_buffer_reader_next(reader, &next) != 0) { + memcpy(output + offset, GPR_SLICE_START_PTR(next), GPR_SLICE_LENGTH(next)); + offset += GPR_SLICE_LENGTH(next); + } + output_obj = rb_str_new(output, length); + + /* Save a references to the computed string in the mark object so that the + * calling to_s does not do any allocations. */ + wrapper->mark = rb_class_new_instance(0, NULL, rb_cObject); + rb_ivar_set(wrapper->mark, id_source, output_obj); + + return output_obj; +} + + +/* Initializes ByteBuffer instances. */ +static VALUE grpc_rb_byte_buffer_init(VALUE self, VALUE src) { + gpr_slice a_slice; + grpc_rb_byte_buffer *wrapper = NULL; + grpc_byte_buffer *byte_buffer = NULL; + + if (TYPE(src) != T_STRING) { + rb_raise(rb_eTypeError, "bad byte_buffer arg: got <%s>, want <String>", + rb_obj_classname(src)); + return Qnil; + } + Data_Get_Struct(self, grpc_rb_byte_buffer, wrapper); + a_slice = gpr_slice_malloc(RSTRING_LEN(src)); + memcpy(GPR_SLICE_START_PTR(a_slice), RSTRING_PTR(src), RSTRING_LEN(src)); + byte_buffer = grpc_byte_buffer_create(&a_slice, 1); + gpr_slice_unref(a_slice); + + if (byte_buffer == NULL) { + rb_raise(rb_eArgError, "could not create a byte_buffer, not sure why"); + return Qnil; + } + wrapper->wrapped = byte_buffer; + + /* Save a references to the original string in the mark object so that the + * pointers used there is valid for the lifetime of the object. */ + wrapper->mark = rb_class_new_instance(0, NULL, rb_cObject); + rb_ivar_set(wrapper->mark, id_source, src); + + return self; +} + +/* rb_cByteBuffer is the ruby class that proxies grpc_byte_buffer. */ +VALUE rb_cByteBuffer = Qnil; + +void Init_google_rpc_byte_buffer() { + rb_cByteBuffer = rb_define_class_under(rb_mGoogleRPC, "ByteBuffer", + rb_cObject); + + /* Allocates an object managed by the ruby runtime */ + rb_define_alloc_func(rb_cByteBuffer, grpc_rb_byte_buffer_alloc); + + /* Provides a ruby constructor and support for dup/clone. */ + rb_define_method(rb_cByteBuffer, "initialize", grpc_rb_byte_buffer_init, 1); + rb_define_method(rb_cByteBuffer, "initialize_copy", + grpc_rb_byte_buffer_init_copy, 1); + + /* Provides a to_s method that returns the buffer value */ + rb_define_method(rb_cByteBuffer, "to_s", grpc_rb_byte_buffer_to_s, 0); + + id_source = rb_intern("__source"); + id_empty = rb_intern(""); +} + +VALUE grpc_rb_byte_buffer_create_with_mark(VALUE mark, grpc_byte_buffer* bb) { + grpc_rb_byte_buffer *byte_buffer = NULL; + if (bb == NULL) { + return Qnil; + } + byte_buffer = ALLOC(grpc_rb_byte_buffer); + byte_buffer->wrapped = bb; + byte_buffer->mark = mark; + return Data_Wrap_Struct(rb_cByteBuffer, grpc_rb_byte_buffer_mark, + grpc_rb_byte_buffer_free, byte_buffer); +} + +/* Gets the wrapped byte_buffer from the ruby wrapper */ +grpc_byte_buffer* grpc_rb_get_wrapped_byte_buffer(VALUE v) { + grpc_rb_byte_buffer *wrapper = NULL; + Data_Get_Struct(v, grpc_rb_byte_buffer, wrapper); + return wrapper->wrapped; +} diff --git a/src/ruby/ext/grpc/rb_byte_buffer.h b/src/ruby/ext/grpc/rb_byte_buffer.h new file mode 100644 index 0000000000..1bdcfe4019 --- /dev/null +++ b/src/ruby/ext/grpc/rb_byte_buffer.h @@ -0,0 +1,54 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#ifndef GRPC_RB_BYTE_BUFFER_H_ +#define GRPC_RB_BYTE_BUFFER_H_ + +#include <grpc/grpc.h> +#include <ruby.h> + +/* rb_cByteBuffer is the ByteBuffer class whose instances proxy + grpc_byte_buffer. */ +extern VALUE rb_cByteBuffer; + +/* Initializes the ByteBuffer class. */ +void Init_google_rpc_byte_buffer(); + +/* grpc_rb_byte_buffer_create_with_mark creates a grpc_rb_byte_buffer with a + * ruby mark object that will be kept alive while the byte_buffer is alive. */ +VALUE grpc_rb_byte_buffer_create_with_mark(VALUE mark, grpc_byte_buffer* bb); + +/* Gets the wrapped byte_buffer from its ruby object. */ +grpc_byte_buffer* grpc_rb_get_wrapped_byte_buffer(VALUE v); + +#endif /* GRPC_RB_BYTE_BUFFER_H_ */ diff --git a/src/ruby/ext/grpc/rb_call.c b/src/ruby/ext/grpc/rb_call.c new file mode 100644 index 0000000000..07f70e041a --- /dev/null +++ b/src/ruby/ext/grpc/rb_call.c @@ -0,0 +1,542 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#include "rb_call.h" + +#include <ruby.h> + +#include <grpc/grpc.h> +#include "rb_byte_buffer.h" +#include "rb_completion_queue.h" +#include "rb_metadata.h" +#include "rb_status.h" +#include "rb_grpc.h" + +/* id_cq is the name of the hidden ivar that preserves a reference to a + * completion queue */ +static ID id_cq; + +/* id_flags is the name of the hidden ivar that preserves the value of + * the flags used to create metadata from a Hash */ +static ID id_flags; + +/* id_input_md is the name of the hidden ivar that preserves the hash used to + * create metadata, so that references to the strings it contains last as long + * as the call the metadata is added to. */ +static ID id_input_md; + +/* id_metadata is name of the attribute used to access the metadata hash + * received by the call and subsequently saved on it. */ +static ID id_metadata; + +/* id_status is name of the attribute used to access the status object + * received by the call and subsequently saved on it. */ +static ID id_status; + +/* hash_all_calls is a hash of Call address -> reference count that is used to + * track the creation and destruction of rb_call instances. + */ +static VALUE hash_all_calls; + +/* Destroys a Call. */ +void grpc_rb_call_destroy(void *p) { + grpc_call *call = NULL; + VALUE ref_count = Qnil; + if (p == NULL) { + return; + }; + call = (grpc_call *)p; + + ref_count = rb_hash_aref(hash_all_calls, OFFT2NUM((VALUE)call)); + if (ref_count == Qnil) { + return; /* No longer in the hash, so already deleted */ + } else if (NUM2UINT(ref_count) == 1) { + rb_hash_delete(hash_all_calls, OFFT2NUM((VALUE)call)); + grpc_call_destroy(call); + } else { + rb_hash_aset(hash_all_calls, OFFT2NUM((VALUE)call), + UINT2NUM(NUM2UINT(ref_count) - 1)); + } +} + +/* Error code details is a hash containing text strings describing errors */ +VALUE rb_error_code_details; + +/* Obtains the error detail string for given error code */ +const char* grpc_call_error_detail_of(grpc_call_error err) { + VALUE detail_ref = rb_hash_aref(rb_error_code_details, UINT2NUM(err)); + const char* detail = "unknown error code!"; + if (detail_ref != Qnil) { + detail = StringValueCStr(detail_ref); + } + return detail; +} + +/* grpc_rb_call_add_metadata_hash_cb is the hash iteration callback used by + grpc_rb_call_add_metadata. +*/ +int grpc_rb_call_add_metadata_hash_cb(VALUE key, VALUE val, VALUE call_obj) { + grpc_call *call = NULL; + grpc_metadata *md = NULL; + VALUE md_obj = Qnil; + VALUE md_obj_args[2]; + VALUE flags = rb_ivar_get(call_obj, id_flags); + grpc_call_error err; + int array_length; + int i; + + /* Construct a metadata object from key and value and add it */ + Data_Get_Struct(call_obj, grpc_call, call); + md_obj_args[0] = key; + + if (TYPE(val) == T_ARRAY) { + /* If the value is an array, add each value in the array separately */ + array_length = RARRAY_LEN(val); + for (i = 0; i < array_length; i++) { + md_obj_args[1] = rb_ary_entry(val, i); + md_obj = rb_class_new_instance(2, md_obj_args, rb_cMetadata); + md = grpc_rb_get_wrapped_metadata(md_obj); + err = grpc_call_add_metadata(call, md, NUM2UINT(flags)); + if (err != GRPC_CALL_OK) { + rb_raise(rb_eCallError, "add metadata failed: %s (code=%d)", + grpc_call_error_detail_of(err), err); + return ST_STOP; + } + } + } else { + md_obj_args[1] = val; + md_obj = rb_class_new_instance(2, md_obj_args, rb_cMetadata); + md = grpc_rb_get_wrapped_metadata(md_obj); + err = grpc_call_add_metadata(call, md, NUM2UINT(flags)); + if (err != GRPC_CALL_OK) { + rb_raise(rb_eCallError, "add metadata failed: %s (code=%d)", + grpc_call_error_detail_of(err), err); + return ST_STOP; + } + } + + return ST_CONTINUE; +} + +/* + call-seq: + call.add_metadata(completion_queue, hash_elements, flags=nil) + + Add metadata elements to the call from a ruby hash, to be sent upon + invocation. flags is a bit-field combination of the write flags defined + above. REQUIRES: grpc_call_start_invoke/grpc_call_accept have not been + called on this call. Produces no events. */ + +static VALUE grpc_rb_call_add_metadata(int argc, VALUE *argv, VALUE self) { + VALUE metadata; + VALUE flags = Qnil; + ID id_size = rb_intern("size"); + + /* "11" == 1 mandatory args, 1 (flags) is optional */ + rb_scan_args(argc, argv, "11", &metadata, &flags); + if (NIL_P(flags)) { + flags = UINT2NUM(0); /* Default to no flags */ + } + if (TYPE(metadata) != T_HASH) { + rb_raise(rb_eTypeError, "add metadata failed: metadata should be a hash"); + return Qnil; + } + if (NUM2UINT(rb_funcall(metadata, id_size, 0)) == 0) { + return Qnil; + } + rb_ivar_set(self, id_flags, flags); + rb_ivar_set(self, id_input_md, metadata); + rb_hash_foreach(metadata, grpc_rb_call_add_metadata_hash_cb, self); + return Qnil; +} + +/* Called by clients to cancel an RPC on the server. + Can be called multiple times, from any thread. */ +static VALUE grpc_rb_call_cancel(VALUE self) { + grpc_call *call = NULL; + grpc_call_error err; + Data_Get_Struct(self, grpc_call, call); + err = grpc_call_cancel(call); + if (err != GRPC_CALL_OK) { + rb_raise(rb_eCallError, "cancel failed: %s (code=%d)", + grpc_call_error_detail_of(err), err); + } + + return Qnil; +} + +/* + call-seq: + call.start_invoke(completion_queue, tag, flags=nil) + + Invoke the RPC. Starts sending metadata and request headers on the wire. + flags is a bit-field combination of the write flags defined above. + REQUIRES: Can be called at most once per call. + Can only be called on the client. + Produces a GRPC_INVOKE_ACCEPTED event on completion. */ +static VALUE grpc_rb_call_start_invoke(int argc, VALUE *argv, VALUE self) { + VALUE cqueue = Qnil; + VALUE invoke_accepted_tag = Qnil; + VALUE metadata_read_tag = Qnil; + VALUE finished_tag = Qnil; + VALUE flags = Qnil; + grpc_call *call = NULL; + grpc_completion_queue *cq = NULL; + grpc_call_error err; + + /* "41" == 4 mandatory args, 1 (flags) is optional */ + rb_scan_args(argc, argv, "41", &cqueue, &invoke_accepted_tag, + &metadata_read_tag, &finished_tag, &flags); + if (NIL_P(flags)) { + flags = UINT2NUM(0); /* Default to no flags */ + } + cq = grpc_rb_get_wrapped_completion_queue(cqueue); + Data_Get_Struct(self, grpc_call, call); + err = grpc_call_start_invoke(call, cq, ROBJECT(invoke_accepted_tag), + ROBJECT(metadata_read_tag), + ROBJECT(finished_tag), + NUM2UINT(flags)); + if (err != GRPC_CALL_OK) { + rb_raise(rb_eCallError, "invoke failed: %s (code=%d)", + grpc_call_error_detail_of(err), err); + } + + /* Add the completion queue as an instance attribute, prevents it from being + * GCed until this call object is GCed */ + rb_ivar_set(self, id_cq, cqueue); + + return Qnil; +} + +/* Initiate a read on a call. Output event contains a byte buffer with the + result of the read. + REQUIRES: No other reads are pending on the call. It is only safe to start + the next read after the corresponding read event is received. */ +static VALUE grpc_rb_call_start_read(VALUE self, VALUE tag) { + grpc_call *call = NULL; + grpc_call_error err; + Data_Get_Struct(self, grpc_call, call); + err = grpc_call_start_read(call, ROBJECT(tag)); + if (err != GRPC_CALL_OK) { + rb_raise(rb_eCallError, "start read failed: %s (code=%d)", + grpc_call_error_detail_of(err), err); + } + + return Qnil; +} + +/* + call-seq: + status = call.status + + Gets the status object saved the call. */ +static VALUE grpc_rb_call_get_status(VALUE self) { + return rb_ivar_get(self, id_status); +} + +/* + call-seq: + call.status = status + + Saves a status object on the call. */ +static VALUE grpc_rb_call_set_status(VALUE self, VALUE status) { + if (!NIL_P(status) && rb_obj_class(status) != rb_cStatus) { + rb_raise(rb_eTypeError, "bad status: got:<%s> want: <Status>", + rb_obj_classname(status)); + return Qnil; + } + + return rb_ivar_set(self, id_status, status); +} + +/* + call-seq: + metadata = call.metadata + + Gets the metadata object saved the call. */ +static VALUE grpc_rb_call_get_metadata(VALUE self) { + return rb_ivar_get(self, id_metadata); +} + +/* + call-seq: + call.metadata = metadata + + Saves the metadata hash on the call. */ +static VALUE grpc_rb_call_set_metadata(VALUE self, VALUE metadata) { + if (!NIL_P(metadata) && TYPE(metadata) != T_HASH) { + rb_raise(rb_eTypeError, "bad metadata: got:<%s> want: <Hash>", + rb_obj_classname(metadata)); + return Qnil; + } + + return rb_ivar_set(self, id_metadata, metadata); +} + +/* + call-seq: + call.start_write(byte_buffer, tag, flags=nil) + + Queue a byte buffer for writing. + flags is a bit-field combination of the write flags defined above. + A write with byte_buffer null is allowed, and will not send any bytes on the + wire. If this is performed without GRPC_WRITE_BUFFER_HINT flag it provides + a mechanism to flush any previously buffered writes to outgoing flow control. + REQUIRES: No other writes are pending on the call. It is only safe to + start the next write after the corresponding write_accepted event + is received. + GRPC_INVOKE_ACCEPTED must have been received by the application + prior to calling this on the client. On the server, + grpc_call_accept must have been called successfully. + Produces a GRPC_WRITE_ACCEPTED event. */ +static VALUE grpc_rb_call_start_write(int argc, VALUE *argv, VALUE self) { + VALUE byte_buffer = Qnil; + VALUE tag = Qnil; + VALUE flags = Qnil; + grpc_call *call = NULL; + grpc_byte_buffer *bfr = NULL; + grpc_call_error err; + + /* "21" == 2 mandatory args, 1 (flags) is optional */ + rb_scan_args(argc, argv, "21", &byte_buffer, &tag, &flags); + if (NIL_P(flags)) { + flags = UINT2NUM(0); /* Default to no flags */ + } + bfr = grpc_rb_get_wrapped_byte_buffer(byte_buffer); + Data_Get_Struct(self, grpc_call, call); + err = grpc_call_start_write(call, bfr, ROBJECT(tag), NUM2UINT(flags)); + if (err != GRPC_CALL_OK) { + rb_raise(rb_eCallError, "start write failed: %s (code=%d)", + grpc_call_error_detail_of(err), err); + } + + return Qnil; +} + +/* Queue a status for writing. + REQUIRES: No other writes are pending on the call. It is only safe to + start the next write after the corresponding write_accepted event + is received. + GRPC_INVOKE_ACCEPTED must have been received by the application + prior to calling this. + Only callable on the server. + Produces a GRPC_FINISHED event when the status is sent and the stream is + fully closed */ +static VALUE grpc_rb_call_start_write_status(VALUE self, VALUE status, + VALUE tag) { + grpc_call *call = NULL; + grpc_status *sts = grpc_rb_get_wrapped_status(status); + grpc_call_error err; + Data_Get_Struct(self, grpc_call, call); + err = grpc_call_start_write_status(call, *sts, ROBJECT(tag)); + if (err != GRPC_CALL_OK) { + rb_raise(rb_eCallError, "start write status: %s (code=%d)", + grpc_call_error_detail_of(err), err); + } + + return Qnil; +} + +/* No more messages to send. + REQUIRES: No other writes are pending on the call. */ +static VALUE grpc_rb_call_writes_done(VALUE self, VALUE tag) { + grpc_call *call = NULL; + grpc_call_error err; + Data_Get_Struct(self, grpc_call, call); + err = grpc_call_writes_done(call, ROBJECT(tag)); + if (err != GRPC_CALL_OK) { + rb_raise(rb_eCallError, "writes done: %s (code=%d)", + grpc_call_error_detail_of(err), err); + } + + return Qnil; +} + +/* call-seq: + call.accept(completion_queue, flags=nil) + + Accept an incoming RPC, binding a completion queue to it. + To be called after adding metadata to the call, but before sending + messages. + flags is a bit-field combination of the write flags defined above. + REQUIRES: Can be called at most once per call. + Can only be called on the server. + Produces no events. */ +static VALUE grpc_rb_call_accept(int argc, VALUE *argv, VALUE self) { + VALUE cqueue = Qnil; + VALUE finished_tag = Qnil; + VALUE flags = Qnil; + grpc_call *call = NULL; + grpc_completion_queue *cq = NULL; + grpc_call_error err; + + /* "21" == 2 mandatory args, 1 (flags) is optional */ + rb_scan_args(argc, argv, "21", &cqueue, &finished_tag, &flags); + if (NIL_P(flags)) { + flags = UINT2NUM(0); /* Default to no flags */ + } + cq = grpc_rb_get_wrapped_completion_queue(cqueue); + Data_Get_Struct(self, grpc_call, call); + err = grpc_call_accept(call, cq, ROBJECT(finished_tag), NUM2UINT(flags)); + if (err != GRPC_CALL_OK) { + rb_raise(rb_eCallError, "accept failed: %s (code=%d)", + grpc_call_error_detail_of(err), err); + } + + /* Add the completion queue as an instance attribute, prevents it from being + * GCed until this call object is GCed */ + rb_ivar_set(self, id_cq, cqueue); + + return Qnil; +} + +/* rb_cCall is the ruby class that proxies grpc_call. */ +VALUE rb_cCall = Qnil; + +/* rb_eCallError is the ruby class of the exception thrown during call + operations; */ +VALUE rb_eCallError = Qnil; + +void Init_google_rpc_error_codes() { + /* Constants representing the error codes of grpc_call_error in grpc.h */ + VALUE rb_RpcErrors = rb_define_module_under(rb_mGoogleRPC, "RpcErrors"); + rb_define_const(rb_RpcErrors, "OK", UINT2NUM(GRPC_CALL_OK)); + rb_define_const(rb_RpcErrors, "ERROR", UINT2NUM(GRPC_CALL_ERROR)); + rb_define_const(rb_RpcErrors, "NOT_ON_SERVER", + UINT2NUM(GRPC_CALL_ERROR_NOT_ON_SERVER)); + rb_define_const(rb_RpcErrors, "NOT_ON_CLIENT", + UINT2NUM(GRPC_CALL_ERROR_NOT_ON_CLIENT)); + rb_define_const(rb_RpcErrors, "ALREADY_INVOKED", + UINT2NUM(GRPC_CALL_ERROR_ALREADY_INVOKED)); + rb_define_const(rb_RpcErrors, "NOT_INVOKED", + UINT2NUM(GRPC_CALL_ERROR_NOT_INVOKED)); + rb_define_const(rb_RpcErrors, "ALREADY_FINISHED", + UINT2NUM(GRPC_CALL_ERROR_ALREADY_FINISHED)); + rb_define_const(rb_RpcErrors, "TOO_MANY_OPERATIONS", + UINT2NUM(GRPC_CALL_ERROR_TOO_MANY_OPERATIONS)); + rb_define_const(rb_RpcErrors, "INVALID_FLAGS", + UINT2NUM(GRPC_CALL_ERROR_INVALID_FLAGS)); + + /* Add the detail strings to a Hash */ + rb_error_code_details = rb_hash_new(); + rb_hash_aset(rb_error_code_details, + UINT2NUM(GRPC_CALL_OK), rb_str_new2("ok")); + rb_hash_aset(rb_error_code_details, UINT2NUM(GRPC_CALL_ERROR), + rb_str_new2("unknown error")); + rb_hash_aset(rb_error_code_details, UINT2NUM(GRPC_CALL_ERROR_NOT_ON_SERVER), + rb_str_new2("not available on a server")); + rb_hash_aset(rb_error_code_details, UINT2NUM(GRPC_CALL_ERROR_NOT_ON_CLIENT), + rb_str_new2("not available on a client")); + rb_hash_aset(rb_error_code_details, UINT2NUM(GRPC_CALL_ERROR_ALREADY_INVOKED), + rb_str_new2("call is already invoked")); + rb_hash_aset(rb_error_code_details, UINT2NUM(GRPC_CALL_ERROR_NOT_INVOKED), + rb_str_new2("call is not yet invoked")); + rb_hash_aset(rb_error_code_details, + UINT2NUM(GRPC_CALL_ERROR_ALREADY_FINISHED), + rb_str_new2("call is already finished")); + rb_hash_aset(rb_error_code_details, + UINT2NUM(GRPC_CALL_ERROR_TOO_MANY_OPERATIONS), + rb_str_new2("outstanding read or write present")); + rb_hash_aset(rb_error_code_details, UINT2NUM(GRPC_CALL_ERROR_INVALID_FLAGS), + rb_str_new2("a bad flag was given")); + rb_define_const(rb_RpcErrors, "ErrorMessages", rb_error_code_details); + rb_obj_freeze(rb_error_code_details); +} + +void Init_google_rpc_call() { + /* CallError inherits from Exception to signal that it is non-recoverable */ + rb_eCallError = rb_define_class_under(rb_mGoogleRPC, "CallError", + rb_eException); + rb_cCall = rb_define_class_under(rb_mGoogleRPC, "Call", rb_cObject); + + /* Prevent allocation or inialization of the Call class */ + rb_define_alloc_func(rb_cCall, grpc_rb_cannot_alloc); + rb_define_method(rb_cCall, "initialize", grpc_rb_cannot_init, 0); + rb_define_method(rb_cCall, "initialize_copy", grpc_rb_cannot_init_copy, 1); + + /* Add ruby analogues of the Call methods. */ + rb_define_method(rb_cCall, "accept", grpc_rb_call_accept, -1); + rb_define_method(rb_cCall, "add_metadata", grpc_rb_call_add_metadata, + -1); + rb_define_method(rb_cCall, "cancel", grpc_rb_call_cancel, 0); + rb_define_method(rb_cCall, "start_invoke", grpc_rb_call_start_invoke, -1); + rb_define_method(rb_cCall, "start_read", grpc_rb_call_start_read, 1); + rb_define_method(rb_cCall, "start_write", grpc_rb_call_start_write, -1); + rb_define_method(rb_cCall, "start_write_status", + grpc_rb_call_start_write_status, 2); + rb_define_method(rb_cCall, "writes_done", grpc_rb_call_writes_done, 1); + rb_define_method(rb_cCall, "status", grpc_rb_call_get_status, 0); + rb_define_method(rb_cCall, "status=", grpc_rb_call_set_status, 1); + rb_define_method(rb_cCall, "metadata", grpc_rb_call_get_metadata, 0); + rb_define_method(rb_cCall, "metadata=", grpc_rb_call_set_metadata, 1); + + /* Ids used to support call attributes */ + id_metadata = rb_intern("metadata"); + id_status = rb_intern("status"); + + /* Ids used by the c wrapping internals. */ + id_cq = rb_intern("__cq"); + id_flags = rb_intern("__flags"); + id_input_md = rb_intern("__input_md"); + + /* The hash for reference counting calls, to ensure they can't be destroyed + * more than once */ + hash_all_calls = rb_hash_new(); + rb_define_const(rb_cCall, "INTERNAL_ALL_CALLs", hash_all_calls); + + Init_google_rpc_error_codes(); +} + +/* Gets the call from the ruby object */ +grpc_call* grpc_rb_get_wrapped_call(VALUE v) { + grpc_call *c = NULL; + Data_Get_Struct(v, grpc_call, c); + return c; +} + +/* Obtains the wrapped object for a given call */ +VALUE grpc_rb_wrap_call(grpc_call* c) { + VALUE obj = Qnil; + if (c == NULL) { + return Qnil; + } + obj = rb_hash_aref(hash_all_calls, OFFT2NUM((VALUE)c)); + if (obj == Qnil) { /* Not in the hash add it */ + rb_hash_aset(hash_all_calls, OFFT2NUM((VALUE)c), UINT2NUM(1)); + } else { + rb_hash_aset(hash_all_calls, OFFT2NUM((VALUE)c), + UINT2NUM(NUM2UINT(obj) + 1)); + } + return Data_Wrap_Struct(rb_cCall, GC_NOT_MARKED, grpc_rb_call_destroy, + c); +} diff --git a/src/ruby/ext/grpc/rb_call.h b/src/ruby/ext/grpc/rb_call.h new file mode 100644 index 0000000000..422e7e7a6c --- /dev/null +++ b/src/ruby/ext/grpc/rb_call.h @@ -0,0 +1,59 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#ifndef GRPC_RB_CALL_H_ +#define GRPC_RB_CALL_H_ + +#include <grpc/grpc.h> +#include <ruby.h> + +/* Gets the wrapped call from a VALUE. */ +grpc_call* grpc_rb_get_wrapped_call(VALUE v); + +/* Gets the VALUE corresponding to given grpc_call. */ +VALUE grpc_rb_wrap_call(grpc_call* c); + +/* Provides the details of an call error */ +const char* grpc_call_error_detail_of(grpc_call_error err); + +/* rb_cCall is the Call class whose instances proxy grpc_call. */ +extern VALUE rb_cCall; + +/* rb_cCallError is the ruby class of the exception thrown during call + operations. */ +extern VALUE rb_eCallError; + +/* Initializes the Call class. */ +void Init_google_rpc_call(); + +#endif /* GRPC_RB_CALL_H_ */ diff --git a/src/ruby/ext/grpc/rb_channel.c b/src/ruby/ext/grpc/rb_channel.c new file mode 100644 index 0000000000..f4c09a392a --- /dev/null +++ b/src/ruby/ext/grpc/rb_channel.c @@ -0,0 +1,235 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#include "rb_channel.h" + +#include <ruby.h> + +#include <grpc/grpc.h> +#include "rb_grpc.h" +#include "rb_call.h" +#include "rb_channel_args.h" +#include "rb_completion_queue.h" +#include "rb_server.h" + +/* id_channel is the name of the hidden ivar that preserves a reference to the + * channel on a call, so that calls are not GCed before their channel. */ +static ID id_channel; + +/* id_target is the name of the hidden ivar that preserves a reference to the + * target string used to create the call, preserved so that is does not get + * GCed before the channel */ +static ID id_target; + +/* Used during the conversion of a hash to channel args during channel setup */ +static VALUE rb_cChannelArgs; + +/* grpc_rb_channel wraps a grpc_channel. It provides a peer ruby object, + * 'mark' to minimize copying when a channel is created from ruby. */ +typedef struct grpc_rb_channel { + /* Holder of ruby objects involved in constructing the channel */ + VALUE mark; + /* The actual channel */ + grpc_channel *wrapped; +} grpc_rb_channel; + +/* Destroys Channel instances. */ +static void grpc_rb_channel_free(void *p) { + grpc_rb_channel *ch = NULL; + if (p == NULL) { + return; + }; + ch = (grpc_rb_channel *)p; + + /* Deletes the wrapped object if the mark object is Qnil, which indicates + * that no other object is the actual owner. */ + if (ch->wrapped != NULL && ch->mark == Qnil) { + grpc_channel_destroy(ch->wrapped); + rb_warning("channel gc: destroyed the c channel"); + } else { + rb_warning("channel gc: did not destroy the c channel"); + } + + xfree(p); +} + +/* Protects the mark object from GC */ +static void grpc_rb_channel_mark(void *p) { + grpc_rb_channel *channel = NULL; + if (p == NULL) { + return; + } + channel = (grpc_rb_channel *)p; + if (channel->mark != Qnil) { + rb_gc_mark(channel->mark); + } +} + +/* Allocates grpc_rb_channel instances. */ +static VALUE grpc_rb_channel_alloc(VALUE cls) { + grpc_rb_channel *wrapper = ALLOC(grpc_rb_channel); + wrapper->wrapped = NULL; + wrapper->mark = Qnil; + return Data_Wrap_Struct(cls, grpc_rb_channel_mark, grpc_rb_channel_free, + wrapper); +} + +/* Initializes channel instances */ +static VALUE grpc_rb_channel_init(VALUE self, VALUE target, + VALUE channel_args) { + grpc_rb_channel *wrapper = NULL; + grpc_channel *ch = NULL; + char *target_chars = StringValueCStr(target); + grpc_channel_args args; + MEMZERO(&args, grpc_channel_args, 1); + + Data_Get_Struct(self, grpc_rb_channel, wrapper); + grpc_rb_hash_convert_to_channel_args(channel_args, &args); + ch = grpc_channel_create(target_chars, &args); + if (args.args != NULL) { + xfree(args.args); /* Allocated by grpc_rb_hash_convert_to_channel_args */ + } + if (ch == NULL) { + rb_raise(rb_eRuntimeError, "could not create an rpc channel to target:%s", + target_chars); + } + rb_ivar_set(self, id_target, target); + wrapper->wrapped = ch; + return self; +} + +/* Clones Channel instances. + + Gives Channel a consistent implementation of Ruby's object copy/dup + protocol. */ +static VALUE grpc_rb_channel_init_copy(VALUE copy, VALUE orig) { + grpc_rb_channel *orig_ch = NULL; + grpc_rb_channel *copy_ch = NULL; + + if (copy == orig) { + return copy; + } + + /* Raise an error if orig is not a channel object or a subclass. */ + if (TYPE(orig) != T_DATA || + RDATA(orig)->dfree != (RUBY_DATA_FUNC)grpc_rb_channel_free) { + rb_raise(rb_eTypeError, "not a %s", rb_obj_classname(rb_cChannel)); + } + + Data_Get_Struct(orig, grpc_rb_channel, orig_ch); + Data_Get_Struct(copy, grpc_rb_channel, copy_ch); + + /* use ruby's MEMCPY to make a byte-for-byte copy of the channel wrapper + * object. */ + MEMCPY(copy_ch, orig_ch, grpc_rb_channel, 1); + return copy; +} + +/* Create a call given a grpc_channel, in order to call method. The request + is not sent until grpc_call_invoke is called. */ +static VALUE grpc_rb_channel_create_call(VALUE self, VALUE method, VALUE host, + VALUE deadline) { + VALUE res = Qnil; + grpc_rb_channel *wrapper = NULL; + grpc_channel *ch = NULL; + grpc_call *call = NULL; + char *method_chars = StringValueCStr(method); + char *host_chars = StringValueCStr(host); + + Data_Get_Struct(self, grpc_rb_channel, wrapper); + ch = wrapper->wrapped; + if (ch == NULL) { + rb_raise(rb_eRuntimeError, "closed!"); + } + + call = grpc_channel_create_call(ch, method_chars, host_chars, + grpc_rb_time_timeval(deadline, + /* absolute time */ 0)); + if (call == NULL) { + rb_raise(rb_eRuntimeError, "cannot create call with method %s", + method_chars); + } + res = grpc_rb_wrap_call(call); + + /* Make this channel an instance attribute of the call so that is is not GCed + * before the call. */ + rb_ivar_set(res, id_channel, self); + return res; +} + +/* Closes the channel, calling it's destroy method */ +static VALUE grpc_rb_channel_destroy(VALUE self) { + grpc_rb_channel *wrapper = NULL; + grpc_channel *ch = NULL; + + Data_Get_Struct(self, grpc_rb_channel, wrapper); + ch = wrapper->wrapped; + if (ch != NULL) { + grpc_channel_destroy(ch); + wrapper->wrapped = NULL; + wrapper->mark = Qnil; + } + + return Qnil; +} + +/* rb_cChannel is the ruby class that proxies grpc_channel. */ +VALUE rb_cChannel = Qnil; + +void Init_google_rpc_channel() { + rb_cChannelArgs = rb_define_class("TmpChannelArgs", rb_cObject); + rb_cChannel = rb_define_class_under(rb_mGoogleRPC, "Channel", rb_cObject); + + /* Allocates an object managed by the ruby runtime */ + rb_define_alloc_func(rb_cChannel, grpc_rb_channel_alloc); + + /* Provides a ruby constructor and support for dup/clone. */ + rb_define_method(rb_cChannel, "initialize", grpc_rb_channel_init, 2); + rb_define_method(rb_cChannel, "initialize_copy", grpc_rb_channel_init_copy, + 1); + + /* Add ruby analogues of the Channel methods. */ + rb_define_method(rb_cChannel, "create_call", grpc_rb_channel_create_call, 3); + rb_define_method(rb_cChannel, "destroy", grpc_rb_channel_destroy, 0); + rb_define_alias(rb_cChannel, "close", "destroy"); + + id_channel = rb_intern("__channel"); + id_target = rb_intern("__target"); +} + +/* Gets the wrapped channel from the ruby wrapper */ +grpc_channel* grpc_rb_get_wrapped_channel(VALUE v) { + grpc_rb_channel *wrapper = NULL; + Data_Get_Struct(v, grpc_rb_channel, wrapper); + return wrapper->wrapped; +} diff --git a/src/ruby/ext/grpc/rb_channel.h b/src/ruby/ext/grpc/rb_channel.h new file mode 100644 index 0000000000..b0a3634474 --- /dev/null +++ b/src/ruby/ext/grpc/rb_channel.h @@ -0,0 +1,49 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#ifndef GRPC_RB_CHANNEL_H_ +#define GRPC_RB_CHANNEL_H_ + +#include <ruby.h> +#include <grpc/grpc.h> + +/* rb_cChannel is the Channel class whose instances proxy grpc_channel. */ +extern VALUE rb_cChannel; + +/* Initializes the Channel class. */ +void Init_google_rpc_channel(); + +/* Gets the wrapped channel from the ruby wrapper */ +grpc_channel* grpc_rb_get_wrapped_channel(VALUE v); + +#endif /* GRPC_RB_CHANNEL_H_ */ diff --git a/src/ruby/ext/grpc/rb_channel_args.c b/src/ruby/ext/grpc/rb_channel_args.c new file mode 100644 index 0000000000..eebced0bd8 --- /dev/null +++ b/src/ruby/ext/grpc/rb_channel_args.c @@ -0,0 +1,157 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#include "rb_channel_args.h" + +#include <ruby.h> +#include <grpc/grpc.h> + +#include "rb_grpc.h" + +/* A callback the processes the hash key values in channel_args hash */ +static int grpc_rb_channel_create_in_process_add_args_hash_cb(VALUE key, + VALUE val, + VALUE args_obj) { + const char* the_key; + grpc_channel_args* args; + + switch (TYPE(key)) { + + case T_STRING: + the_key = StringValuePtr(key); + break; + + case T_SYMBOL: + the_key = rb_id2name(SYM2ID(key)); + break; + + default: + rb_raise(rb_eTypeError, "bad chan arg: got <%s>, want <String|Symbol>", + rb_obj_classname(key)); + return ST_STOP; + } + + Data_Get_Struct(args_obj, grpc_channel_args, args); + if (args->num_args <= 0) { + rb_raise(rb_eRuntimeError, "hash_cb bug: num_args is %lu for key:%s", + args->num_args, StringValueCStr(key)); + return ST_STOP; + } + + args->args[args->num_args - 1].key = (char *)the_key; + switch (TYPE(val)) { + + case T_SYMBOL: + args->args[args->num_args - 1].type = GRPC_ARG_STRING; + args->args[args->num_args - 1].value.string = + (char *)rb_id2name(SYM2ID(val)); + --args->num_args; + return ST_CONTINUE; + + case T_STRING: + args->args[args->num_args - 1].type = GRPC_ARG_STRING; + args->args[args->num_args - 1].value.string = StringValueCStr(val); + --args->num_args; + return ST_CONTINUE; + + case T_FIXNUM: + args->args[args->num_args - 1].type = GRPC_ARG_INTEGER; + args->args[args->num_args - 1].value.integer = NUM2INT(val); + --args->num_args; + return ST_CONTINUE; + + default: + rb_raise(rb_eTypeError, "%s: bad value: got <%s>, want <String|Fixnum>", + StringValueCStr(key), rb_obj_classname(val)); + return ST_STOP; + } + rb_raise(rb_eRuntimeError, "impl bug: hash_cb reached to far while on key:%s", + StringValueCStr(key)); + return ST_STOP; +} + +/* channel_convert_params allows the call to + grpc_rb_hash_convert_to_channel_args to be made within an rb_protect + exception-handler. This allows any allocated memory to be freed before + propagating any exception that occurs */ +typedef struct channel_convert_params { + VALUE src_hash; + grpc_channel_args* dst; +} channel_convert_params; + + +static VALUE grpc_rb_hash_convert_to_channel_args0(VALUE as_value) { + ID id_size = rb_intern("size"); + VALUE rb_cChannelArgs = rb_define_class("TmpChannelArgs", rb_cObject); + channel_convert_params* params = (channel_convert_params *)as_value; + size_t num_args = 0; + + if (!NIL_P(params->src_hash) && TYPE(params->src_hash) != T_HASH) { + rb_raise(rb_eTypeError, "bad channel args: got:<%s> want: a hash or nil", + rb_obj_classname(params->src_hash)); + return Qnil; + } + + if (TYPE(params->src_hash) == T_HASH) { + num_args = NUM2INT(rb_funcall(params->src_hash, id_size, 0)); + params->dst->num_args = num_args; + params->dst->args = ALLOC_N(grpc_arg, num_args); + MEMZERO(params->dst->args, grpc_arg, num_args); + rb_hash_foreach(params->src_hash, + grpc_rb_channel_create_in_process_add_args_hash_cb, + Data_Wrap_Struct(rb_cChannelArgs, GC_NOT_MARKED, + GC_DONT_FREE, params->dst)); + /* reset num_args as grpc_rb_channel_create_in_process_add_args_hash_cb + * decrements it during has processing */ + params->dst->num_args = num_args; + } + return Qnil; +} + +void grpc_rb_hash_convert_to_channel_args(VALUE src_hash, + grpc_channel_args* dst) { + channel_convert_params params; + int status = 0; + + /* Make a protected call to grpc_rb_hash_convert_channel_args */ + params.src_hash = src_hash; + params.dst = dst; + rb_protect(grpc_rb_hash_convert_to_channel_args0, (VALUE) ¶ms, &status); + if (status != 0) { + if (dst->args != NULL) { + /* Free any allocated memory before propagating the error */ + xfree(dst->args); + } + rb_jump_tag(status); + } +} diff --git a/src/ruby/ext/grpc/rb_channel_args.h b/src/ruby/ext/grpc/rb_channel_args.h new file mode 100644 index 0000000000..bbff017c1e --- /dev/null +++ b/src/ruby/ext/grpc/rb_channel_args.h @@ -0,0 +1,53 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#ifndef GRPC_RB_CHANNEL_ARGS_H_ +#define GRPC_RB_CHANNEL_ARGS_H_ + +#include <ruby.h> +#include <grpc/grpc.h> + +/* Converts a hash object containing channel args to a channel args instance. + * + * This func ALLOCs args->args. The caller is responsible for freeing it. If + * a ruby error is raised during processing of the hash values, the func takes + * care to deallocate any memory allocated so far, and propagate the error. + * + * @param src_hash A ruby hash + * @param dst the grpc_channel_args that the hash entries will be added to. + */ +void grpc_rb_hash_convert_to_channel_args(VALUE src_hash, + grpc_channel_args* dst); + + +#endif /* GRPC_RB_CHANNEL_ARGS_H_ */ diff --git a/src/ruby/ext/grpc/rb_completion_queue.c b/src/ruby/ext/grpc/rb_completion_queue.c new file mode 100644 index 0000000000..62d045e971 --- /dev/null +++ b/src/ruby/ext/grpc/rb_completion_queue.c @@ -0,0 +1,194 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#include "rb_completion_queue.h" + +#include <ruby.h> + +#include <grpc/grpc.h> +#include <grpc/support/time.h> +#include "rb_grpc.h" +#include "rb_event.h" + +/* Used to allow grpc_completion_queue_next call to release the GIL */ +typedef struct next_call_stack { + grpc_completion_queue *cq; + grpc_event *event; + gpr_timespec timeout; + void* tag; +} next_call_stack; + +/* Calls grpc_completion_queue_next without holding the ruby GIL */ +static void *grpc_rb_completion_queue_next_no_gil( + next_call_stack *next_call) { + next_call->event = grpc_completion_queue_next(next_call->cq, + next_call->timeout); + return NULL; +} + +/* Calls grpc_completion_queue_pluck without holding the ruby GIL */ +static void *grpc_rb_completion_queue_pluck_no_gil( + next_call_stack *next_call) { + next_call->event = grpc_completion_queue_pluck(next_call->cq, + next_call->tag, + next_call->timeout); + return NULL; +} + + +/* Shuts down and drains the completion queue if necessary. + * + * This is done when the ruby completion queue object is about to be GCed. + */ +static void grpc_rb_completion_queue_shutdown_drain( + grpc_completion_queue* cq) { + next_call_stack next_call; + grpc_completion_type type; + int drained = 0; + MEMZERO(&next_call, next_call_stack, 1); + + grpc_completion_queue_shutdown(cq); + next_call.cq = cq; + next_call.event = NULL; + /* TODO(temiola): the timeout should be a module level constant that defaults + * to gpr_inf_future. + * + * - at the moment this does not work, it stalls. Using a small timeout like + * this one works, and leads to fast test run times; a longer timeout was + * causing unnecessary delays in the test runs. + * + * - investigate further, this is probably another example of C-level cleanup + * not working consistently in all cases. + */ + next_call.timeout = gpr_time_add(gpr_now(), gpr_time_from_micros(5e3)); + do { + rb_thread_call_without_gvl(grpc_rb_completion_queue_next_no_gil, + (void *)&next_call, NULL, NULL); + if (next_call.event == NULL) { + break; + } + type = next_call.event->type; + if (type != GRPC_QUEUE_SHUTDOWN) { + ++drained; + rb_warning("completion queue shutdown: %d undrained events", drained); + } + grpc_event_finish(next_call.event); + next_call.event = NULL; + } while (type != GRPC_QUEUE_SHUTDOWN); +} + +/* Helper function to free a completion queue. */ +static void grpc_rb_completion_queue_destroy(void *p) { + grpc_completion_queue *cq = NULL; + if (p == NULL) { + return; + } + cq = (grpc_completion_queue *)p; + grpc_rb_completion_queue_shutdown_drain(cq); + grpc_completion_queue_destroy(cq); +} + +/* Allocates a completion queue. */ +static VALUE grpc_rb_completion_queue_alloc(VALUE cls) { + grpc_completion_queue* cq = grpc_completion_queue_create(); + if (cq == NULL) { + rb_raise(rb_eArgError, + "could not create a completion queue: not sure why"); + } + return Data_Wrap_Struct(cls, GC_NOT_MARKED, + grpc_rb_completion_queue_destroy, cq); +} + +/* Blocks until the next event is available, and returns the event. */ +static VALUE grpc_rb_completion_queue_next(VALUE self, VALUE timeout) { + next_call_stack next_call; + MEMZERO(&next_call, next_call_stack, 1); + Data_Get_Struct(self, grpc_completion_queue, next_call.cq); + next_call.timeout = grpc_rb_time_timeval(timeout, /* absolute time*/ 0); + next_call.event = NULL; + rb_thread_call_without_gvl(grpc_rb_completion_queue_next_no_gil, + (void *)&next_call, NULL, NULL); + if (next_call.event == NULL) { + return Qnil; + } + return Data_Wrap_Struct(rb_cEvent, GC_NOT_MARKED, grpc_rb_event_finish, + next_call.event); +} + +/* Blocks until the next event for given tag is available, and returns the + * event. */ +static VALUE grpc_rb_completion_queue_pluck(VALUE self, VALUE tag, + VALUE timeout) { + next_call_stack next_call; + MEMZERO(&next_call, next_call_stack, 1); + Data_Get_Struct(self, grpc_completion_queue, next_call.cq); + next_call.timeout = grpc_rb_time_timeval(timeout, /* absolute time*/ 0); + next_call.tag = ROBJECT(tag); + next_call.event = NULL; + rb_thread_call_without_gvl(grpc_rb_completion_queue_pluck_no_gil, + (void *)&next_call, NULL, NULL); + if (next_call.event == NULL) { + return Qnil; + } + return Data_Wrap_Struct(rb_cEvent, GC_NOT_MARKED, grpc_rb_event_finish, + next_call.event); +} + +/* rb_cCompletionQueue is the ruby class that proxies grpc_completion_queue. */ +VALUE rb_cCompletionQueue = Qnil; + +void Init_google_rpc_completion_queue() { + rb_cCompletionQueue = rb_define_class_under(rb_mGoogleRPC, + "CompletionQueue", + rb_cObject); + + /* constructor: uses an alloc func without an initializer. Using a simple + alloc func works here as the grpc header does not specify any args for + this func, so no separate initialization step is necessary. */ + rb_define_alloc_func(rb_cCompletionQueue, grpc_rb_completion_queue_alloc); + + /* Add the next method that waits for the next event. */ + rb_define_method(rb_cCompletionQueue, "next", + grpc_rb_completion_queue_next, 1); + + /* Add the pluck method that waits for the next event of given tag */ + rb_define_method(rb_cCompletionQueue, "pluck", + grpc_rb_completion_queue_pluck, 2); +} + +/* Gets the wrapped completion queue from the ruby wrapper */ +grpc_completion_queue* grpc_rb_get_wrapped_completion_queue(VALUE v) { + grpc_completion_queue *cq = NULL; + Data_Get_Struct(v, grpc_completion_queue, cq); + return cq; +} diff --git a/src/ruby/ext/grpc/rb_completion_queue.h b/src/ruby/ext/grpc/rb_completion_queue.h new file mode 100644 index 0000000000..1ec2718ed4 --- /dev/null +++ b/src/ruby/ext/grpc/rb_completion_queue.h @@ -0,0 +1,50 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#ifndef GRPC_RB_COMPLETION_QUEUE_H_ +#define GRPC_RB_COMPLETION_QUEUE_H_ + +#include <grpc/grpc.h> +#include <ruby.h> + +/* Gets the wrapped completion queue from the ruby wrapper */ +grpc_completion_queue *grpc_rb_get_wrapped_completion_queue(VALUE v); + +/* rb_cCompletionQueue is the CompletionQueue class whose instances proxy + grpc_completion_queue. */ +extern VALUE rb_cCompletionQueue; + +/* Initializes the CompletionQueue class. */ +void Init_google_rpc_completion_queue(); + +#endif /* GRPC_RB_COMPLETION_QUEUE_H_ */ diff --git a/src/ruby/ext/grpc/rb_event.c b/src/ruby/ext/grpc/rb_event.c new file mode 100644 index 0000000000..6f542f9eba --- /dev/null +++ b/src/ruby/ext/grpc/rb_event.c @@ -0,0 +1,284 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#include "rb_event.h" + +#include <ruby.h> + +#include <grpc/grpc.h> +#include "rb_grpc.h" +#include "rb_byte_buffer.h" +#include "rb_call.h" +#include "rb_metadata.h" +#include "rb_status.h" + +/* rb_mCompletionType is a ruby module that holds the completion type values */ +VALUE rb_mCompletionType = Qnil; + +/* Helper function to free an event. */ +void grpc_rb_event_finish(void *p) { + grpc_event_finish(p); +} + +static VALUE grpc_rb_event_result(VALUE self); + +/* Obtains the type of an event. */ +static VALUE grpc_rb_event_type(VALUE self) { + grpc_event *event = NULL; + Data_Get_Struct(self, grpc_event, event); + switch (event->type) { + case GRPC_QUEUE_SHUTDOWN: + return rb_const_get(rb_mCompletionType, rb_intern("QUEUE_SHUTDOWN")); + + case GRPC_READ: + return rb_const_get(rb_mCompletionType, rb_intern("READ")); + + case GRPC_INVOKE_ACCEPTED: + grpc_rb_event_result(self); /* validates the result */ + return rb_const_get(rb_mCompletionType, rb_intern("INVOKE_ACCEPTED")); + + case GRPC_WRITE_ACCEPTED: + grpc_rb_event_result(self); /* validates the result */ + return rb_const_get(rb_mCompletionType, rb_intern("WRITE_ACCEPTED")); + + case GRPC_FINISH_ACCEPTED: + grpc_rb_event_result(self); /* validates the result */ + return rb_const_get(rb_mCompletionType, rb_intern("FINISH_ACCEPTED")); + + case GRPC_CLIENT_METADATA_READ: + return rb_const_get(rb_mCompletionType, + rb_intern("CLIENT_METADATA_READ")); + + case GRPC_FINISHED: + return rb_const_get(rb_mCompletionType, rb_intern("FINISHED")); + + case GRPC_SERVER_RPC_NEW: + return rb_const_get(rb_mCompletionType, rb_intern("SERVER_RPC_NEW")); + + default: + rb_raise(rb_eRuntimeError, + "unrecognized event code for an rpc event:%d", event->type); + } + return Qnil; /* should not be reached */ +} + +/* Obtains the tag associated with an event. */ +static VALUE grpc_rb_event_tag(VALUE self) { + grpc_event *event = NULL; + Data_Get_Struct(self, grpc_event, event); + if (event->tag == NULL) { + return Qnil; + } + return (VALUE)event->tag; +} + +/* Obtains the call associated with an event. */ +static VALUE grpc_rb_event_call(VALUE self) { + grpc_event *ev = NULL; + Data_Get_Struct(self, grpc_event, ev); + if (ev->call != NULL) { + return grpc_rb_wrap_call(ev->call); + } + return Qnil; +} + +/* Obtains the metadata associated with an event. */ +static VALUE grpc_rb_event_metadata(VALUE self) { + grpc_event *event = NULL; + grpc_metadata *metadata = NULL; + VALUE key = Qnil; + VALUE new_ary = Qnil; + VALUE result = Qnil; + VALUE value = Qnil; + size_t count = 0; + size_t i = 0; + + /* Figure out which metadata to read. */ + Data_Get_Struct(self, grpc_event, event); + switch (event->type) { + + case GRPC_CLIENT_METADATA_READ: + count = event->data.client_metadata_read.count; + metadata = event->data.client_metadata_read.elements; + break; + + case GRPC_SERVER_RPC_NEW: + count = event->data.server_rpc_new.metadata_count; + metadata = event->data.server_rpc_new.metadata_elements; + break; + + default: + rb_raise(rb_eRuntimeError, + "bug: bad event type reading server metadata. got %d; want %d", + event->type, GRPC_SERVER_RPC_NEW); + return Qnil; + } + + result = rb_hash_new(); + for (i = 0; i < count; i++) { + key = rb_str_new2(metadata[i].key); + value = rb_hash_aref(result, key); + if (value == Qnil) { + value = rb_str_new( + metadata[i].value, + metadata[i].value_length); + rb_hash_aset(result, key, value); + } else if (TYPE(value) == T_ARRAY) { + /* Add the string to the returned array */ + rb_ary_push(value, rb_str_new( + metadata[i].value, + metadata[i].value_length)); + } else { + /* Add the current value with this key and the new one to an array */ + new_ary = rb_ary_new(); + rb_ary_push(new_ary, value); + rb_ary_push(new_ary, rb_str_new( + metadata[i].value, + metadata[i].value_length)); + rb_hash_aset(result, key, new_ary); + } + } + return result; +} + +/* Obtains the data associated with an event. */ +static VALUE grpc_rb_event_result(VALUE self) { + grpc_event *event = NULL; + Data_Get_Struct(self, grpc_event, event); + + switch (event->type) { + + case GRPC_QUEUE_SHUTDOWN: + return Qnil; + + case GRPC_READ: + return grpc_rb_byte_buffer_create_with_mark(self, event->data.read); + + case GRPC_FINISH_ACCEPTED: + if (event->data.finish_accepted == GRPC_OP_OK) { + return Qnil; + } + rb_raise(rb_eEventError, "finish failed, not sure why (code=%d)", + event->data.finish_accepted); + break; + + case GRPC_INVOKE_ACCEPTED: + if (event->data.invoke_accepted == GRPC_OP_OK) { + return Qnil; + } + rb_raise(rb_eEventError, "invoke failed, not sure why (code=%d)", + event->data.invoke_accepted); + break; + + case GRPC_WRITE_ACCEPTED: + if (event->data.write_accepted == GRPC_OP_OK) { + return Qnil; + } + rb_raise(rb_eEventError, "write failed, not sure why (code=%d)", + event->data.invoke_accepted); + break; + + case GRPC_CLIENT_METADATA_READ: + return grpc_rb_event_metadata(self); + + case GRPC_FINISHED: + return grpc_rb_status_create_with_mark(self, &event->data.finished); + break; + + case GRPC_SERVER_RPC_NEW: + return rb_struct_new( + rb_sNewServerRpc, + rb_str_new2(event->data.server_rpc_new.method), + rb_str_new2(event->data.server_rpc_new.host), + Data_Wrap_Struct( + rb_cTimeVal, GC_NOT_MARKED, GC_DONT_FREE, + (void *)&event->data.server_rpc_new.deadline), + grpc_rb_event_metadata(self), + NULL); + + default: + rb_raise(rb_eRuntimeError, + "unrecognized event code for an rpc event:%d", event->type); + } + + return Qfalse; +} + +/* rb_sNewServerRpc is the struct that holds new server rpc details. */ +VALUE rb_sNewServerRpc = Qnil; + +/* rb_cEvent is the Event class whose instances proxy grpc_event */ +VALUE rb_cEvent = Qnil; + +/* rb_eEventError is the ruby class of the exception thrown on failures during + rpc event processing. */ +VALUE rb_eEventError = Qnil; + +void Init_google_rpc_event() { + rb_eEventError = rb_define_class_under(rb_mGoogleRPC, "EventError", + rb_eStandardError); + rb_cEvent = rb_define_class_under(rb_mGoogleRPC, "Event", rb_cObject); + rb_sNewServerRpc = rb_struct_define("NewServerRpc", "method", "host", + "deadline", "metadata", NULL); + + /* Prevent allocation or inialization from ruby. */ + rb_define_alloc_func(rb_cEvent, grpc_rb_cannot_alloc); + rb_define_method(rb_cEvent, "initialize", grpc_rb_cannot_init, 0); + rb_define_method(rb_cEvent, "initialize_copy", grpc_rb_cannot_init_copy, 1); + + /* Accessors for the data available in an event. */ + rb_define_method(rb_cEvent, "call", grpc_rb_event_call, 0); + rb_define_method(rb_cEvent, "result", grpc_rb_event_result, 0); + rb_define_method(rb_cEvent, "tag", grpc_rb_event_tag, 0); + rb_define_method(rb_cEvent, "type", grpc_rb_event_type, 0); + + /* Constants representing the completion types */ + rb_mCompletionType = rb_define_module_under(rb_mGoogleRPC, "CompletionType"); + rb_define_const(rb_mCompletionType, "QUEUE_SHUTDOWN", + INT2NUM(GRPC_QUEUE_SHUTDOWN)); + rb_define_const(rb_mCompletionType, "READ", INT2NUM(GRPC_READ)); + rb_define_const(rb_mCompletionType, "INVOKE_ACCEPTED", + INT2NUM(GRPC_INVOKE_ACCEPTED)); + rb_define_const(rb_mCompletionType, "WRITE_ACCEPTED", + INT2NUM(GRPC_WRITE_ACCEPTED)); + rb_define_const(rb_mCompletionType, "FINISH_ACCEPTED", + INT2NUM(GRPC_FINISH_ACCEPTED)); + rb_define_const(rb_mCompletionType, "CLIENT_METADATA_READ", + INT2NUM(GRPC_CLIENT_METADATA_READ)); + rb_define_const(rb_mCompletionType, "FINISHED", + INT2NUM(GRPC_FINISHED)); + rb_define_const(rb_mCompletionType, "SERVER_RPC_NEW", + INT2NUM(GRPC_SERVER_RPC_NEW)); + rb_define_const(rb_mCompletionType, "RESERVED", + INT2NUM(GRPC_COMPLETION_DO_NOT_USE)); +} diff --git a/src/ruby/ext/grpc/rb_event.h b/src/ruby/ext/grpc/rb_event.h new file mode 100644 index 0000000000..c398b6c6c8 --- /dev/null +++ b/src/ruby/ext/grpc/rb_event.h @@ -0,0 +1,55 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#ifndef GRPC_RB_EVENT_H_ +#define GRPC_RB_EVENT_H_ + +#include <ruby.h> + +/* rb_sNewServerRpc is the struct that holds new server rpc details. */ +extern VALUE rb_sNewServerRpc; + +/* rb_cEvent is the Event class whose instances proxy grpc_event. */ +extern VALUE rb_cEvent; + +/* rb_cEventError is the ruby class that acts the exception thrown during rpc + event processing. */ +extern VALUE rb_eEventError; + +/* Helper function to free an event. */ +void grpc_rb_event_finish(void *p); + +/* Initializes the Event and EventError classes. */ +void Init_google_rpc_event(); + +#endif /* GRPC_RB_EVENT_H_ */ diff --git a/src/ruby/ext/grpc/rb_grpc.c b/src/ruby/ext/grpc/rb_grpc.c new file mode 100644 index 0000000000..5cc45cf743 --- /dev/null +++ b/src/ruby/ext/grpc/rb_grpc.c @@ -0,0 +1,230 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#include "rb_grpc.h" + +#include <math.h> +#include <ruby.h> +#include <sys/time.h> + +#include <grpc/grpc.h> +#include <grpc/support/time.h> +#include "rb_byte_buffer.h" +#include "rb_call.h" +#include "rb_channel.h" +#include "rb_completion_queue.h" +#include "rb_event.h" +#include "rb_metadata.h" +#include "rb_server.h" +#include "rb_status.h" + +/* Define common vars and funcs declared in rb.h */ +const RUBY_DATA_FUNC GC_NOT_MARKED = NULL; +const RUBY_DATA_FUNC GC_DONT_FREE = NULL; + +VALUE rb_cTimeVal = Qnil; + +/* Alloc func that blocks allocation of a given object by raising an + * exception. */ +VALUE grpc_rb_cannot_alloc(VALUE cls) { + rb_raise(rb_eTypeError, + "allocation of %s only allowed from the gRPC native layer", + rb_class2name(cls)); + return Qnil; +} + +/* Init func that fails by raising an exception. */ +VALUE grpc_rb_cannot_init(VALUE self) { + rb_raise(rb_eTypeError, + "initialization of %s only allowed from the gRPC native layer", + rb_obj_classname(self)); + return Qnil; +} + +/* Init/Clone func that fails by raising an exception. */ +VALUE grpc_rb_cannot_init_copy(VALUE copy, VALUE self) { + rb_raise(rb_eTypeError, + "initialization of %s only allowed from the gRPC native layer", + rb_obj_classname(copy)); + return Qnil; +} + +/* id_tv_{,u}sec are accessor methods on Ruby Time instances. */ +static ID id_tv_sec; +static ID id_tv_nsec; + +/** + * grpc_rb_time_timeval creates a time_eval from a ruby time object. + * + * This func is copied from ruby source, MRI/source/time.c, which is published + * under the same license as the ruby.h, on which the entire extensions is + * based. + */ +gpr_timespec grpc_rb_time_timeval(VALUE time, int interval) { + gpr_timespec t; + gpr_timespec *time_const; + const char *tstr = interval ? "time interval" : "time"; + const char *want = " want <secs from epoch>|<Time>|<GRPC::TimeConst.*>"; + + switch (TYPE(time)) { + + case T_DATA: + if (CLASS_OF(time) == rb_cTimeVal) { + Data_Get_Struct(time, gpr_timespec, time_const); + t = *time_const; + } else if (CLASS_OF(time) == rb_cTime) { + t.tv_sec = NUM2INT(rb_funcall(time, id_tv_sec, 0)); + t.tv_nsec = NUM2INT(rb_funcall(time, id_tv_nsec, 0)); + } else { + rb_raise(rb_eTypeError, + "bad input: (%s)->c_timeval, got <%s>,%s", + tstr, rb_obj_classname(time), want); + } + break; + + case T_FIXNUM: + t.tv_sec = FIX2LONG(time); + if (interval && t.tv_sec < 0) + rb_raise(rb_eArgError, "%s must be positive", tstr); + t.tv_nsec = 0; + break; + + case T_FLOAT: + if (interval && RFLOAT(time)->float_value < 0.0) + rb_raise(rb_eArgError, "%s must be positive", tstr); + else { + double f, d; + + d = modf(RFLOAT(time)->float_value, &f); + if (d < 0) { + d += 1; + f -= 1; + } + t.tv_sec = (time_t)f; + if (f != t.tv_sec) { + rb_raise(rb_eRangeError, "%f out of Time range", + RFLOAT(time)->float_value); + } + t.tv_nsec = (time_t)(d*1e9+0.5); + } + break; + + case T_BIGNUM: + t.tv_sec = NUM2LONG(time); + if (interval && t.tv_sec < 0) + rb_raise(rb_eArgError, "%s must be positive", tstr); + t.tv_nsec = 0; + break; + + default: + rb_raise(rb_eTypeError, + "bad input: (%s)->c_timeval, got <%s>,%s", + tstr, rb_obj_classname(time), want); + break; + } + return t; +} + +/* id_at is the constructor method of the ruby standard Time class. */ +static ID id_at; + +/* id_inspect is the inspect method found on various ruby objects. */ +static ID id_inspect; + +/* id_to_s is the to_s method found on various ruby objects. */ +static ID id_to_s; + +/* Converts `a wrapped time constant to a standard time. */ +VALUE grpc_rb_time_val_to_time(VALUE self) { + gpr_timespec *time_const = NULL; + Data_Get_Struct(self, gpr_timespec, time_const); + return rb_funcall(rb_cTime, id_at, 2, INT2NUM(time_const->tv_sec), + INT2NUM(time_const->tv_nsec)); +} + +/* Invokes inspect on the ctime version of the time val. */ +VALUE grpc_rb_time_val_inspect(VALUE self) { + return rb_funcall(grpc_rb_time_val_to_time(self), id_inspect, 0); +} + +/* Invokes to_s on the ctime version of the time val. */ +VALUE grpc_rb_time_val_to_s(VALUE self) { + return rb_funcall(grpc_rb_time_val_to_time(self), id_to_s, 0); +} + +/* Adds a module with constants that map to gpr's static timeval structs. */ +void Init_google_time_consts() { + VALUE rb_mTimeConsts = rb_define_module_under(rb_mGoogleRPC, "TimeConsts"); + rb_cTimeVal = rb_define_class_under(rb_mGoogleRPC, "TimeSpec", rb_cObject); + rb_define_const(rb_mTimeConsts, "ZERO", + Data_Wrap_Struct(rb_cTimeVal, GC_NOT_MARKED, + GC_DONT_FREE, (void *)&gpr_time_0)); + rb_define_const(rb_mTimeConsts, "INFINITE_FUTURE", + Data_Wrap_Struct(rb_cTimeVal, GC_NOT_MARKED, + GC_DONT_FREE, (void *)&gpr_inf_future)); + rb_define_const(rb_mTimeConsts, "INFINITE_PAST", + Data_Wrap_Struct(rb_cTimeVal, GC_NOT_MARKED, + GC_DONT_FREE, (void *)&gpr_inf_past)); + rb_define_method(rb_cTimeVal, "to_time", grpc_rb_time_val_to_time, 0); + rb_define_method(rb_cTimeVal, "inspect", grpc_rb_time_val_inspect, 0); + rb_define_method(rb_cTimeVal, "to_s", grpc_rb_time_val_to_s, 0); + id_at = rb_intern("at"); + id_inspect = rb_intern("inspect"); + id_to_s = rb_intern("to_s"); + id_tv_sec = rb_intern("tv_sec"); + id_tv_nsec = rb_intern("tv_nsec"); +} + +void grpc_rb_shutdown(void *vm) { + grpc_shutdown(); +} + +/* Initialize the Google RPC module. */ +VALUE rb_mGoogle = Qnil; +VALUE rb_mGoogleRPC = Qnil; +void Init_grpc() { + grpc_init(); + ruby_vm_at_exit(grpc_rb_shutdown); + rb_mGoogle = rb_define_module("Google"); + rb_mGoogleRPC = rb_define_module_under(rb_mGoogle, "RPC"); + + Init_google_rpc_byte_buffer(); + Init_google_rpc_event(); + Init_google_rpc_channel(); + Init_google_rpc_completion_queue(); + Init_google_rpc_call(); + Init_google_rpc_metadata(); + Init_google_rpc_server(); + Init_google_rpc_status(); + Init_google_time_consts(); +} diff --git a/src/ruby/ext/grpc/rb_grpc.h b/src/ruby/ext/grpc/rb_grpc.h new file mode 100644 index 0000000000..fd43c3795f --- /dev/null +++ b/src/ruby/ext/grpc/rb_grpc.h @@ -0,0 +1,71 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#ifndef GRPC_RB_H_ +#define GRPC_RB_H_ + +#include <sys/time.h> +#include <ruby.h> +#include <grpc/support/time.h> + +/* rb_mGoogle is the top-level Google module. */ +extern VALUE rb_mGoogle; + +/* rb_mGoogleRPC is the module containing all the ruby wrapper GRPC classes. */ +extern VALUE rb_mGoogleRPC; + +/* Class used to wrap timeval structs. */ +extern VALUE rb_cTimeVal; + +/* GC_NOT_MARKED is used in calls to Data_Wrap_Struct to indicate that the + wrapped struct does not need to participate in ruby gc. */ +extern const RUBY_DATA_FUNC GC_NOT_MARKED; + +/* GC_DONT_FREED is used in calls to Data_Wrap_Struct to indicate that the + wrapped struct should not be freed the wrapped ruby object is released by + the garbage collector. */ +extern const RUBY_DATA_FUNC GC_DONT_FREE; + +/* A ruby object alloc func that fails by raising an exception. */ +VALUE grpc_rb_cannot_alloc(VALUE cls); + +/* A ruby object init func that fails by raising an exception. */ +VALUE grpc_rb_cannot_init(VALUE self); + +/* A ruby object clone init func that fails by raising an exception. */ +VALUE grpc_rb_cannot_init_copy(VALUE copy, VALUE self); + +/* grpc_rb_time_timeval creates a gpr_timespec from a ruby time object. */ +gpr_timespec grpc_rb_time_timeval(VALUE time, int interval); + +#endif /* GRPC_RB_H_ */ diff --git a/src/ruby/ext/grpc/rb_metadata.c b/src/ruby/ext/grpc/rb_metadata.c new file mode 100644 index 0000000000..13d515a929 --- /dev/null +++ b/src/ruby/ext/grpc/rb_metadata.c @@ -0,0 +1,215 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#include "rb_metadata.h" + +#include <ruby.h> +#include <string.h> + +#include <grpc/grpc.h> +#include "rb_grpc.h" + +/* grpc_rb_metadata wraps a grpc_metadata. It provides a peer ruby object, + * 'mark' to minimize copying when a metadata is created from ruby. */ +typedef struct grpc_rb_metadata { + /* Holder of ruby objects involved in constructing the metadata */ + VALUE mark; + /* The actual metadata */ + grpc_metadata *wrapped; +} grpc_rb_metadata; + + +/* Destroys Metadata instances. */ +static void grpc_rb_metadata_free(void *p) { + if (p == NULL) { + return; + }; + + /* Because metadata is only created during a call to grpc_call_add_metadata, + * and the call takes ownership of the metadata, this does not free the + * wrapped struct, only the wrapper */ + xfree(p); +} + +/* Protects the mark object from GC */ +static void grpc_rb_metadata_mark(void *p) { + grpc_rb_metadata *md = NULL; + if (p == NULL) { + return; + } + + md = (grpc_rb_metadata *)p; + /* If it's not already cleaned up, mark the mark object */ + if (md->mark != Qnil && BUILTIN_TYPE(md->mark) != T_NONE) { + rb_gc_mark(md->mark); + } +} + +/* Allocates Metadata instances. + + Provides safe default values for the Metadata fields. */ +static VALUE grpc_rb_metadata_alloc(VALUE cls) { + grpc_rb_metadata *wrapper = ALLOC(grpc_rb_metadata); + wrapper->wrapped = NULL; + wrapper->mark = Qnil; + return Data_Wrap_Struct(cls, grpc_rb_metadata_mark, grpc_rb_metadata_free, + wrapper); +} + +/* id_key and id_value are the names of the hidden ivars that preserve the + * original byte_buffer source string */ +static ID id_key; +static ID id_value; + +/* Initializes Metadata instances. */ +static VALUE grpc_rb_metadata_init(VALUE self, VALUE key, VALUE value) { + grpc_rb_metadata *wrapper = NULL; + grpc_metadata *md = ALLOC(grpc_metadata); + + /* Use direct pointers to the strings wrapped by the ruby object to avoid + * copying */ + Data_Get_Struct(self, grpc_rb_metadata, wrapper); + wrapper->wrapped = md; + if (TYPE(key) == T_SYMBOL) { + md->key = (char *)rb_id2name(SYM2ID(key)); + } else { /* StringValueCStr does all other type exclusions for us */ + md->key = StringValueCStr(key); + } + md->value = RSTRING_PTR(value); + md->value_length = RSTRING_LEN(value); + + /* Save references to the original values on the mark object so that the + * pointers used there are valid for the lifetime of the object. */ + wrapper->mark = rb_class_new_instance(0, NULL, rb_cObject); + rb_ivar_set(wrapper->mark, id_key, key); + rb_ivar_set(wrapper->mark, id_value, value); + + return self; +} + +/* Clones Metadata instances. + + Gives Metadata a consistent implementation of Ruby's object copy/dup + protocol. */ +static VALUE grpc_rb_metadata_init_copy(VALUE copy, VALUE orig) { + grpc_rb_metadata *orig_md = NULL; + grpc_rb_metadata *copy_md = NULL; + + if (copy == orig) { + return copy; + } + + /* Raise an error if orig is not a metadata object or a subclass. */ + if (TYPE(orig) != T_DATA || + RDATA(orig)->dfree != (RUBY_DATA_FUNC)grpc_rb_metadata_free) { + rb_raise(rb_eTypeError, "not a %s", rb_obj_classname(rb_cMetadata)); + } + + Data_Get_Struct(orig, grpc_rb_metadata, orig_md); + Data_Get_Struct(copy, grpc_rb_metadata, copy_md); + + /* use ruby's MEMCPY to make a byte-for-byte copy of the metadata wrapper + * object. */ + MEMCPY(copy_md, orig_md, grpc_rb_metadata, 1); + return copy; +} + +/* Gets the key from a metadata instance. */ +static VALUE grpc_rb_metadata_key(VALUE self) { + VALUE key = Qnil; + grpc_rb_metadata *wrapper = NULL; + grpc_metadata *md = NULL; + + Data_Get_Struct(self, grpc_rb_metadata, wrapper); + if (wrapper->mark != Qnil) { + key = rb_ivar_get(wrapper->mark, id_key); + if (key != Qnil) { + return key; + } + } + + md = wrapper->wrapped; + if (md == NULL || md->key == NULL) { + return Qnil; + } + return rb_str_new2(md->key); +} + +/* Gets the value from a metadata instance. */ +static VALUE grpc_rb_metadata_value(VALUE self) { + VALUE val = Qnil; + grpc_rb_metadata *wrapper = NULL; + grpc_metadata *md = NULL; + + Data_Get_Struct(self, grpc_rb_metadata, wrapper); + if (wrapper->mark != Qnil) { + val = rb_ivar_get(wrapper->mark, id_value); + if (val != Qnil) { + return val; + } + } + + md = wrapper->wrapped; + if (md == NULL || md->value == NULL) { + return Qnil; + } + return rb_str_new2(md->value); +} + +/* rb_cMetadata is the Metadata class whose instances proxy grpc_metadata. */ +VALUE rb_cMetadata = Qnil; +void Init_google_rpc_metadata() { + rb_cMetadata = rb_define_class_under(rb_mGoogleRPC, "Metadata", rb_cObject); + + /* Allocates an object managed by the ruby runtime */ + rb_define_alloc_func(rb_cMetadata, grpc_rb_metadata_alloc); + + /* Provides a ruby constructor and support for dup/clone. */ + rb_define_method(rb_cMetadata, "initialize", grpc_rb_metadata_init, 2); + rb_define_method(rb_cMetadata, "initialize_copy", grpc_rb_metadata_init_copy, + 1); + + /* Provides accessors for the code and details. */ + rb_define_method(rb_cMetadata, "key", grpc_rb_metadata_key, 0); + rb_define_method(rb_cMetadata, "value", grpc_rb_metadata_value, 0); + + id_key = rb_intern("__key"); + id_value = rb_intern("__value"); +} + +/* Gets the wrapped metadata from the ruby wrapper */ +grpc_metadata* grpc_rb_get_wrapped_metadata(VALUE v) { + grpc_rb_metadata *wrapper = NULL; + Data_Get_Struct(v, grpc_rb_metadata, wrapper); + return wrapper->wrapped; +} diff --git a/src/ruby/ext/grpc/rb_metadata.h b/src/ruby/ext/grpc/rb_metadata.h new file mode 100644 index 0000000000..6b705914d6 --- /dev/null +++ b/src/ruby/ext/grpc/rb_metadata.h @@ -0,0 +1,53 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#ifndef GRPC_RB_METADATA_H_ +#define GRPC_RB_METADATA_H_ + +#include <grpc/grpc.h> +#include <ruby.h> + +/* rb_cMetadata is the Metadata class whose instances proxy grpc_metadata. */ +extern VALUE rb_cMetadata; + +/* grpc_rb_metadata_create_with_mark creates a grpc_rb_metadata with a ruby mark + * object that will be kept alive while the metadata is alive. */ +extern VALUE grpc_rb_metadata_create_with_mark(VALUE mark, grpc_metadata *md); + +/* Gets the wrapped metadata from the ruby wrapper */ +grpc_metadata* grpc_rb_get_wrapped_metadata(VALUE v); + +/* Initializes the Metadata class. */ +void Init_google_rpc_metadata(); + +#endif /* GRPC_RB_METADATA_H_ */ diff --git a/src/ruby/ext/grpc/rb_server.c b/src/ruby/ext/grpc/rb_server.c new file mode 100644 index 0000000000..f4230bd471 --- /dev/null +++ b/src/ruby/ext/grpc/rb_server.c @@ -0,0 +1,226 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#include "rb_server.h" + +#include <ruby.h> + +#include <grpc/grpc.h> +#include "rb_call.h" +#include "rb_channel_args.h" +#include "rb_completion_queue.h" +#include "rb_grpc.h" + +/* rb_cServer is the ruby class that proxies grpc_server. */ +VALUE rb_cServer = Qnil; + +/* grpc_rb_server wraps a grpc_server. It provides a peer ruby object, + * 'mark' to minimize copying when a server is created from ruby. */ +typedef struct grpc_rb_server { + /* Holder of ruby objects involved in constructing the server */ + VALUE mark; + /* The actual server */ + grpc_server *wrapped; +} grpc_rb_server; + +/* Destroys server instances. */ +static void grpc_rb_server_free(void *p) { + grpc_rb_server *svr = NULL; + if (p == NULL) { + return; + }; + svr = (grpc_rb_server *)p; + + /* Deletes the wrapped object if the mark object is Qnil, which indicates + * that no other object is the actual owner. */ + if (svr->wrapped != NULL && svr->mark == Qnil) { + grpc_server_shutdown(svr->wrapped); + grpc_server_destroy(svr->wrapped); + } + + xfree(p); +} + +/* Protects the mark object from GC */ +static void grpc_rb_server_mark(void *p) { + grpc_rb_server *server = NULL; + if (p == NULL) { + return; + } + server = (grpc_rb_server *)p; + if (server->mark != Qnil) { + rb_gc_mark(server->mark); + } +} + +/* Allocates grpc_rb_server instances. */ +static VALUE grpc_rb_server_alloc(VALUE cls) { + grpc_rb_server *wrapper = ALLOC(grpc_rb_server); + wrapper->wrapped = NULL; + wrapper->mark = Qnil; + return Data_Wrap_Struct(cls, grpc_rb_server_mark, grpc_rb_server_free, + wrapper); +} + +/* Initializes Server instances. */ +static VALUE grpc_rb_server_init(VALUE self, VALUE cqueue, VALUE channel_args) { + grpc_completion_queue *cq = grpc_rb_get_wrapped_completion_queue(cqueue); + grpc_rb_server *wrapper = NULL; + grpc_server *srv = NULL; + grpc_channel_args args; + MEMZERO(&args, grpc_channel_args, 1); + + Data_Get_Struct(self, grpc_rb_server, wrapper); + grpc_rb_hash_convert_to_channel_args(channel_args, &args); + srv = grpc_server_create(cq, &args); + if (args.args != NULL) { + xfree(args.args); /* Allocated by grpc_rb_hash_convert_to_channel_args */ + } + if (srv == NULL) { + rb_raise(rb_eRuntimeError, "could not create a gRPC server, not sure why"); + } + wrapper->wrapped = srv; + + /* Add the cq as the server's mark object. This ensures the ruby cq can't be + * GCed before the server */ + wrapper->mark = cqueue; + return self; +} + +/* Clones Server instances. + + Gives Server a consistent implementation of Ruby's object copy/dup + protocol. */ +static VALUE grpc_rb_server_init_copy(VALUE copy, VALUE orig) { + grpc_rb_server *orig_srv = NULL; + grpc_rb_server *copy_srv = NULL; + + if (copy == orig) { + return copy; + } + + /* Raise an error if orig is not a server object or a subclass. */ + if (TYPE(orig) != T_DATA || + RDATA(orig)->dfree != (RUBY_DATA_FUNC)grpc_rb_server_free) { + rb_raise(rb_eTypeError, "not a %s", rb_obj_classname(rb_cServer)); + } + + Data_Get_Struct(orig, grpc_rb_server, orig_srv); + Data_Get_Struct(copy, grpc_rb_server, copy_srv); + + /* use ruby's MEMCPY to make a byte-for-byte copy of the server wrapper + * object. */ + MEMCPY(copy_srv, orig_srv, grpc_rb_server, 1); + return copy; +} + +static VALUE grpc_rb_server_request_call(VALUE self, VALUE tag_new) { + grpc_call_error err; + grpc_rb_server *s = NULL; + Data_Get_Struct(self, grpc_rb_server, s); + if (s->wrapped == NULL) { + rb_raise(rb_eRuntimeError, "closed!"); + } else { + err = grpc_server_request_call(s->wrapped, ROBJECT(tag_new)); + if (err != GRPC_CALL_OK) { + rb_raise(rb_eCallError, "server request failed: %s (code=%d)", + grpc_call_error_detail_of(err), err); + } + } + return Qnil; +} + +static VALUE grpc_rb_server_start(VALUE self) { + grpc_rb_server *s = NULL; + Data_Get_Struct(self, grpc_rb_server, s); + if (s->wrapped == NULL) { + rb_raise(rb_eRuntimeError, "closed!"); + } else { + grpc_server_start(s->wrapped); + } + return Qnil; +} + +static VALUE grpc_rb_server_destroy(VALUE self) { + grpc_rb_server *s = NULL; + Data_Get_Struct(self, grpc_rb_server, s); + if (s->wrapped != NULL) { + grpc_server_shutdown(s->wrapped); + grpc_server_destroy(s->wrapped); + s->wrapped = NULL; + s->mark = Qnil; + } + return Qnil; +} + +static VALUE grpc_rb_server_add_http2_port(VALUE self, VALUE port) { + grpc_rb_server *s = NULL; + int added_ok = 0; + Data_Get_Struct(self, grpc_rb_server, s); + if (s->wrapped == NULL) { + rb_raise(rb_eRuntimeError, "closed!"); + } else { + added_ok = grpc_server_add_http2_port(s->wrapped, StringValueCStr(port)); + if (added_ok == 0) { + rb_raise(rb_eRuntimeError, "could not add port %s to server, not sure why", + StringValueCStr(port)); + } + } + return Qnil; +} + +void Init_google_rpc_server() { + rb_cServer = rb_define_class_under(rb_mGoogleRPC, "Server", rb_cObject); + + /* Allocates an object managed by the ruby runtime */ + rb_define_alloc_func(rb_cServer, grpc_rb_server_alloc); + + /* Provides a ruby constructor and support for dup/clone. */ + rb_define_method(rb_cServer, "initialize", grpc_rb_server_init, 2); + rb_define_method(rb_cServer, "initialize_copy", grpc_rb_server_init_copy, 1); + + /* Add the server methods. */ + rb_define_method(rb_cServer, "request_call", grpc_rb_server_request_call, 1); + rb_define_method(rb_cServer, "start", grpc_rb_server_start, 0); + rb_define_method(rb_cServer, "destroy", grpc_rb_server_destroy, 0); + rb_define_alias(rb_cServer, "close", "destroy"); + rb_define_method(rb_cServer, "add_http2_port", grpc_rb_server_add_http2_port, + 1); +} + +/* Gets the wrapped server from the ruby wrapper */ +grpc_server* grpc_rb_get_wrapped_server(VALUE v) { + grpc_rb_server *wrapper = NULL; + Data_Get_Struct(v, grpc_rb_server, wrapper); + return wrapper->wrapped; +} diff --git a/src/ruby/ext/grpc/rb_server.h b/src/ruby/ext/grpc/rb_server.h new file mode 100644 index 0000000000..4619203d60 --- /dev/null +++ b/src/ruby/ext/grpc/rb_server.h @@ -0,0 +1,50 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#ifndef GRPC_RB_BYTE_BUFFER_H_ +#define GRPC_RB_SERVER_H_ + +#include <ruby.h> +#include <grpc/grpc.h> + +/* rb_cServer is the Server class whose instances proxy + grpc_byte_buffer. */ +extern VALUE rb_cServer; + +/* Initializes the Server class. */ +void Init_google_rpc_server(); + +/* Gets the wrapped server from the ruby wrapper */ +grpc_server* grpc_rb_get_wrapped_server(VALUE v); + +#endif /* GRPC_RB_SERVER_H_ */ diff --git a/src/ruby/ext/grpc/rb_status.c b/src/ruby/ext/grpc/rb_status.c new file mode 100644 index 0000000000..747c47c556 --- /dev/null +++ b/src/ruby/ext/grpc/rb_status.c @@ -0,0 +1,243 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#include "rb_status.h" + +#include <ruby.h> +#include <string.h> + +#include <grpc/grpc.h> +#include <grpc/status.h> +#include "rb_grpc.h" + +/* grpc_rb_status wraps a grpc_status. It provides a peer ruby object, 'mark' + * to minimize copying when a status is created from ruby. */ +typedef struct grpc_rb_status { + /* Holder of ruby objects involved in constructing the status */ + VALUE mark; + /* The actual status */ + grpc_status *wrapped; +} grpc_rb_status; + +/* Destroys Status instances. */ +static void grpc_rb_status_free(void *p) { + grpc_rb_status *status = NULL; + if (p == NULL) { + return; + }; + status = (grpc_rb_status *)p; + + /* Delete the wrapped object if the mark object is Qnil, which indicates that + * no other object is the actual owner. */ + if (status->wrapped != NULL && status->mark == Qnil) { + status->mark = Qnil; + if (status->wrapped->details) { + xfree(status->wrapped->details); + } + xfree(status->wrapped); + } + + xfree(p); +} + +/* Protects the mark object from GC */ +static void grpc_rb_status_mark(void *p) { + grpc_rb_status *status = NULL; + if (p == NULL) { + return; + } + status = (grpc_rb_status *)p; + + /* If it's not already cleaned up, mark the mark object */ + if (status->mark != Qnil) { + rb_gc_mark(status->mark); + } +} + +/* Allocates Status instances. + + Provides safe initial defaults for the instance fields. */ +static VALUE grpc_rb_status_alloc(VALUE cls) { + grpc_rb_status *wrapper = ALLOC(grpc_rb_status); + wrapper->wrapped = NULL; + wrapper->mark = Qnil; + return Data_Wrap_Struct(cls, grpc_rb_status_mark, grpc_rb_status_free, + wrapper); +} + +/* The name of the attribute used on the mark object to hold the details. */ +static ID id_details; + +/* Initializes Status instances. */ +static VALUE grpc_rb_status_init(VALUE self, VALUE code, VALUE details) { + grpc_rb_status *wrapper = NULL; + grpc_status *status = NULL; + Data_Get_Struct(self, grpc_rb_status, wrapper); + + /* Use a direct pointer to the original detail value to avoid copying. Assume + * that details is null-terminated. */ + status = ALLOC(grpc_status); + status->details = StringValueCStr(details); + status->code = NUM2INT(code); + wrapper->wrapped = status; + + /* Create the mark and add the original details object to it. */ + wrapper->mark = rb_class_new_instance(0, NULL, rb_cObject); + rb_ivar_set(wrapper->mark, id_details, details); + return self; +} + +/* Clones Status instances. + + Gives Status a consistent implementation of Ruby's object copy/dup + protocol. */ +static VALUE grpc_rb_status_init_copy(VALUE copy, VALUE orig) { + grpc_rb_status *orig_status = NULL; + grpc_rb_status *copy_status = NULL; + + if (copy == orig) { + return copy; + } + + /* Raise an error if orig is not a Status object or a subclass. */ + if (TYPE(orig) != T_DATA || + RDATA(orig)->dfree != (RUBY_DATA_FUNC)grpc_rb_status_free) { + rb_raise(rb_eTypeError, "not a %s", rb_obj_classname(rb_cStatus)); + } + + Data_Get_Struct(orig, grpc_rb_status, orig_status); + Data_Get_Struct(copy, grpc_rb_status, copy_status); + MEMCPY(copy_status, orig_status, grpc_rb_status, 1); + return copy; +} + +/* Gets the Status code. */ +static VALUE grpc_rb_status_code(VALUE self) { + grpc_rb_status *status = NULL; + Data_Get_Struct(self, grpc_rb_status, status); + return INT2NUM(status->wrapped->code); +} + +/* Gets the Status details. */ +static VALUE grpc_rb_status_details(VALUE self) { + VALUE from_ruby; + grpc_rb_status *wrapper = NULL; + grpc_status *status; + + Data_Get_Struct(self, grpc_rb_status, wrapper); + if (wrapper->mark != Qnil) { + from_ruby = rb_ivar_get(wrapper->mark, id_details); + if (from_ruby != Qnil) { + return from_ruby; + } + } + + status = wrapper->wrapped; + if (status == NULL || status->details == NULL) { + return Qnil; + } + + return rb_str_new2(status->details); +} + +void Init_google_status_codes() { + /* Constants representing the status codes or grpc_status_code in status.h */ + VALUE rb_mStatusCodes = rb_define_module_under(rb_mGoogleRPC, "StatusCodes"); + rb_define_const(rb_mStatusCodes, "OK", INT2NUM(GRPC_STATUS_OK)); + rb_define_const(rb_mStatusCodes, "CANCELLED", INT2NUM(GRPC_STATUS_CANCELLED)); + rb_define_const(rb_mStatusCodes, "UNKNOWN", INT2NUM(GRPC_STATUS_UNKNOWN)); + rb_define_const(rb_mStatusCodes, "INVALID_ARGUMENT", + INT2NUM(GRPC_STATUS_INVALID_ARGUMENT)); + rb_define_const(rb_mStatusCodes, "DEADLINE_EXCEEDED", + INT2NUM(GRPC_STATUS_DEADLINE_EXCEEDED)); + rb_define_const(rb_mStatusCodes, "NOT_FOUND", INT2NUM(GRPC_STATUS_NOT_FOUND)); + rb_define_const(rb_mStatusCodes, "ALREADY_EXISTS", + INT2NUM(GRPC_STATUS_ALREADY_EXISTS)); + rb_define_const(rb_mStatusCodes, "PERMISSION_DENIED", + INT2NUM(GRPC_STATUS_PERMISSION_DENIED)); + rb_define_const(rb_mStatusCodes, "UNAUTHENTICATED", + INT2NUM(GRPC_STATUS_UNAUTHENTICATED)); + rb_define_const(rb_mStatusCodes, "RESOURCE_EXHAUSTED", + INT2NUM(GRPC_STATUS_RESOURCE_EXHAUSTED)); + rb_define_const(rb_mStatusCodes, "FAILED_PRECONDITION", + INT2NUM(GRPC_STATUS_FAILED_PRECONDITION)); + rb_define_const(rb_mStatusCodes, "ABORTED", INT2NUM(GRPC_STATUS_ABORTED)); + rb_define_const(rb_mStatusCodes, "OUT_OF_RANGE", + INT2NUM(GRPC_STATUS_OUT_OF_RANGE)); + rb_define_const(rb_mStatusCodes, "UNIMPLEMENTED", + INT2NUM(GRPC_STATUS_UNIMPLEMENTED)); + rb_define_const(rb_mStatusCodes, "INTERNAL", INT2NUM(GRPC_STATUS_INTERNAL)); + rb_define_const(rb_mStatusCodes, "UNAVAILABLE", + INT2NUM(GRPC_STATUS_UNAVAILABLE)); + rb_define_const(rb_mStatusCodes, "DATA_LOSS", INT2NUM(GRPC_STATUS_DATA_LOSS)); +} + +/* rb_cStatus is the Status class whose instances proxy grpc_status. */ +VALUE rb_cStatus = Qnil; + +/* Initializes the Status class. */ +void Init_google_rpc_status() { + rb_cStatus = rb_define_class_under(rb_mGoogleRPC, "Status", rb_cObject); + + /* Allocates an object whose memory is managed by the Ruby. */ + rb_define_alloc_func(rb_cStatus, grpc_rb_status_alloc); + + /* Provides a ruby constructor and support for dup/clone. */ + rb_define_method(rb_cStatus, "initialize", grpc_rb_status_init, 2); + rb_define_method(rb_cStatus, "initialize_copy", grpc_rb_status_init_copy, 1); + + /* Provides accessors for the code and details. */ + rb_define_method(rb_cStatus, "code", grpc_rb_status_code, 0); + rb_define_method(rb_cStatus, "details", grpc_rb_status_details, 0); + id_details = rb_intern("__details"); + Init_google_status_codes(); +} + +VALUE grpc_rb_status_create_with_mark(VALUE mark, grpc_status* s) { + grpc_rb_status *status = NULL; + if (s == NULL) { + return Qnil; + } + status = ALLOC(grpc_rb_status); + status->wrapped = s; + status->mark = mark; + return Data_Wrap_Struct(rb_cStatus, grpc_rb_status_mark, grpc_rb_status_free, + status); +} + +/* Gets the wrapped status from the ruby wrapper */ +grpc_status* grpc_rb_get_wrapped_status(VALUE v) { + grpc_rb_status *wrapper = NULL; + Data_Get_Struct(v, grpc_rb_status, wrapper); + return wrapper->wrapped; +} diff --git a/src/ruby/ext/grpc/rb_status.h b/src/ruby/ext/grpc/rb_status.h new file mode 100644 index 0000000000..ceb6f9f81e --- /dev/null +++ b/src/ruby/ext/grpc/rb_status.h @@ -0,0 +1,53 @@ +/* + * + * Copyright 2014, 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. + * + */ + +#ifndef GRPC_RB_STATUS_H_ +#define GRPC_RB_STATUS_H_ + +#include <grpc/grpc.h> +#include <ruby.h> + +/* rb_cStatus is the Status class whose instances proxy grpc_status. */ +extern VALUE rb_cStatus; + +/* grpc_rb_status_create_with_mark creates a grpc_rb_status with a ruby mark + * object that will be kept alive while the status is alive. */ +extern VALUE grpc_rb_status_create_with_mark(VALUE mark, grpc_status *s); + +/* Gets the wrapped status from the ruby wrapper object */ +grpc_status* grpc_rb_get_wrapped_status(VALUE v); + +/* Initializes the Status class. */ +void Init_google_rpc_status(); + +#endif /* GRPC_RB_STATUS_H_ */ diff --git a/src/ruby/grpc.gemspec b/src/ruby/grpc.gemspec new file mode 100755 index 0000000000..c63c80dd58 --- /dev/null +++ b/src/ruby/grpc.gemspec @@ -0,0 +1,30 @@ +# encoding: utf-8 +$:.push File.expand_path("../lib", __FILE__) +require 'grpc/version' + +Gem::Specification.new do |s| + s.name = "grpc" + s.version = Google::RPC::VERSION + s.authors = ["One Platform Team"] + s.email = "stubby-team@google.com" + s.homepage = "http://go/grpc" + s.summary = 'Google RPC system in Ruby' + s.description = 'Send RPCs from Ruby' + + s.files = `git ls-files`.split("\n") + s.test_files = `git ls-files -- spec/*`.split("\n") + s.executables = `git ls-files -- examples/*.rb`.split("\n").map{ |f| File.basename(f) } + s.require_paths = ['lib' ] + s.platform = Gem::Platform::RUBY + + s.add_dependency 'xray' + s.add_dependency 'logging', '~> 1.8' + s.add_dependency 'beefcake', '~> 1.1' + + s.add_development_dependency "bundler", "~> 1.7" + s.add_development_dependency "rake", "~> 10.0" + s.add_development_dependency 'rake-compiler', '~> 0' + s.add_development_dependency 'rspec', "~> 3.0" + + s.extensions = %w[ext/grpc/extconf.rb] +end diff --git a/src/ruby/lib/grpc.rb b/src/ruby/lib/grpc.rb new file mode 100644 index 0000000000..60a3b96527 --- /dev/null +++ b/src/ruby/lib/grpc.rb @@ -0,0 +1,38 @@ +# Copyright 2014, 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/event' +require 'grpc/errors' +require 'grpc/grpc' +require 'grpc/logconfig' +require 'grpc/time_consts' +require 'grpc/version' + +# alias GRPC +GRPC = Google::RPC diff --git a/src/ruby/lib/grpc/errors.rb b/src/ruby/lib/grpc/errors.rb new file mode 100644 index 0000000000..d14e69c65a --- /dev/null +++ b/src/ruby/lib/grpc/errors.rb @@ -0,0 +1,68 @@ +# Copyright 2014, 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' + +module Google + + module RPC + + # OutOfTime is an exception class that indicates that an RPC exceeded its + # deadline. + OutOfTime = Class.new(StandardError) + + # BadStatus is an exception class that indicates that an error occurred at + # 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. + class BadStatus < StandardError + + attr_reader :code, :details + + # @param code [Numeric] the status code + # @param details [String] the details of the exception + def initialize(code, details='unknown cause') + super("#{code}:#{details}") + @code = code + @details = details + end + + # Converts the exception to a GRPC::Status for use in the networking + # wrapper layer. + # + # @return [Status] with the same code and details + def to_status + Status.new(code, details) + end + + end + + end + +end diff --git a/src/ruby/lib/grpc/event.rb b/src/ruby/lib/grpc/event.rb new file mode 100644 index 0000000000..c108cd4c1e --- /dev/null +++ b/src/ruby/lib/grpc/event.rb @@ -0,0 +1,38 @@ +# Copyright 2014, 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. + +module Google + module RPC + class Event # Add an inspect method to C-defined Event class. + def inspect + '<%s: type:%s, tag:%s result:%s>' % [self.class, type, tag, result] + end + end + end +end diff --git a/src/ruby/lib/grpc/generic/active_call.rb b/src/ruby/lib/grpc/generic/active_call.rb new file mode 100644 index 0000000000..d987b3966f --- /dev/null +++ b/src/ruby/lib/grpc/generic/active_call.rb @@ -0,0 +1,485 @@ +# Copyright 2014, 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 'forwardable' +require 'grpc' +require 'grpc/generic/bidi_call' + +def assert_event_type(got, want) + raise 'Unexpected rpc event: got %s, want %s' % [got, want] unless got == want +end + +module GRPC + + # The ActiveCall class provides simple methods for sending marshallable + # data to a call + class ActiveCall + include CompletionType + include StatusCodes + attr_reader(:deadline) + + # client_start_invoke begins a client invocation. + # + # Flow Control note: this blocks until flow control accepts that client + # request can go ahead. + # + # deadline is the absolute deadline for the call. + # + # @param call [Call] a call on which to start and invocation + # @param q [CompletionQueue] used to wait for INVOKE_ACCEPTED + # @param deadline [Fixnum,TimeSpec] the deadline for INVOKE_ACCEPTED + def self.client_start_invoke(call, q, deadline) + raise ArgumentError.new('not a call') unless call.is_a?Call + if !q.is_a?CompletionQueue + raise ArgumentError.new('not a CompletionQueue') + end + invoke_accepted, client_metadata_read = Object.new, Object.new + finished_tag = Object.new + call.start_invoke(q, invoke_accepted, client_metadata_read, finished_tag) + # wait for the invocation to be accepted + ev = q.pluck(invoke_accepted, TimeConsts::INFINITE_FUTURE) + raise OutOfTime if ev.nil? + finished_tag + end + + # Creates an ActiveCall. + # + # ActiveCall should only be created after a call is accepted. That means + # different things on a client and a server. On the client, the call is + # accepted after call.start_invoke followed by receipt of the + # corresponding INVOKE_ACCEPTED. on the server, this is after + # call.accept. + # + # #initialize cannot determine if the call is accepted or not; so if a + # call that's not accepted is used here, the error won't be visible until + # the ActiveCall methods are called. + # + # deadline is the absolute deadline for the call. + # + # @param call [Call] the call used by the ActiveCall + # @param q [CompletionQueue] the completion queue used to accept + # the call + # @param marshal [Function] f(obj)->string that marshal requests + # @param unmarshal [Function] f(string)->obj that unmarshals responses + # @param deadline [Fixnum] the deadline for the call to complete + # @param finished_tag [Object] the object used as the call's finish tag, + # if the call has begun + # @param started [true|false] (default true) indicates if the call has begun + def initialize(call, q, marshal, unmarshal, deadline, finished_tag: nil, + started: true) + raise ArgumentError.new('not a call') unless call.is_a?Call + if !q.is_a?CompletionQueue + raise ArgumentError.new('not a CompletionQueue') + end + @call = call + @cq = q + @deadline = deadline + @finished_tag = finished_tag + @marshal = marshal + @started = started + @unmarshal = unmarshal + end + + # Obtains the status of the call. + # + # this value is nil until the call completes + # @return this call's status + def status + @call.status + end + + # Obtains the metadata of the call. + # + # At the start of the call this will be nil. During the call this gets + # some values as soon as the other end of the connection acknowledges the + # request. + # + # @return this calls's metadata + def metadata + @call.metadata + end + + # Cancels the call. + # + # Cancels the call. The call does not return any result, but once this it + # has been called, the call should eventually terminate. Due to potential + # races between the execution of the cancel and the in-flight request, the + # result of the call after calling #cancel is indeterminate: + # + # - the call may terminate with a BadStatus exception, with code=CANCELLED + # - the call may terminate with OK Status, and return a response + # - the call may terminate with a different BadStatus exception if that was + # happening + def cancel + @call.cancel + end + + # indicates if the call is shutdown + def shutdown + @shutdown ||= false + end + + # indicates if the call is cancelled. + def cancelled + @cancelled ||= false + end + + # multi_req_view provides a restricted view of this ActiveCall for use + # in a server client-streaming handler. + def multi_req_view + MultiReqView.new(self) + end + + # single_req_view provides a restricted view of this ActiveCall for use in + # a server request-response handler. + def single_req_view + SingleReqView.new(self) + end + + # operation provides a restricted view of this ActiveCall for use as + # a Operation. + def operation + Operation.new(self) + end + + # writes_done indicates that all writes are completed. + # + # It blocks until the remote endpoint acknowledges by sending a FINISHED + # event, unless assert_finished is set to false. Any calls to + # #remote_send after this call will fail. + # + # @param assert_finished [true, false] when true(default), waits for + # FINISHED. + def writes_done(assert_finished=true) + @call.writes_done(self) + ev = @cq.pluck(self, TimeConsts::INFINITE_FUTURE) + assert_event_type(ev.type, FINISH_ACCEPTED) + logger.debug("Writes done: waiting for finish? #{assert_finished}") + if assert_finished + ev = @cq.pluck(@finished_tag, TimeConsts::INFINITE_FUTURE) + raise "unexpected event: #{ev.inspect}" if ev.nil? + return @call.status + end + end + + # finished waits until the call is completed. + # + # It blocks until the remote endpoint acknowledges by sending a FINISHED + # event. + def finished + ev = @cq.pluck(@finished_tag, TimeConsts::INFINITE_FUTURE) + raise "unexpected event: #{ev.inspect}" unless ev.type == FINISHED + if ev.result.code != StatusCodes::OK + raise BadStatus.new(ev.result.code, ev.result.details) + end + res = ev.result + + # NOTE(temiola): This is necessary to allow the C call struct wrapped + # within the active_call to be GCed; this is necessary so that other + # C-level destructors get called in the required order. + ev = nil # allow the event to be GCed + res + end + + # remote_send sends a request to the remote endpoint. + # + # It blocks until the remote endpoint acknowledges by sending a + # WRITE_ACCEPTED. req can be marshalled already. + # + # @param req [Object, String] the object to send or it's marshal form. + # @param marshalled [false, true] indicates if the object is already + # marshalled. + def remote_send(req, marshalled=false) + assert_queue_is_ready + logger.debug("sending payload #{req.inspect}, marshalled? #{marshalled}") + if marshalled + payload = req + else + payload = @marshal.call(req) + end + @call.start_write(ByteBuffer.new(payload), self) + + # call queue#pluck, and wait for WRITE_ACCEPTED, so as not to return + # until the flow control allows another send on this call. + ev = @cq.pluck(self, TimeConsts::INFINITE_FUTURE) + assert_event_type(ev.type, WRITE_ACCEPTED) + ev = nil + end + + # send_status sends a status to the remote endpoint + # + # @param code [int] the status code to send + # @param details [String] details + # @param assert_finished [true, false] when true(default), waits for + # FINISHED. + def send_status(code=OK, details='', assert_finished=false) + assert_queue_is_ready + @call.start_write_status(Status.new(code, details), self) + ev = @cq.pluck(self, TimeConsts::INFINITE_FUTURE) + assert_event_type(ev.type, FINISH_ACCEPTED) + logger.debug("Status sent: #{code}:'#{details}'") + if assert_finished + return finished + end + nil + end + + # remote_read reads a response from the remote endpoint. + # + # It blocks until the remote endpoint sends a READ or FINISHED event. On + # a READ, it returns the response after unmarshalling it. On + # FINISHED, it returns nil if the status is OK, otherwise raising BadStatus + def remote_read + @call.start_read(self) + ev = @cq.pluck(self, TimeConsts::INFINITE_FUTURE) + assert_event_type(ev.type, READ) + logger.debug("received req: #{ev.result.inspect}") + if !ev.result.nil? + logger.debug("received req.to_s: #{ev.result.to_s}") + res = @unmarshal.call(ev.result.to_s) + logger.debug("received_req (unmarshalled): #{res.inspect}") + return res + end + logger.debug('found nil; the final response has been sent') + nil + end + + # each_remote_read passes each response to the given block or returns an + # enumerator the responses if no block is given. + # + # == Enumerator == + # + # * #next blocks until the remote endpoint sends a READ or FINISHED + # * for each read, enumerator#next yields the response + # * on status + # * if it's is OK, enumerator#next raises StopException + # * if is not OK, enumerator#next raises RuntimeException + # + # == Block == + # + # * if provided it is executed for each response + # * the call blocks until no more responses are provided + # + # @return [Enumerator] if no block was given + def each_remote_read + return enum_for(:each_remote_read) if !block_given? + loop do + resp = remote_read() + break if resp.is_a?Status # this will be an OK status, bad statii raise + break if resp.nil? # the last response was received + yield resp + end + end + + # each_remote_read_then_finish passes each response to the given block or + # returns an enumerator of the responses if no block is given. + # + # It is like each_remote_read, but it blocks on finishing on detecting + # the final message. + # + # == Enumerator == + # + # * #next blocks until the remote endpoint sends a READ or FINISHED + # * for each read, enumerator#next yields the response + # * on status + # * if it's is OK, enumerator#next raises StopException + # * if is not OK, enumerator#next raises RuntimeException + # + # == Block == + # + # * if provided it is executed for each response + # * the call blocks until no more responses are provided + # + # @return [Enumerator] if no block was given + def each_remote_read_then_finish + return enum_for(:each_remote_read_then_finish) if !block_given? + loop do + resp = remote_read + break if resp.is_a?Status # this will be an OK status, bad statii raise + if resp.nil? # the last response was received, but not finished yet + finished + break + end + yield resp + end + end + + # request_response sends a request to a GRPC server, and returns the + # response. + # @param req [Object] the request sent to the server + # @return [Object] the response received from the server + def request_response(req) + start_call unless @started + remote_send(req) + writes_done(false) + response = remote_read + if !response.is_a?(Status) # finish if status not yet received + finished + end + response + end + + # client_streamer sends a stream of requests to a GRPC server, and + # returns a single response. + # + # requests provides an 'iterable' of Requests. I.e. it follows Ruby's + # #each enumeration protocol. In the simplest case, requests will be an + # array of marshallable objects; in typical case it will be an Enumerable + # that allows dynamic construction of the marshallable objects. + # + # @param requests [Object] an Enumerable of requests to send + # @return [Object] the response received from the server + def client_streamer(requests) + start_call unless @started + requests.each { |r| remote_send(r) } + writes_done(false) + response = remote_read + if !response.is_a?(Status) # finish if status not yet received + finished + end + response + end + + # server_streamer sends one request to the GRPC server, which yields a + # stream of responses. + # + # responses provides an enumerator over the streamed responses, i.e. it + # follows Ruby's #each iteration protocol. The enumerator blocks while + # waiting for each response, stops when the server signals that no + # further responses will be supplied. If the implicit block is provided, + # it is executed with each response as the argument and no result is + # returned. + # + # @param req [Object] the request sent to the server + # @return [Enumerator|nil] a response Enumerator + def server_streamer(req) + start_call unless @started + remote_send(req) + writes_done(false) + replies = enum_for(:each_remote_read_then_finish) + return replies if !block_given? + replies.each { |r| yield r } + end + + # bidi_streamer sends a stream of requests to the GRPC server, and yields + # a stream of responses. + # + # This method takes an Enumerable of requests, and returns and enumerable + # of responses. + # + # == requests == + # + # requests provides an 'iterable' of Requests. I.e. it follows Ruby's #each + # enumeration protocol. In the simplest case, requests will be an array of + # marshallable objects; in typical case it will be an Enumerable that + # allows dynamic construction of the marshallable objects. + # + # == responses == + # + # This is an enumerator of responses. I.e, its #next method blocks + # waiting for the next response. Also, if at any point the block needs + # to consume all the remaining responses, this can be done using #each or + # #collect. Calling #each or #collect should only be done if + # the_call#writes_done has been called, otherwise the block will loop + # forever. + # + # @param requests [Object] an Enumerable of requests to send + # @return [Enumerator, nil] a response Enumerator + def bidi_streamer(requests, &blk) + start_call unless @started + bd = BidiCall.new(@call, @cq, @marshal, @unmarshal, @deadline, + @finished_tag) + bd.run_on_client(requests, &blk) + end + + # run_server_bidi orchestrates a BiDi stream processing on a server. + # + # N.B. gen_each_reply is a func(Enumerable<Requests>) + # + # It takes an enumerable of requests as an arg, in case there is a + # relationship between the stream of requests and the stream of replies. + # + # This does not mean that must necessarily be one. E.g, the replies + # produced by gen_each_reply could ignore the received_msgs + # + # @param gen_each_reply [Proc] generates the BiDi stream replies + def run_server_bidi(gen_each_reply) + bd = BidiCall.new(@call, @cq, @marshal, @unmarshal, @deadline, + @finished_tag) + bd.run_on_server(gen_each_reply) + end + + private + + def start_call + @finished_tag = ActiveCall.client_start_invoke(@call, @cq, @deadline) + @started = true + end + + def self.view_class(*visible_methods) + Class.new do + extend ::Forwardable + def_delegators :@wrapped, *visible_methods + + # @param wrapped [ActiveCall] the call whose methods are shielded + def initialize(wrapped) + @wrapped = wrapped + end + end + end + + # SingleReqView limits access to an ActiveCall's methods for use in server + # handlers that receive just one request. + SingleReqView = view_class(:cancelled, :deadline) + + # MultiReqView limits access to an ActiveCall's methods for use in + # server client_streamer handlers. + MultiReqView = view_class(:cancelled, :deadline, :each_queued_msg, + :each_remote_read) + + # Operation limits access to an ActiveCall's methods for use as + # a Operation on the client. + Operation = view_class(:cancel, :cancelled, :deadline, :execute, :metadata, + :status) + + # confirms that no events are enqueued, and that the queue is not + # shutdown. + def assert_queue_is_ready + begin + ev = @cq.pluck(self, TimeConsts::ZERO) + raise "unexpected event #{ev.inspect}" unless ev.nil? + rescue OutOfTime + # expected, nothing should be on the queue and the deadline was ZERO, + # except things using another tag + end + end + + end + +end diff --git a/src/ruby/lib/grpc/generic/bidi_call.rb b/src/ruby/lib/grpc/generic/bidi_call.rb new file mode 100644 index 0000000000..a3566e1118 --- /dev/null +++ b/src/ruby/lib/grpc/generic/bidi_call.rb @@ -0,0 +1,320 @@ +# Copyright 2014, 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 'forwardable' +require 'grpc' + +module GRPC + + # The BiDiCall class orchestrates exection of a BiDi stream on a client or + # server. + class BidiCall + include CompletionType + include StatusCodes + + # Creates a BidiCall. + # + # BidiCall should only be created after a call is accepted. That means + # different things on a client and a server. On the client, the call is + # accepted after call.start_invoke followed by receipt of the corresponding + # INVOKE_ACCEPTED. On the server, this is after call.accept. + # + # #initialize cannot determine if the call is accepted or not; so if a + # call that's not accepted is used here, the error won't be visible until + # the BidiCall#run is called. + # + # deadline is the absolute deadline for the call. + # + # @param call [Call] the call used by the ActiveCall + # @param q [CompletionQueue] the completion queue used to accept + # the call + # @param marshal [Function] f(obj)->string that marshal requests + # @param unmarshal [Function] f(string)->obj that unmarshals responses + # @param deadline [Fixnum] the deadline for the call to complete + # @param finished_tag [Object] the object used as the call's finish tag, + def initialize(call, q, marshal, unmarshal, deadline, finished_tag) + raise ArgumentError.new('not a call') unless call.is_a?Call + if !q.is_a?CompletionQueue + raise ArgumentError.new('not a CompletionQueue') + end + @call = call + @cq = q + @deadline = deadline + @finished_tag = finished_tag + @marshal = marshal + @readq = Queue.new + @unmarshal = unmarshal + @writeq = Queue.new + end + + # Begins orchestration of the Bidi stream for a client sending requests. + # + # The method either returns an Enumerator of the responses, or accepts a + # block that can be invoked with each response. + # + # @param requests the Enumerable of requests to send + # @return an Enumerator of requests to yield + def run_on_client(requests, &blk) + enq_th = enqueue_for_sending(requests) + loop_th = start_read_write_loop + replies = each_queued_msg + return replies if blk.nil? + replies.each { |r| blk.call(r) } + end + + # Begins orchestration of the Bidi stream for a server generating replies. + # + # N.B. gen_each_reply is a func(Enumerable<Requests>) + # + # It takes an enumerable of requests as an arg, in case there is a + # relationship between the stream of requests and the stream of replies. + # + # This does not mean that must necessarily be one. E.g, the replies + # produced by gen_each_reply could ignore the received_msgs + # + # @param gen_each_reply [Proc] generates the BiDi stream replies. + def run_on_server(gen_each_reply) + replys = gen_each_reply.call(each_queued_msg) + enq_th = enqueue_for_sending(replys) + loop_th = start_read_write_loop(is_client:false) + loop_th.join + enq_th.join + end + + private + + END_OF_READS = :end_of_reads + END_OF_WRITES = :end_of_writes + + # each_queued_msg yields each message on this instances readq + # + # - messages are added to the readq by #read_write_loop + # - iteration ends when the instance itself is added + def each_queued_msg + return enum_for(:each_queued_msg) if !block_given? + count = 0 + loop do + logger.debug("each_queued_msg: msg##{count}") + count += 1 + req = @readq.pop + throw req if req.is_a?StandardError + break if req.equal?(END_OF_READS) + yield req + end + end + + # during bidi-streaming, read the requests to send from a separate thread + # read so that read_write_loop does not block waiting for requests to read. + def enqueue_for_sending(requests) + Thread.new do # TODO(temiola) run on a thread pool + begin + requests.each { |req| @writeq.push(req)} + @writeq.push(END_OF_WRITES) + rescue StandardError => e + logger.warn('enqueue_for_sending failed') + logger.warn(e) + @writeq.push(e) + end + end + end + + # starts the read_write loop + def start_read_write_loop(is_client: true) + t = Thread.new do + begin + read_write_loop(is_client: is_client) + rescue StandardError => e + logger.warn('start_read_write_loop failed') + logger.warn(e) + @readq.push(e) # let each_queued_msg terminate with the error + end + end + t.priority = 3 # hint that read_write_loop threads should be favoured + t + end + + # drain_writeq removes any outstanding message on the writeq + def drain_writeq + while @writeq.size != 0 do + discarded = @writeq.pop + logger.warn("discarding: queued write: #{discarded}") + end + end + + # sends the next queued write + # + # The return value is an array with three values + # - the first indicates if a writes was started + # - the second that all writes are done + # - the third indicates that are still writes to perform but they are lates + # + # If value pulled from writeq is a StandardError, the producer hit an error + # that should be raised. + # + # @param is_client [Boolean] when true, writes_done will be called when the + # last entry is read from the writeq + # + # @return [in_write, done_writing] + def next_queued_write(is_client: true) + in_write, done_writing = false, false + + # send the next item on the queue if there is any + return [in_write, done_writing] if @writeq.size == 0 + + # TODO(temiola): provide a queue class that returns nil after a timeout + req = @writeq.pop + if req.equal?(END_OF_WRITES) + logger.debug('done writing after last req') + if is_client + logger.debug('sent writes_done after last req') + @call.writes_done(self) + end + done_writing = true + return [in_write, done_writing] + elsif req.is_a?(StandardError) # used to signal an error in the producer + logger.debug('done writing due to a failure') + if is_client + logger.debug('sent writes_done after a failure') + @call.writes_done(self) + end + logger.warn(req) + done_writing = true + return [in_write, done_writing] + end + + # send the payload + payload = @marshal.call(req) + @call.start_write(ByteBuffer.new(payload), self) + logger.debug("rwloop: sent payload #{req.inspect}") + in_write = true + return [in_write, done_writing] + end + + # read_write_loop takes items off the write_queue and sends them, reads + # msgs and adds them to the read queue. + def read_write_loop(is_client: true) + done_reading, done_writing = false, false + finished, pre_finished = false, false + in_write, writes_late = false, false + count = 0 + + # queue the initial read before beginning the loop + @call.start_read(self) + + loop do + # whether or not there are outstanding writes is independent of the + # next event from the completion queue. The producer may queue the + # first msg at any time, e.g, after the loop is started running. So, + # it's essential for the loop to check for possible writes here, in + # order to correctly begin writing. + if !in_write and !done_writing + in_write, done_writing = next_queued_write(is_client: is_client) + end + logger.debug("rwloop is_client? #{is_client}") + logger.debug("rwloop count: #{count}") + count += 1 + + # Loop control: + # + # - Break when no further events need to read. On clients, this means + # waiting for a FINISHED, servers just need to wait for all reads and + # writes to be done. + # + # - Also, don't read an event unless there's one expected. This can + # happen, e.g, when all the reads are done, there are no writes + # available, but writing is not complete. + logger.debug("done_reading? #{done_reading}") + logger.debug("done_writing? #{done_writing}") + logger.debug("finish accepted? #{pre_finished}") + logger.debug("finished? #{finished}") + logger.debug("in write? #{in_write}") + if is_client + break if done_writing and done_reading and pre_finished and finished + logger.debug('waiting for another event') + if in_write or !done_reading or !pre_finished + logger.debug('waiting for another event') + ev = @cq.pluck(self, TimeConsts::INFINITE_FUTURE) + elsif !finished + logger.debug('waiting for another event') + ev = @cq.pluck(@finish_tag, TimeConsts::INFINITE_FUTURE) + else + next # no events to wait on, but not done writing + end + else + break if done_writing and done_reading + if in_write or !done_reading + logger.debug('waiting for another event') + ev = @cq.pluck(self, TimeConsts::INFINITE_FUTURE) + else + next # no events to wait on, but not done writing + end + end + + # handle the next event. + if ev.nil? + drain_writeq + raise OutOfTime + elsif ev.type == WRITE_ACCEPTED + logger.debug('write accepted!') + in_write = false + next + elsif ev.type == FINISH_ACCEPTED + logger.debug('finish accepted!') + pre_finished = true + next + elsif ev.type == READ + logger.debug("received req: #{ev.result.inspect}") + if ev.result.nil? + logger.debug('done reading!') + done_reading = true + @readq.push(END_OF_READS) + else + # push the latest read onto the queue and continue reading + logger.debug("received req.to_s: #{ev.result.to_s}") + res = @unmarshal.call(ev.result.to_s) + logger.debug("req (unmarshalled): #{res.inspect}") + @readq.push(res) + if !done_reading + @call.start_read(self) + end + end + elsif ev.type == FINISHED + logger.debug("finished! with status:#{ev.result.inspect}") + finished = true + ev.call.status = ev.result + if ev.result.code != OK + raise BadStatus.new(ev.result.code, ev.result.details) + end + end + end + end + + end + +end diff --git a/src/ruby/lib/grpc/generic/client_stub.rb b/src/ruby/lib/grpc/generic/client_stub.rb new file mode 100644 index 0000000000..fee31e3353 --- /dev/null +++ b/src/ruby/lib/grpc/generic/client_stub.rb @@ -0,0 +1,358 @@ +# Copyright 2014, 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' +require 'grpc/generic/active_call' +require 'xray/thread_dump_signal_handler' + +module GRPC + + # ClientStub represents an endpoint used to send requests to GRPC servers. + class ClientStub + include StatusCodes + + # Default deadline is 5 seconds. + DEFAULT_DEADLINE = 5 + + # Creates a new ClientStub. + # + # Minimally, a stub is created with the just the host of the gRPC service + # it wishes to access, e.g., + # + # my_stub = ClientStub.new(example.host.com:50505) + # + # Any arbitrary keyword arguments are treated as channel arguments used to + # configure the RPC connection to the host. + # + # There are two specific keywords are that not used to configure the + # channel: + # + # - :channel_override + # when present, this must be a pre-created GRPC::Channel. If it's present + # the host and arbitrary keyword arg areignored, and the RPC connection uses + # this channel. + # + # - :deadline + # when present, this is the default deadline used for calls + # + # @param host [String] the host the stub connects to + # @param q [TaggedCompletionQueue] used to wait for events + # @param channel_override [Channel] a pre-created channel + # @param deadline [Number] the default deadline to use in requests + # @param kw [KeywordArgs] the channel arguments + def initialize(host, q, + channel_override:nil, + deadline:DEFAULT_DEADLINE, + **kw) + if !q.is_a?CompletionQueue + raise ArgumentError.new('not a CompletionQueue') + end + @host = host + if !channel_override.nil? + ch = channel_override + raise ArgumentError.new('not a Channel') unless ch.is_a?(Channel) + else + ch = Channel.new(host, **kw) + end + + @deadline = deadline + @ch = ch + @queue = q + end + + # request_response sends a request to a GRPC server, and returns the + # response. + # + # == Flow Control == + # This is a blocking call. + # + # * it does not return until a response is received. + # + # * the requests is sent only when GRPC core's flow control allows it to + # be sent. + # + # == Errors == + # An RuntimeError is raised if + # + # * the server responds with a non-OK status + # + # * the deadline is exceeded + # + # == Return Value == + # + # If return_op is false, the call returns the response + # + # If return_op is true, the call returns an Operation, calling execute + # on the Operation returns the response. + # + # @param method [String] the RPC method to call on the GRPC server + # @param req [Object] the request sent to the server + # @param marshal [Function] f(obj)->string that marshals requests + # @param unmarshal [Function] f(string)->obj that unmarshals responses + # @param deadline [Numeric] (optional) the max completion time in seconds + # @param return_op [true|false] (default false) return an Operation if true + # @return [Object] the response received from the server + def request_response(method, req, marshal, unmarshal, deadline=nil, + return_op:false) + c = new_active_call(method, marshal, unmarshal, deadline || @deadline) + return c.request_response(req) unless return_op + + # return the operation view of the active_call; define #execute as a + # new method for this instance that invokes #request_response. + op = c.operation + op.define_singleton_method(:execute) do + c.request_response(req) + end + op + end + + # client_streamer sends a stream of requests to a GRPC server, and + # returns a single response. + # + # requests provides an 'iterable' of Requests. I.e. it follows Ruby's + # #each enumeration protocol. In the simplest case, requests will be an + # array of marshallable objects; in typical case it will be an Enumerable + # that allows dynamic construction of the marshallable objects. + # + # == Flow Control == + # This is a blocking call. + # + # * it does not return until a response is received. + # + # * each requests is sent only when GRPC core's flow control allows it to + # be sent. + # + # == Errors == + # An RuntimeError is raised if + # + # * the server responds with a non-OK status + # + # * the deadline is exceeded + # + # == Return Value == + # + # If return_op is false, the call consumes the requests and returns + # the response. + # + # If return_op is true, the call returns the response. + # + # @param method [String] the RPC method to call on the GRPC server + # @param requests [Object] an Enumerable of requests to send + # @param marshal [Function] f(obj)->string that marshals requests + # @param unmarshal [Function] f(string)->obj that unmarshals responses + # @param deadline [Numeric] the max completion time in seconds + # @param return_op [true|false] (default false) return an Operation if true + # @return [Object|Operation] the response received from the server + def client_streamer(method, requests, marshal, unmarshal, deadline=nil, + return_op:false) + c = new_active_call(method, marshal, unmarshal, deadline || @deadline) + return c.client_streamer(requests) unless return_op + + # return the operation view of the active_call; define #execute as a + # new method for this instance that invokes #client_streamer. + op = c.operation + op.define_singleton_method(:execute) do + c.client_streamer(requests) + end + op + end + + # server_streamer sends one request to the GRPC server, which yields a + # stream of responses. + # + # responses provides an enumerator over the streamed responses, i.e. it + # follows Ruby's #each iteration protocol. The enumerator blocks while + # waiting for each response, stops when the server signals that no + # further responses will be supplied. If the implicit block is provided, + # it is executed with each response as the argument and no result is + # returned. + # + # == Flow Control == + # This is a blocking call. + # + # * the request is sent only when GRPC core's flow control allows it to + # be sent. + # + # * the request will not complete until the server sends the final response + # followed by a status message. + # + # == Errors == + # An RuntimeError is raised if + # + # * the server responds with a non-OK status when any response is + # * retrieved + # + # * the deadline is exceeded + # + # == Return Value == + # + # if the return_op is false, the return value is an Enumerator of the + # results, unless a block is provided, in which case the block is + # executed with each response. + # + # if return_op is true, the function returns an Operation whose #execute + # method runs server streamer call. Again, Operation#execute either + # calls the given block with each response or returns an Enumerator of the + # responses. + # + # @param method [String] the RPC method to call on the GRPC server + # @param req [Object] the request sent to the server + # @param marshal [Function] f(obj)->string that marshals requests + # @param unmarshal [Function] f(string)->obj that unmarshals responses + # @param deadline [Numeric] the max completion time in seconds + # @param return_op [true|false] (default false) return an Operation if true + # @param blk [Block] when provided, is executed for each response + # @return [Enumerator|Operation|nil] as discussed above + def server_streamer(method, req, marshal, unmarshal, deadline=nil, + return_op:false, &blk) + c = new_active_call(method, marshal, unmarshal, deadline || @deadline) + return c.server_streamer(req, &blk) unless return_op + + # return the operation view of the active_call; define #execute + # as a new method for this instance that invokes #server_streamer + op = c.operation + op.define_singleton_method(:execute) do + c.server_streamer(req, &blk) + end + op + end + + # bidi_streamer sends a stream of requests to the GRPC server, and yields + # a stream of responses. + # + # This method takes an Enumerable of requests, and returns and enumerable + # of responses. + # + # == requests == + # + # requests provides an 'iterable' of Requests. I.e. it follows Ruby's #each + # enumeration protocol. In the simplest case, requests will be an array of + # marshallable objects; in typical case it will be an Enumerable that + # allows dynamic construction of the marshallable objects. + # + # == responses == + # + # This is an enumerator of responses. I.e, its #next method blocks + # waiting for the next response. Also, if at any point the block needs + # to consume all the remaining responses, this can be done using #each or + # #collect. Calling #each or #collect should only be done if + # the_call#writes_done has been called, otherwise the block will loop + # forever. + # + # == Flow Control == + # This is a blocking call. + # + # * the call completes when the next call to provided block returns + # * [False] + # + # * the execution block parameters are two objects for sending and + # receiving responses, each of which blocks waiting for flow control. + # E.g, calles to bidi_call#remote_send will wait until flow control + # allows another write before returning; and obviously calls to + # responses#next block until the next response is available. + # + # == Termination == + # + # As well as sending and receiving messages, the block passed to the + # function is also responsible for: + # + # * calling bidi_call#writes_done to indicate no further reqs will be + # sent. + # + # * returning false if once the bidi stream is functionally completed. + # + # Note that response#next will indicate that there are no further + # responses by throwing StopIteration, but can only happen either + # if bidi_call#writes_done is called. + # + # To terminate the RPC correctly the block: + # + # * must call bidi#writes_done and then + # + # * either return false as soon as there is no need for other responses + # + # * loop on responses#next until no further responses are available + # + # == Errors == + # An RuntimeError is raised if + # + # * the server responds with a non-OK status when any response is + # * retrieved + # + # * the deadline is exceeded + # + # == Return Value == + # + # if the return_op is false, the return value is an Enumerator of the + # results, unless a block is provided, in which case the block is + # executed with each response. + # + # if return_op is true, the function returns an Operation whose #execute + # method runs the Bidi call. Again, Operation#execute either calls a + # given block with each response or returns an Enumerator of the responses. + # + # @param method [String] the RPC method to call on the GRPC server + # @param requests [Object] an Enumerable of requests to send + # @param marshal [Function] f(obj)->string that marshals requests + # @param unmarshal [Function] f(string)->obj that unmarshals responses + # @param deadline [Numeric] (optional) the max completion time in seconds + # @param blk [Block] when provided, is executed for each response + # @param return_op [true|false] (default false) return an Operation if true + # @return [Enumerator|nil|Operation] as discussed above + def bidi_streamer(method, requests, marshal, unmarshal, deadline=nil, + return_op:false, &blk) + c = new_active_call(method, marshal, unmarshal, deadline || @deadline) + return c.bidi_streamer(requests, &blk) unless return_op + + # return the operation view of the active_call; define #execute + # as a new method for this instance that invokes #bidi_streamer + op = c.operation + op.define_singleton_method(:execute) do + c.bidi_streamer(requests, &blk) + end + op + end + + private + # Creates a new active stub + # + # @param ch [GRPC::Channel] the channel used to create the stub. + # @param marshal [Function] f(obj)->string that marshals requests + # @param unmarshal [Function] f(string)->obj that unmarshals responses + # @param deadline [TimeConst] + def new_active_call(ch, marshal, unmarshal, deadline=nil) + absolute_deadline = TimeConsts.from_relative_time(deadline) + call = @ch.create_call(ch, @host, absolute_deadline) + ActiveCall.new(call, @queue, marshal, unmarshal, absolute_deadline, + started:false) + end + + end + +end diff --git a/src/ruby/lib/grpc/generic/rpc_desc.rb b/src/ruby/lib/grpc/generic/rpc_desc.rb new file mode 100644 index 0000000000..43b4d4ffc6 --- /dev/null +++ b/src/ruby/lib/grpc/generic/rpc_desc.rb @@ -0,0 +1,157 @@ +# Copyright 2014, 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' + +module GRPC + + # RpcDesc is a Descriptor of an RPC method. + class RpcDesc < Struct.new(:name, :input, :output, :marshal_method, + :unmarshal_method) + + # Used to wrap a message class to indicate that it needs to be streamed. + class Stream + attr_accessor :type + + def initialize(type) + @type = type + end + end + + # @return [Proc] { |instance| marshalled(instance) } + def marshal_proc + Proc.new { |o| o.method(marshal_method).call.to_s } + end + + # @param [:input, :output] target determines whether to produce the an + # unmarshal Proc for the rpc input parameter or + # its output parameter + # + # @return [Proc] An unmarshal proc { |marshalled(instance)| instance } + def unmarshal_proc(target) + raise ArgumentError if not [:input, :output].include?(target) + unmarshal_class = method(target).call + if unmarshal_class.is_a?Stream + unmarshal_class = unmarshal_class.type + end + Proc.new { |o| unmarshal_class.method(unmarshal_method).call(o) } + end + + def run_server_method(active_call, mth) + # While a server method is running, it might be cancelled, its deadline + # might be reached, the handler could throw an unknown error, or a + # well-behaved handler could throw a StatusError. + begin + if is_request_response? + req = active_call.remote_read + resp = mth.call(req, active_call.single_req_view) + active_call.remote_send(resp) + elsif is_client_streamer? + resp = mth.call(active_call.multi_req_view) + active_call.remote_send(resp) + elsif is_server_streamer? + req = active_call.remote_read + replys = mth.call(req, active_call.single_req_view) + replys.each { |r| active_call.remote_send(r) } + else # is a bidi_stream + active_call.run_server_bidi(mth) + end + send_status(active_call, StatusCodes::OK, 'OK') + active_call.finished + rescue BadStatus => e + # this is raised by handlers that want GRPC to send an application + # error code and detail message. + logger.debug("app error: #{active_call}, status:#{e.code}:#{e.details}") + send_status(active_call, e.code, e.details) + rescue CallError => e + # This is raised by GRPC internals but should rarely, if ever happen. + # Log it, but don't notify the other endpoint.. + logger.warn("failed call: #{active_call}\n#{e}") + rescue OutOfTime + # This is raised when active_call#method.call exceeeds the deadline + # event. Send a status of deadline exceeded + logger.warn("late call: #{active_call}") + send_status(active_call, StatusCodes::DEADLINE_EXCEEDED, 'late') + rescue EventError => e + # This is raised by GRPC internals but should rarely, if ever happen. + # Log it, but don't notify the other endpoint.. + logger.warn("failed call: #{active_call}\n#{e}") + rescue StandardError => e + # This will usuaally be an unhandled error in the handling code. + # Send back a UNKNOWN status to the client + logger.warn("failed handler: #{active_call}; sending status:UNKNOWN") + logger.warn(e) + send_status(active_call, StatusCodes::UNKNOWN, 'no reason given') + end + end + + def assert_arity_matches(mth) + if (is_request_response? || is_server_streamer?) + if mth.arity != 2 + raise arity_error(mth, 2, "should be #{mth.name}(req, call)") + end + else + if mth.arity != 1 + raise arity_error(mth, 1, "should be #{mth.name}(call)") + end + end + end + + def is_request_response? + !input.is_a?(Stream) && !output.is_a?(Stream) + end + + def is_client_streamer? + input.is_a?(Stream) && !output.is_a?(Stream) + end + + def is_server_streamer? + !input.is_a?(Stream) && output.is_a?(Stream) + end + + def is_bidi_streamer? + input.is_a?(Stream) && output.is_a?(Stream) + end + + def arity_error(mth, want, msg) + "##{mth.name}: bad arg count; got:#{mth.arity}, want:#{want}, #{msg}" + end + + def send_status(active_client, code, details) + begin + active_client.send_status(code, details) + rescue StandardError => e + logger.warn('Could not send status %d:%s' % [code, details]) + logger.warn(e) + end + end + + end + +end diff --git a/src/ruby/lib/grpc/generic/rpc_server.rb b/src/ruby/lib/grpc/generic/rpc_server.rb new file mode 100644 index 0000000000..e6efdc32c1 --- /dev/null +++ b/src/ruby/lib/grpc/generic/rpc_server.rb @@ -0,0 +1,408 @@ +# Copyright 2014, 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' +require 'grpc/generic/active_call' +require 'grpc/generic/service' +require 'thread' +require 'xray/thread_dump_signal_handler' + +module GRPC + + # RpcServer hosts a number of services and makes them available on the + # network. + class RpcServer + include CompletionType + extend ::Forwardable + + def_delegators :@server, :add_http2_port + + # Default thread pool size is 3 + DEFAULT_POOL_SIZE = 3 + + # Default max_waiting_requests size is 20 + DEFAULT_MAX_WAITING_REQUESTS = 20 + + # Creates a new RpcServer. + # + # The RPC server is configured using keyword arguments. + # + # There are some specific keyword args used to configure the RpcServer + # instance, however other arbitrary are allowed and when present are used + # to configure the listeninng connection set up by the RpcServer. + # + # * server_override: which if passed must be a [GRPC::Server]. When + # present. + # + # * poll_period: when present, the server polls for new events with this + # period + # + # * pool_size: the size of the thread pool the server uses to run its + # threads + # + # * completion_queue_override: when supplied, this will be used as the + # completion_queue that the server uses to receive network events, + # otherwise its creates a new instance itself + # + # * 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 + def initialize(pool_size:DEFAULT_POOL_SIZE, + max_waiting_requests:DEFAULT_MAX_WAITING_REQUESTS, + poll_period:TimeConsts::INFINITE_FUTURE, + completion_queue_override:nil, + server_override:nil, + **kw) + if !completion_queue_override.nil? + cq = completion_queue_override + if !cq.is_a?(CompletionQueue) + raise ArgumentError.new('not a CompletionQueue') + end + else + cq = CompletionQueue.new + end + @cq = cq + + if !server_override.nil? + srv = server_override + raise ArgumentError.new('not a Server') unless srv.is_a?(Server) + else + srv = Server.new(@cq, **kw) + end + @server = srv + + @pool_size = pool_size + @max_waiting_requests = max_waiting_requests + @poll_period = poll_period + @run_mutex = Mutex.new + @run_cond = ConditionVariable.new + @pool = Pool.new(@pool_size) + end + + # stops a running server + # + # the call has no impact if the server is already stopped, otherwise + # server's current call loop is it's last. + def stop + if @running + @stopped = true + @pool.stop + end + end + + # determines if the server is currently running + def running? + @running ||= false + end + + # Is called from other threads to wait for #run to start up the server. + # + # If run has not been called, this returns immediately. + # + # @param timeout [Numeric] number of seconds to wait + # @result [true, false] true if the server is running, false otherwise + def wait_till_running(timeout=0.1) + end_time, sleep_period = Time.now + timeout, (1.0 * timeout)/100 + while Time.now < end_time + if !running? + @run_mutex.synchronize { @run_cond.wait(@run_mutex) } + end + sleep(sleep_period) + end + return running? + end + + # determines if the server is currently stopped + def stopped? + @stopped ||= false + end + + # handle registration of classes + # + # service is either a class that includes GRPC::GenericService and whose + # #new function can be called without argument or any instance of such a + # class. + # + # E.g, after + # + # class Divider + # include GRPC::GenericService + # rpc :div DivArgs, DivReply # single request, single response + # def initialize(optional_arg='default option') # no args + # ... + # end + # + # srv = GRPC::RpcServer.new(...) + # + # # Either of these works + # + # srv.handle(Divider) + # + # # or + # + # srv.handle(Divider.new('replace optional arg')) + # + # It raises RuntimeError: + # - if service is not valid service class or object + # - if it is a valid service, but the handler methods are already registered + # - if the server is already running + # + # @param service [Object|Class] a service class or object as described + # above + def handle(service) + raise 'cannot add services if the server is running' if running? + raise 'cannot add services if the server is stopped' if stopped? + cls = service.is_a?(Class) ? service : service.class + assert_valid_service_class(cls) + add_rpc_descs_for(service) + end + + # runs the server + # + # - if no rpc_descs are registered, this exits immediately, otherwise it + # continues running permanently and does not return until program exit. + # + # - #running? returns true after this is called, until #stop cause the + # the server to stop. + def run + if rpc_descs.size == 0 + logger.warn('did not run as no services were present') + return + end + @run_mutex.synchronize do + @running = true + @run_cond.signal + end + @pool.start + @server.start + server_tag = Object.new + while !stopped? + @server.request_call(server_tag) + ev = @cq.pluck(server_tag, @poll_period) + next if ev.nil? + if ev.type != SERVER_RPC_NEW + logger.warn("bad evt: got:#{ev.type}, want:#{SERVER_RPC_NEW}") + next + end + c = new_active_server_call(ev.call, ev.result) + if !c.nil? + mth = ev.result.method.to_sym + + # NOTE(temiola): This is necessary to allow the C call struct wrapped + # within the active_call created by the event to be GCed; this is + # necessary so that other C-level destructors get called in the + # required order. + ev = nil + + @pool.schedule(c) do |call| + rpc_descs[mth].run_server_method(call, rpc_handlers[mth]) + end + end + end + @running = false + end + + def new_active_server_call(call, new_server_rpc) + # TODO(temiola): perhaps reuse the main server completion queue here, but + # for now, create a new completion queue per call, pending best practice + # usage advice from the c core. + + # Accept the call. This is necessary even if a status is to be sent back + # immediately + finished_tag = Object.new + call_queue = CompletionQueue.new + call.accept(call_queue, finished_tag) + + # Send UNAVAILABLE if there are too many unprocessed jobs + jobs_count, max = @pool.jobs_waiting, @max_waiting_requests + logger.info("waiting: #{jobs_count}, max: #{max}") + if @pool.jobs_waiting > @max_waiting_requests + logger.warn("NOT AVAILABLE: too many jobs_waiting: #{new_server_rpc}") + noop = Proc.new { |x| x } + c = ActiveCall.new(call, call_queue, noop, noop, + new_server_rpc.deadline, finished_tag: finished_tag) + c.send_status(StatusCodes::UNAVAILABLE, '') + return nil + end + + # Send NOT_FOUND if the method does not exist + mth = new_server_rpc.method.to_sym + if !rpc_descs.has_key?(mth) + logger.warn("NOT_FOUND: #{new_server_rpc}") + noop = Proc.new { |x| x } + c = ActiveCall.new(call, call_queue, noop, noop, + new_server_rpc.deadline, finished_tag: finished_tag) + c.send_status(StatusCodes::NOT_FOUND, '') + return nil + end + + # Create the ActiveCall + rpc_desc = rpc_descs[mth] + logger.info("deadline is #{new_server_rpc.deadline}; (now=#{Time.now})") + ActiveCall.new(call, call_queue, + rpc_desc.marshal_proc, rpc_desc.unmarshal_proc(:input), + new_server_rpc.deadline, finished_tag: finished_tag) + end + + # Pool is a simple thread pool for running server requests. + class Pool + + def initialize(size) + raise 'pool size must be positive' unless size > 0 + @jobs = Queue.new + @size = size + @stopped = false + @stop_mutex = Mutex.new + @stop_cond = ConditionVariable.new + @workers = [] + end + + # Returns the number of jobs waiting + def jobs_waiting + @jobs.size + 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) + raise 'already stopped' if @stopped + return if blk.nil? + logger.info('schedule another job') + @jobs << [blk, args] + end + + # Starts running the jobs in the thread pool. + def start + raise 'already stopped' if @stopped + until @workers.size == @size.to_i + next_thread = Thread.new do + catch(:exit) do # allows { throw :exit } to kill a thread + loop do + begin + blk, args = @jobs.pop + blk.call(*args) + rescue StandardError => e + logger.warn('Error in worker thread') + logger.warn(e) + end + end + end + + # removes the threads from workers, and signal when all the threads + # are complete. + @stop_mutex.synchronize do + @workers.delete(Thread.current) + if @workers.size == 0 + @stop_cond.signal + end + end + end + @workers << next_thread + end + end + + # Stops the jobs in the pool + def stop + logger.info('stopping, will wait for all the workers to exit') + @workers.size.times { schedule { throw :exit } } + @stopped = true + + # TODO(temiola): allow configuration of the keepalive period + keep_alive = 5 + @stop_mutex.synchronize do + if @workers.size > 0 + @stop_cond.wait(@stop_mutex, keep_alive) + end + end + + # Forcibly shutdown any threads that are still alive. + if @workers.size > 0 + logger.warn("forcibly terminating #{@workers.size} worker(s)") + @workers.each do |t| + next unless t.alive? + begin + t.exit + rescue StandardError => e + logger.warn('error while terminating a worker') + logger.warn(e) + end + end + end + + logger.info('stopped, all workers are shutdown') + end + + end + + protected + + def rpc_descs + @rpc_descs ||= {} + end + + def rpc_handlers + @rpc_handlers ||= {} + end + + private + + def assert_valid_service_class(cls) + if !cls.include?(GenericService) + raise "#{cls} should 'include GenericService'" + end + if cls.rpc_descs.size == 0 + raise "#{cls} should specify some rpc descriptions" + end + cls.assert_rpc_descs_have_methods + end + + def add_rpc_descs_for(service) + cls = service.is_a?(Class) ? service : service.class + specs = rpc_descs + handlers = rpc_handlers + cls.rpc_descs.each_pair do |name,spec| + route = "/#{cls.service_name}/#{name}".to_sym + if specs.has_key?(route) + raise "Cannot add rpc #{route} from #{spec}, already registered" + else + specs[route] = spec + if service.is_a?(Class) + handlers[route] = cls.new.method(name.to_s.underscore.to_sym) + else + handlers[route] = service.method(name.to_s.underscore.to_sym) + end + logger.info("handling #{route} with #{handlers[route]}") + end + end + end + end + +end diff --git a/src/ruby/lib/grpc/generic/service.rb b/src/ruby/lib/grpc/generic/service.rb new file mode 100644 index 0000000000..1a3d0dc63e --- /dev/null +++ b/src/ruby/lib/grpc/generic/service.rb @@ -0,0 +1,247 @@ +# Copyright 2014, 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' +require 'grpc/generic/client_stub' +require 'grpc/generic/rpc_desc' + +# Extend String to add a method underscore +class String + + # creates a new string that is the underscore separate version of this one. + # + # E.g, + # PrintHTML -> print_html + # AMethod -> a_method + # AnRpc -> an_rpc + def underscore + word = self.dup + word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2') + word.gsub!(/([a-z\d])([A-Z])/, '\1_\2') + word.tr!('-', '_') + word.downcase! + word + end + +end + +module GRPC + + # Provides behaviour used to implement schema-derived service classes. + # + # Is intended to be used to support both client and server IDL-schema-derived + # servers. + module GenericService + + # Used to indicate that a name has already been specified + class DuplicateRpcName < StandardError + def initialize(name) + super("rpc (#{name}) is already defined") + end + end + + # Provides a simple DSL to describe RPC services. + # + # E.g, a Maths service that uses the serializable messages DivArgs, + # DivReply and Num might define its endpoint uses the following way: + # + # rpc :div DivArgs, DivReply # single request, single response + # rpc :sum stream(Num), Num # streamed input, single response + # rpc :fib FibArgs, stream(Num) # single request, streamed response + # rpc :div_many stream(DivArgs), stream(DivReply) + # # streamed req and resp + # + # Each 'rpc' adds an RpcDesc to classes including this module, and + # #assert_rpc_descs_have_methods is used to ensure the including class + # provides methods with signatures that support all the descriptors. + module Dsl + + # This configures the method names that the serializable message + # implementation uses to marshal and unmarshal messages. + # + # - unmarshal_class method must be a class method on the serializable + # message type that takes a string (byte stream) and produces and object + # + # - marshal_instance_method is called on a serializable message instance + # and produces a serialized string. + # + # The Dsl verifies that the types in the descriptor have both the + # unmarshal and marshal methods. + attr_writer(:marshal_instance_method, :unmarshal_class_method) + attr_accessor(:service_name) + + # Adds an RPC spec. + # + # Takes the RPC name and the classes representing the types to be + # serialized, and adds them to the including classes rpc_desc hash. + # + # input and output should both have the methods #marshal and #unmarshal + # that are responsible for writing and reading an object instance from a + # byte buffer respectively. + # + # @param name [String] the name of the rpc + # @param input [Object] the input parameter's class + # @param output [Object] the output parameter's class + def rpc(name, input, output) + raise DuplicateRpcName, name if rpc_descs.has_key?(name) + assert_can_marshal(input) + assert_can_marshal(output) + rpc_descs[name] = RpcDesc.new(name, input, output, + marshal_instance_method, + unmarshal_class_method) + end + + def inherited(subclass) + # Each subclass should have distinct class variable with its own + # rpc_descs. + subclass.rpc_descs.merge!(rpc_descs) + subclass.service_name = service_name + end + + # the name of the instance method used to marshal events to a byte stream. + def marshal_instance_method + @marshal_instance_method ||= :marshal + end + + # the name of the class method used to unmarshal from a byte stream. + def unmarshal_class_method + @unmarshal_class_method ||= :unmarshal + end + + def assert_can_marshal(cls) + if cls.is_a?RpcDesc::Stream + cls = cls.type + end + + mth = unmarshal_class_method + if !cls.methods.include?(mth) + raise ArgumentError, "#{cls} needs #{cls}.#{mth}" + end + + mth = marshal_instance_method + if !cls.instance_methods.include?(mth) + raise ArgumentError, "#{cls} needs #{cls}.new.#{mth}" + end + end + + # @param cls [Class] the class of a serializable type + # @return cls wrapped in a RpcDesc::Stream + def stream(cls) + assert_can_marshal(cls) + RpcDesc::Stream.new(cls) + end + + # the RpcDescs defined for this GenericService, keyed by name. + def rpc_descs + @rpc_descs ||= {} + end + + # Creates a rpc client class with methods for accessing the methods + # currently in rpc_descs. + def rpc_stub_class + descs = rpc_descs + route_prefix = service_name + Class.new(ClientStub) do + + # @param host [String] the host the stub connects to + # @param kw [KeywordArgs] the channel arguments, plus any optional + # args for configuring the client's channel + def initialize(host, **kw) + super(host, CompletionQueue.new, **kw) + end + + # Used define_method to add a method for each rpc_desc. Each method + # calls the base class method for the given descriptor. + descs.each_pair do |name,desc| + mth_name = name.to_s.underscore.to_sym + marshal = desc.marshal_proc + unmarshal = desc.unmarshal_proc(:output) + route = "/#{route_prefix}/#{name}" + if desc.is_request_response? + define_method(mth_name) do |req,deadline=nil| + logger.debug("calling #{@host}:#{route}") + request_response(route, req, marshal, unmarshal, deadline) + end + elsif desc.is_client_streamer? + define_method(mth_name) do |reqs,deadline=nil| + logger.debug("calling #{@host}:#{route}") + client_streamer(route, reqs, marshal, unmarshal, deadline) + end + elsif desc.is_server_streamer? + define_method(mth_name) do |req,deadline=nil,&blk| + logger.debug("calling #{@host}:#{route}") + server_streamer(route, req, marshal, unmarshal, deadline, &blk) + end + else # is a bidi_stream + define_method(mth_name) do |reqs, deadline=nil,&blk| + logger.debug("calling #{@host}:#{route}") + bidi_streamer(route, reqs, marshal, unmarshal, deadline, &blk) + end + end + end + + end + + end + + # Asserts that the appropriate methods are defined for each added rpc + # spec. Is intended to aid verifying that server classes are correctly + # implemented. + def assert_rpc_descs_have_methods + rpc_descs.each_pair do |m,spec| + mth_name = m.to_s.underscore.to_sym + if !self.instance_methods.include?(mth_name) + raise "#{self} does not provide instance method '#{mth_name}'" + end + spec.assert_arity_matches(self.instance_method(mth_name)) + end + end + + end + + def self.included(o) + o.extend(Dsl) + + # Update to the use the name including module. This can be nil e,g. when + # modules are declared dynamically. + if o.name.nil? + o.service_name = 'GenericService' + else + modules = o.name.split('::') + if modules.length > 2 + o.service_name = modules[modules.length - 2] + else + o.service_name = modules.first + end + end + end + + end + +end diff --git a/src/ruby/lib/grpc/logconfig.rb b/src/ruby/lib/grpc/logconfig.rb new file mode 100644 index 0000000000..6d8e1899a0 --- /dev/null +++ b/src/ruby/lib/grpc/logconfig.rb @@ -0,0 +1,40 @@ +# Copyright 2014, 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 'logging' + +include Logging.globally # logger is accessible everywhere + +Logging.logger.root.appenders = Logging.appenders.stdout +Logging.logger.root.level = :info + +# TODO(temiola): provide command-line configuration for logging +Logging.logger['Google::RPC'].level = :debug +Logging.logger['Google::RPC::ActiveCall'].level = :info +Logging.logger['Google::RPC::BidiCall'].level = :info diff --git a/src/ruby/lib/grpc/time_consts.rb b/src/ruby/lib/grpc/time_consts.rb new file mode 100644 index 0000000000..2cbab5d965 --- /dev/null +++ b/src/ruby/lib/grpc/time_consts.rb @@ -0,0 +1,69 @@ +# Copyright 2014, 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' + +module Google + module RPC + module TimeConsts # re-opens a module in the C extension. + + # Converts a time delta to an absolute deadline. + # + # Assumes timeish is a relative time, and converts its to an absolute, + # with following exceptions: + # + # * if timish is one of the TimeConsts.TimeSpec constants the value is + # preserved. + # * timish < 0 => TimeConsts.INFINITE_FUTURE + # * timish == 0 => TimeConsts.ZERO + # + # @param timeish [Number|TimeSpec] + # @return timeish [Number|TimeSpec] + def from_relative_time(timeish) + if timeish.is_a?TimeSpec + timeish + elsif timeish.nil? + TimeConsts::ZERO + elsif !timeish.is_a?Numeric + raise TypeError('Cannot make an absolute deadline from %s', + timeish.inspect) + elsif timeish < 0 + TimeConsts::INFINITE_FUTURE + elsif timeish == 0 + TimeConsts::ZERO + else !timeish.nil? + Time.now + timeish + end + end + + module_function :from_relative_time + + end + end +end diff --git a/src/ruby/lib/grpc/version.rb b/src/ruby/lib/grpc/version.rb new file mode 100644 index 0000000000..0a84f4c3a7 --- /dev/null +++ b/src/ruby/lib/grpc/version.rb @@ -0,0 +1,34 @@ +# Copyright 2014, 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. + +module Google + module RPC + VERSION = '0.0.1' + end +end diff --git a/src/ruby/spec/alloc_spec.rb b/src/ruby/spec/alloc_spec.rb new file mode 100644 index 0000000000..99cc39d0ba --- /dev/null +++ b/src/ruby/spec/alloc_spec.rb @@ -0,0 +1,46 @@ +# Copyright 2014, 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' + +describe 'Wrapped classes where .new cannot create an instance' do + + describe GRPC::Event do + it 'should fail .new fail with a runtime error' do + expect { GRPC::Event.new }.to raise_error(TypeError) + end + end + + describe GRPC::Call do + it 'should fail .new fail with a runtime error' do + expect { GRPC::Event.new }.to raise_error(TypeError) + end + end + +end diff --git a/src/ruby/spec/byte_buffer_spec.rb b/src/ruby/spec/byte_buffer_spec.rb new file mode 100644 index 0000000000..d4d3a692b1 --- /dev/null +++ b/src/ruby/spec/byte_buffer_spec.rb @@ -0,0 +1,71 @@ +# Copyright 2014, 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' + +describe GRPC::ByteBuffer do + + describe '#new' do + + it 'is constructed from a string' do + expect { GRPC::ByteBuffer.new('#new') }.not_to raise_error + end + + it 'can be constructed from the empty string' do + expect { GRPC::ByteBuffer.new('') }.not_to raise_error + end + + it 'cannot be constructed from nil' do + expect { GRPC::ByteBuffer.new(nil) }.to raise_error TypeError + end + + it 'cannot be constructed from non-strings' do + [1, Object.new, :a_symbol].each do |x| + expect { GRPC::ByteBuffer.new(x) }.to raise_error TypeError + end + end + + end + + describe '#to_s' do + it 'is the string value the ByteBuffer was constructed with' do + expect(GRPC::ByteBuffer.new('#to_s').to_s).to eq('#to_s') + end + end + + describe '#dup' do + it 'makes an instance whose #to_s is the original string value' do + bb = GRPC::ByteBuffer.new('#dup') + a_copy = bb.dup + expect(a_copy.to_s).to eq('#dup') + expect(a_copy.dup.to_s).to eq('#dup') + end + end + +end diff --git a/src/ruby/spec/call_spec.rb b/src/ruby/spec/call_spec.rb new file mode 100644 index 0000000000..339f2c1a94 --- /dev/null +++ b/src/ruby/spec/call_spec.rb @@ -0,0 +1,200 @@ +# Copyright 2014, 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' +require 'port_picker' + +include GRPC::StatusCodes + +describe GRPC::RpcErrors do + + before(:each) do + @known_types = { + :OK => 0, + :ERROR => 1, + :NOT_ON_SERVER => 2, + :NOT_ON_CLIENT => 3, + :ALREADY_INVOKED => 4, + :NOT_INVOKED => 5, + :ALREADY_FINISHED => 6, + :TOO_MANY_OPERATIONS => 7, + :INVALID_FLAGS => 8, + :ErrorMessages => { + 0=>'ok', + 1=>'unknown error', + 2=>'not available on a server', + 3=>'not available on a client', + 4=>'call is already invoked', + 5=>'call is not yet invoked', + 6=>'call is already finished', + 7=>'outstanding read or write present', + 8=>'a bad flag was given', + } + } + end + + it 'should have symbols for all the known error codes' do + m = GRPC::RpcErrors + syms_and_codes = m.constants.collect { |c| [c, m.const_get(c)] } + expect(Hash[syms_and_codes]).to eq(@known_types) + end + +end + +describe GRPC::Call do + + before(:each) do + @tag = Object.new + @client_queue = GRPC::CompletionQueue.new + @server_queue = GRPC::CompletionQueue.new + port = find_unused_tcp_port + host = "localhost:#{port}" + @server = GRPC::Server.new(@server_queue, nil) + @server.add_http2_port(host) + @ch = GRPC::Channel.new(host, nil) + end + + after(:each) do + @server.close + end + + describe '#start_read' do + it 'should fail if called immediately' do + expect { make_test_call.start_read(@tag) }.to raise_error GRPC::CallError + end + end + + describe '#start_write' do + it 'should fail if called immediately' do + bytes = GRPC::ByteBuffer.new('test string') + expect { make_test_call.start_write(bytes, @tag) } + .to raise_error GRPC::CallError + end + end + + describe '#start_write_status' do + it 'should fail if called immediately' do + sts = GRPC::Status.new(153, 'test detail') + expect { make_test_call.start_write_status(sts, @tag) } + .to raise_error GRPC::CallError + end + end + + describe '#writes_done' do + it 'should fail if called immediately' do + expect { make_test_call.writes_done(@tag) }.to raise_error GRPC::CallError + end + end + + describe '#add_metadata' do + it 'adds metadata to a call without fail' do + call = make_test_call + n = 37 + metadata = Hash[n.times.collect { |i| ["key%d" % i, "value%d" %i] } ] + expect { call.add_metadata(metadata) }.to_not raise_error + end + end + + describe '#start_invoke' do + it 'should cause the INVOKE_ACCEPTED event' do + call = make_test_call + expect(call.start_invoke(@client_queue, @tag, @tag, @tag)).to be_nil + ev = @client_queue.next(deadline) + expect(ev.call).to be_a(GRPC::Call) + expect(ev.tag).to be(@tag) + expect(ev.type).to be(GRPC::CompletionType::INVOKE_ACCEPTED) + expect(ev.call).to_not be(call) + end + end + + describe '#start_write' do + it 'should cause the WRITE_ACCEPTED event' do + call = make_test_call + call.start_invoke(@client_queue, @tag, @tag, @tag) + ev = @client_queue.next(deadline) + expect(ev.type).to be(GRPC::CompletionType::INVOKE_ACCEPTED) + expect(call.start_write(GRPC::ByteBuffer.new('test_start_write'), + @tag)).to be_nil + ev = @client_queue.next(deadline) + expect(ev.call).to be_a(GRPC::Call) + expect(ev.type).to be(GRPC::CompletionType::WRITE_ACCEPTED) + expect(ev.tag).to be(@tag) + end + end + + describe '#status' do + it 'can save the status and read it back' do + call = make_test_call + sts = GRPC::Status.new(OK, 'OK') + expect { call.status = sts }.not_to raise_error + expect(call.status).to be(sts) + end + + it 'must be set to a status' do + call = make_test_call + bad_sts = Object.new + expect { call.status = bad_sts }.to raise_error(TypeError) + end + + it 'can be set to nil' do + call = make_test_call + expect { call.status = nil }.not_to raise_error + end + end + + describe '#metadata' do + it 'can save the metadata hash and read it back' do + call = make_test_call + md = {'k1' => 'v1', 'k2' => 'v2'} + expect { call.metadata = md }.not_to raise_error + expect(call.metadata).to be(md) + end + + it 'must be set with a hash' do + call = make_test_call + bad_md = Object.new + expect { call.metadata = bad_md }.to raise_error(TypeError) + end + + it 'can be set to nil' do + call = make_test_call + expect { call.metadata = nil }.not_to raise_error + end + end + + + def make_test_call + @ch.create_call('dummy_method', 'dummy_host', deadline) + end + + def deadline + Time.now + 2 # in 2 seconds; arbitrary + end + +end diff --git a/src/ruby/spec/channel_spec.rb b/src/ruby/spec/channel_spec.rb new file mode 100644 index 0000000000..bd46bffc10 --- /dev/null +++ b/src/ruby/spec/channel_spec.rb @@ -0,0 +1,164 @@ +# Copyright 2014, 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' +require 'port_picker' + +module GRPC + + describe Channel do + + before(:each) do + @cq = CompletionQueue.new + end + + describe '#new' do + + it 'take a host name without channel args' do + expect { Channel.new('dummy_host', nil) }.not_to raise_error + end + + it 'does not take a hash with bad keys as channel args' do + blk = construct_with_args(Object.new => 1) + expect(&blk).to raise_error TypeError + blk = construct_with_args(1 => 1) + expect(&blk).to raise_error TypeError + end + + it 'does not take a hash with bad values as channel args' do + blk = construct_with_args(:symbol => Object.new) + expect(&blk).to raise_error TypeError + blk = construct_with_args('1' => Hash.new) + expect(&blk).to raise_error TypeError + end + + it 'can take a hash with a symbol key as channel args' do + blk = construct_with_args(:a_symbol => 1) + expect(&blk).to_not raise_error + end + + it 'can take a hash with a string key as channel args' do + blk = construct_with_args('a_symbol' => 1) + expect(&blk).to_not raise_error + end + + it 'can take a hash with a string value as channel args' do + blk = construct_with_args(:a_symbol => '1') + expect(&blk).to_not raise_error + end + + it 'can take a hash with a symbol value as channel args' do + blk = construct_with_args(:a_symbol => :another_symbol) + expect(&blk).to_not raise_error + end + + it 'can take a hash with a numeric value as channel args' do + blk = construct_with_args(:a_symbol => 1) + expect(&blk).to_not raise_error + end + + it 'can take a hash with many args as channel args' do + args = Hash[127.times.collect { |x| [x.to_s, x] } ] + blk = construct_with_args(args) + expect(&blk).to_not raise_error + end + + end + + describe '#create_call' do + it 'creates a call OK' do + port = find_unused_tcp_port + host = "localhost:#{port}" + ch = Channel.new(host, nil) + + deadline = Time.now + 5 + expect(ch.create_call('dummy_method', 'dummy_host', deadline)) + .not_to be(nil) + end + + it 'raises an error if called on a closed channel' do + port = find_unused_tcp_port + host = "localhost:#{port}" + ch = Channel.new(host, nil) + ch.close + + deadline = Time.now + 5 + blk = Proc.new do + ch.create_call('dummy_method', 'dummy_host', deadline) + end + expect(&blk).to raise_error(RuntimeError) + end + + end + + describe '#destroy' do + it 'destroys a channel ok' do + port = find_unused_tcp_port + host = "localhost:#{port}" + ch = Channel.new(host, nil) + blk = Proc.new { ch.destroy } + expect(&blk).to_not raise_error + end + + it 'can be called more than once without error' do + port = find_unused_tcp_port + host = "localhost:#{port}" + ch = Channel.new(host, nil) + blk = Proc.new { ch.destroy } + blk.call + expect(&blk).to_not raise_error + end + end + + describe '#close' do + it 'closes a channel ok' do + port = find_unused_tcp_port + host = "localhost:#{port}" + ch = Channel.new(host, nil) + blk = Proc.new { ch.close } + expect(&blk).to_not raise_error + end + + it 'can be called more than once without error' do + port = find_unused_tcp_port + host = "localhost:#{port}" + ch = Channel.new(host, nil) + blk = Proc.new { ch.close } + blk.call + expect(&blk).to_not raise_error + end + end + + def construct_with_args(a) + Proc.new {Channel.new('dummy_host', a)} + end + + end + +end diff --git a/src/ruby/spec/client_server_spec.rb b/src/ruby/spec/client_server_spec.rb new file mode 100644 index 0000000000..64068ab391 --- /dev/null +++ b/src/ruby/spec/client_server_spec.rb @@ -0,0 +1,349 @@ +# Copyright 2014, 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' +require 'port_picker' +require 'spec_helper' + +include GRPC::CompletionType +include GRPC + +shared_context 'setup: tags' do + + before(:example) do + @server_finished_tag = Object.new + @client_finished_tag = Object.new + @server_tag = Object.new + @tag = Object.new + end + + def deadline + Time.now + 0.05 + end + + def expect_next_event_on(queue, type, tag) + ev = queue.pluck(tag, deadline) + if type.nil? + expect(ev).to be_nil + else + expect(ev).to_not be_nil + expect(ev.type).to be(type) + end + ev + end + + def server_receives_and_responds_with(reply_text) + reply = ByteBuffer.new(reply_text) + @server.request_call(@server_tag) + ev = @server_queue.pluck(@server_tag, TimeConsts::INFINITE_FUTURE) + expect(ev).not_to be_nil + expect(ev.type).to be(SERVER_RPC_NEW) + ev.call.accept(@server_queue, @server_finished_tag) + ev.call.start_read(@server_tag) + ev = @server_queue.pluck(@server_tag, TimeConsts::INFINITE_FUTURE) + expect(ev.type).to be(READ) + ev.call.start_write(reply, @server_tag) + ev = @server_queue.pluck(@server_tag, TimeConsts::INFINITE_FUTURE) + expect(ev).not_to be_nil + expect(ev.type).to be(WRITE_ACCEPTED) + return ev.call + end + + def client_sends(call, sent='a message') + req = ByteBuffer.new(sent) + call.start_invoke(@client_queue, @tag, @tag, @client_finished_tag) + ev = @client_queue.pluck(@tag, TimeConsts::INFINITE_FUTURE) + expect(ev).not_to be_nil + expect(ev.type).to be(INVOKE_ACCEPTED) + call.start_write(req, @tag) + ev = @client_queue.pluck(@tag, TimeConsts::INFINITE_FUTURE) + expect(ev).not_to be_nil + expect(ev.type).to be(WRITE_ACCEPTED) + return sent + end + + def new_client_call + @ch.create_call('/method', 'localhost', deadline) + end + +end + +shared_examples 'basic GRPC message delivery is OK' do + + include_context 'setup: tags' + + it 'servers receive requests from clients and start responding' do + reply = ByteBuffer.new('the server payload') + call = new_client_call + msg = client_sends(call) + + # check the server rpc new was received + @server.request_call(@server_tag) + ev = expect_next_event_on(@server_queue, SERVER_RPC_NEW, @server_tag) + + # accept the call + server_call = ev.call + server_call.accept(@server_queue, @server_finished_tag) + + # confirm the server can read the inbound message + server_call.start_read(@server_tag) + ev = expect_next_event_on(@server_queue, READ, @server_tag) + expect(ev.result.to_s).to eq(msg) + + # the server response + server_call.start_write(reply, @server_tag) + ev = expect_next_event_on(@server_queue, WRITE_ACCEPTED, @server_tag) + end + + it 'responses written by servers are received by the client' do + call = new_client_call + client_sends(call) + server_receives_and_responds_with('server_response') + + call.start_read(@tag) + ev = expect_next_event_on(@client_queue, CLIENT_METADATA_READ, @tag) + ev = expect_next_event_on(@client_queue, READ, @tag) + expect(ev.result.to_s).to eq('server_response') + end + + it 'servers can ignore a client write and send a status' do + reply = ByteBuffer.new('the server payload') + call = new_client_call + msg = client_sends(call) + + # check the server rpc new was received + @server.request_call(@server_tag) + ev = expect_next_event_on(@server_queue, SERVER_RPC_NEW, @server_tag) + expect(ev.tag).to be(@server_tag) + + # accept the call - need to do this to sent status. + server_call = ev.call + server_call.accept(@server_queue, @server_finished_tag) + sts = Status.new(StatusCodes::NOT_FOUND, 'not found') + server_call.start_write_status(sts, @server_tag) + + # client gets an empty response for the read, preceeded by some metadata. + call.start_read(@tag) + ev = expect_next_event_on(@client_queue, CLIENT_METADATA_READ, @tag) + ev = expect_next_event_on(@client_queue, READ, @tag) + expect(ev.tag).to be(@tag) + expect(ev.result.to_s).to eq('') + + # finally, after client sends writes_done, they get the finished. + call.writes_done(@tag) + ev = expect_next_event_on(@client_queue, FINISH_ACCEPTED, @tag) + ev = expect_next_event_on(@client_queue, FINISHED, @client_finished_tag) + expect(ev.result.code).to eq(StatusCodes::NOT_FOUND) + end + + it 'completes calls by sending status to client and server' do + call = new_client_call + client_sends(call) + server_call = server_receives_and_responds_with('server_response') + sts = Status.new(10101, 'status code is 10101') + server_call.start_write_status(sts, @server_tag) + + # first the client says writes are done + call.start_read(@tag) + ev = expect_next_event_on(@client_queue, CLIENT_METADATA_READ, @tag) + ev = expect_next_event_on(@client_queue, READ, @tag) + call.writes_done(@tag) + + # but nothing happens until the server sends a status + expect_next_event_on(@server_queue, FINISH_ACCEPTED, @server_tag) + ev = expect_next_event_on(@server_queue, FINISHED, @server_finished_tag) + expect(ev.result).to be_a(Status) + + # client gets FINISHED + expect_next_event_on(@client_queue, FINISH_ACCEPTED, @tag) + ev = expect_next_event_on(@client_queue, FINISHED, @client_finished_tag) + expect(ev.result.details).to eq('status code is 10101') + expect(ev.result.code).to eq(10101) + end + +end + + +shared_examples 'GRPC metadata delivery works OK' do + + include_context 'setup: tags' + + describe 'from client => server' do + + before(:example) do + n = 7 # arbitrary number of metadata + diff_keys = Hash[n.times.collect { |i| ['k%d' % i, 'v%d' % i] }] + null_vals = Hash[n.times.collect { |i| ['k%d' % i, 'v\0%d' % i] }] + same_keys = Hash[n.times.collect { |i| ['k%d' % i, ['v%d' % i] * n] }] + symbol_key = {:a_key => 'a val'} + @valid_metadata = [diff_keys, same_keys, null_vals, symbol_key] + @bad_keys = [] + @bad_keys << { Object.new => 'a value' } + @bad_keys << { 1 => 'a value' } + end + + it 'raises an exception if a metadata key is invalid' do + @bad_keys.each do |md| + call = new_client_call + expect { call.add_metadata(md) }.to raise_error + end + end + + it 'sends an empty hash when no metadata is added' do + call = new_client_call + client_sends(call) + + # Server gets a response + @server.request_call(@server_tag) + expect_next_event_on(@server_queue, SERVER_RPC_NEW, @server_tag) + end + + it 'sends all the metadata pairs when keys and values are valid' do + @valid_metadata.each do |md| + call = new_client_call + call.add_metadata(md) + + # Client begins a call OK + call.start_invoke(@client_queue, @tag, @tag, @client_finished_tag) + ev = expect_next_event_on(@client_queue, INVOKE_ACCEPTED, @tag) + + # ... server has all metadata available even though the client did not + # send a write + @server.request_call(@server_tag) + ev = expect_next_event_on(@server_queue, SERVER_RPC_NEW, @server_tag) + replace_symbols = Hash[md.each_pair.collect { |x,y| [x.to_s, y] }] + result = ev.result.metadata + expect(result.merge(replace_symbols)).to eq(result) + end + end + + end + + describe 'from server => client' do + + before(:example) do + n = 7 # arbitrary number of metadata + diff_keys = Hash[n.times.collect { |i| ['k%d' % i, 'v%d' % i] }] + null_vals = Hash[n.times.collect { |i| ['k%d' % i, 'v\0%d' % i] }] + same_keys = Hash[n.times.collect { |i| ['k%d' % i, ['v%d' % i] * n] }] + symbol_key = {:a_key => 'a val'} + @valid_metadata = [diff_keys, same_keys, null_vals, symbol_key] + @bad_keys = [] + @bad_keys << { Object.new => 'a value' } + @bad_keys << { 1 => 'a value' } + end + + it 'raises an exception if a metadata key is invalid' do + @bad_keys.each do |md| + call = new_client_call + client_sends(call) + + # server gets the invocation + @server.request_call(@server_tag) + ev = expect_next_event_on(@server_queue, SERVER_RPC_NEW, @server_tag) + expect { ev.call.add_metadata(md) }.to raise_error + end + end + + it 'sends a hash that contains the status when no metadata is added' do + call = new_client_call + client_sends(call) + + # server gets the invocation + @server.request_call(@server_tag) + ev = expect_next_event_on(@server_queue, SERVER_RPC_NEW, @server_tag) + server_call = ev.call + + # ... server accepts the call without adding metadata + server_call.accept(@server_queue, @server_finished_tag) + + # ... these server sends some data, allowing the metadata read + server_call.start_write(ByteBuffer.new('reply with metadata'), + @server_tag) + expect_next_event_on(@server_queue, WRITE_ACCEPTED, @server_tag) + + # there is the HTTP status metadata, though there should not be any + # TODO(temiola): update this with the bug number to be resolved + ev = expect_next_event_on(@client_queue, CLIENT_METADATA_READ, @tag) + expect(ev.result).to eq({':status' => '200'}) + end + + it 'sends all the pairs and status:200 when keys and values are valid' do + @valid_metadata.each do |md| + call = new_client_call + client_sends(call) + + # server gets the invocation + @server.request_call(@server_tag) + ev = expect_next_event_on(@server_queue, SERVER_RPC_NEW, @server_tag) + server_call = ev.call + + # ... server adds metadata and accepts the call + server_call.add_metadata(md) + server_call.accept(@server_queue, @server_finished_tag) + + # Now the client can read the metadata + ev = expect_next_event_on(@client_queue, CLIENT_METADATA_READ, @tag) + replace_symbols = Hash[md.each_pair.collect { |x,y| [x.to_s, y] }] + replace_symbols[':status'] = '200' + expect(ev.result).to eq(replace_symbols) + end + + end + + end + +end + + +describe 'the http client/server' do + + before(:example) do + port = find_unused_tcp_port + host = "localhost:#{port}" + @client_queue = GRPC::CompletionQueue.new + @server_queue = GRPC::CompletionQueue.new + @server = GRPC::Server.new(@server_queue, nil) + @server.add_http2_port(host) + @server.start + @ch = Channel.new(host, nil) + end + + after(:example) do + @server.close + end + + + it_behaves_like 'basic GRPC message delivery is OK' do + end + + it_behaves_like 'GRPC metadata delivery works OK' do + end + +end diff --git a/src/ruby/spec/completion_queue_spec.rb b/src/ruby/spec/completion_queue_spec.rb new file mode 100644 index 0000000000..37432443a9 --- /dev/null +++ b/src/ruby/spec/completion_queue_spec.rb @@ -0,0 +1,82 @@ +# Copyright 2014, 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' + +describe GRPC::CompletionQueue do + + describe '#new' do + it 'is constructed successufully' do + expect { GRPC::CompletionQueue.new }.not_to raise_error + expect(GRPC::CompletionQueue.new).to be_a(GRPC::CompletionQueue) + end + end + + describe '#next' do + it 'can be called without failing' do + ch = GRPC::CompletionQueue.new + expect { ch.next(3) }.not_to raise_error + end + + it 'can be called with the time constants' do + ch = GRPC::CompletionQueue.new + # don't use INFINITE_FUTURE, as there we have no events. + non_blocking_consts = [:ZERO, :INFINITE_PAST] + m = GRPC::TimeConsts + non_blocking_consts.each do |c| + a_time = m.const_get(c) + expect { ch.next(a_time) }.not_to raise_error + end + end + + end + + describe '#pluck' do + it 'can be called without failing' do + ch = GRPC::CompletionQueue.new + tag = Object.new + expect { ch.pluck(tag, 3) }.not_to raise_error + end + + it 'can be called with the time constants' do + ch = GRPC::CompletionQueue.new + # don't use INFINITE_FUTURE, as there we have no events. + non_blocking_consts = [:ZERO, :INFINITE_PAST] + m = GRPC::TimeConsts + tag = Object.new + non_blocking_consts.each do |c| + a_time = m.const_get(c) + expect { ch.pluck(tag, a_time) }.not_to raise_error + end + end + + end + + +end diff --git a/src/ruby/spec/event_spec.rb b/src/ruby/spec/event_spec.rb new file mode 100644 index 0000000000..19b9754d31 --- /dev/null +++ b/src/ruby/spec/event_spec.rb @@ -0,0 +1,54 @@ +# Copyright 2014, 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' + +describe GRPC::CompletionType do + + before(:each) do + @known_types = { + :QUEUE_SHUTDOWN => 0, + :READ => 1, + :INVOKE_ACCEPTED => 2, + :WRITE_ACCEPTED => 3, + :FINISH_ACCEPTED => 4, + :CLIENT_METADATA_READ => 5, + :FINISHED => 6, + :SERVER_RPC_NEW => 7, + :RESERVED => 8 + } + end + + it 'should have all the known types' do + mod = GRPC::CompletionType + expect(Hash[mod.constants.collect { |c| [c, mod.const_get(c)] }]) + .to eq(@known_types) + end + +end diff --git a/src/ruby/spec/generic/active_call_spec.rb b/src/ruby/spec/generic/active_call_spec.rb new file mode 100644 index 0000000000..872625ccf0 --- /dev/null +++ b/src/ruby/spec/generic/active_call_spec.rb @@ -0,0 +1,321 @@ +# Copyright 2014, 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' +require 'grpc/generic/active_call' +require_relative '../port_picker' + +module GRPC + + describe ActiveCall do + + before(:each) do + @pass_through = Proc.new { |x| x } + @server_tag = Object.new + @server_finished_tag = Object.new + @tag = Object.new + + @client_queue = CompletionQueue.new + @server_queue = CompletionQueue.new + port = find_unused_tcp_port + host = "localhost:#{port}" + @server = GRPC::Server.new(@server_queue, nil) + @server.add_http2_port(host) + @server.start + @ch = GRPC::Channel.new(host, nil) + end + + after(:each) do + @server.close + end + + describe 'restricted view methods' do + before(:each) do + call = make_test_call + finished_tag = ActiveCall.client_start_invoke(call, @client_queue, + deadline) + @client_call = ActiveCall.new(call, @client_queue, @pass_through, + @pass_through, deadline, + finished_tag: finished_tag) + end + + describe '#multi_req_view' do + it 'exposes a fixed subset of the ActiveCall methods' do + want = ['cancelled', 'deadline', 'each_remote_read', 'shutdown'] + v = @client_call.multi_req_view + want.each do |w| + expect(v.methods.include?(w)) + end + end + end + + describe '#single_req_view' do + it 'exposes a fixed subset of the ActiveCall methods' do + want = ['cancelled', 'deadline', 'shutdown'] + v = @client_call.single_req_view + want.each do |w| + expect(v.methods.include?(w)) + end + end + end + end + + describe '#remote_send' do + it 'allows a client to send a payload to the server' do + call = make_test_call + finished_tag = ActiveCall.client_start_invoke(call, @client_queue, + deadline) + @client_call = ActiveCall.new(call, @client_queue, @pass_through, + @pass_through, deadline, + finished_tag: finished_tag) + msg = 'message is a string' + @client_call.remote_send(msg) + + # check that server rpc new was received + @server.request_call(@server_tag) + ev = @server_queue.next(deadline) + expect(ev.type).to be(CompletionType::SERVER_RPC_NEW) + expect(ev.call).to be_a(Call) + expect(ev.tag).to be(@server_tag) + + # Accept the call, and verify that the server reads the response ok. + ev.call.accept(@client_queue, @server_tag) + server_call = ActiveCall.new(ev.call, @client_queue, @pass_through, + @pass_through, deadline) + expect(server_call.remote_read).to eq(msg) + end + + it 'marshals the payload using the marshal func' do + call = make_test_call + finished_tag = ActiveCall.client_start_invoke(call, @client_queue, + deadline) + marshal = Proc.new { |x| 'marshalled:' + x } + client_call = ActiveCall.new(call, @client_queue, marshal, + @pass_through, deadline, + finished_tag: finished_tag) + msg = 'message is a string' + client_call.remote_send(msg) + + # confirm that the message was marshalled + @server.request_call(@server_tag) + ev = @server_queue.next(deadline) + ev.call.accept(@client_queue, @server_tag) + server_call = ActiveCall.new(ev.call, @client_queue, @pass_through, + @pass_through, deadline) + expect(server_call.remote_read).to eq('marshalled:' + msg) + end + + end + + describe '#remote_read' do + it 'reads the response sent by a server' do + call, pass_through = make_test_call, Proc.new { |x| x } + finished_tag = ActiveCall.client_start_invoke(call, @client_queue, + deadline) + client_call = ActiveCall.new(call, @client_queue, @pass_through, + @pass_through, deadline, + finished_tag: finished_tag) + msg = 'message is a string' + client_call.remote_send(msg) + server_call = expect_server_to_receive(msg) + server_call.remote_send('server_response') + expect(client_call.remote_read).to eq('server_response') + end + + it 'get a nil msg before a status when an OK status is sent' do + call, pass_through = make_test_call, Proc.new { |x| x } + finished_tag = ActiveCall.client_start_invoke(call, @client_queue, + deadline) + client_call = ActiveCall.new(call, @client_queue, @pass_through, + @pass_through, deadline, + finished_tag: finished_tag) + msg = 'message is a string' + client_call.remote_send(msg) + client_call.writes_done(false) + server_call = expect_server_to_receive(msg) + server_call.remote_send('server_response') + server_call.send_status(StatusCodes::OK, 'OK') + expect(client_call.remote_read).to eq('server_response') + res = client_call.remote_read + expect(res).to be_nil + end + + + it 'unmarshals the response using the unmarshal func' do + call = make_test_call + finished_tag = ActiveCall.client_start_invoke(call, @client_queue, + deadline) + unmarshal = Proc.new { |x| 'unmarshalled:' + x } + client_call = ActiveCall.new(call, @client_queue, @pass_through, + unmarshal, deadline, + finished_tag: finished_tag) + + # confirm the client receives the unmarshalled message + msg = 'message is a string' + client_call.remote_send(msg) + server_call = expect_server_to_receive(msg) + server_call.remote_send('server_response') + expect(client_call.remote_read).to eq('unmarshalled:server_response') + end + + end + + describe '#each_remote_read' do + it 'creates an Enumerator' do + call = make_test_call + client_call = ActiveCall.new(call, @client_queue, @pass_through, + @pass_through, deadline) + expect(client_call.each_remote_read).to be_a(Enumerator) + end + + it 'the returns an enumerator that can read n responses' do + call = make_test_call + finished_tag = ActiveCall.client_start_invoke(call, @client_queue, + deadline) + client_call = ActiveCall.new(call, @client_queue, @pass_through, + @pass_through, deadline, + finished_tag: finished_tag) + msg = 'message is 4a string' + reply = 'server_response' + client_call.remote_send(msg) + server_call = expect_server_to_receive(msg) + e = client_call.each_remote_read + n = 3 # arbitrary value > 1 + n.times do + server_call.remote_send(reply) + expect(e.next).to eq(reply) + end + end + + it 'the returns an enumerator that stops after an OK Status' do + call = make_test_call + finished_tag = ActiveCall.client_start_invoke(call, @client_queue, + deadline) + client_call = ActiveCall.new(call, @client_queue, @pass_through, + @pass_through, deadline, + finished_tag: finished_tag) + msg = 'message is a string' + reply = 'server_response' + client_call.remote_send(msg) + client_call.writes_done(false) + server_call = expect_server_to_receive(msg) + e = client_call.each_remote_read + n = 3 # arbitrary value > 1 + n.times do + server_call.remote_send(reply) + expect(e.next).to eq(reply) + end + server_call.send_status(StatusCodes::OK, 'OK') + expect { e.next }.to raise_error(StopIteration) + end + + end + + describe '#writes_done' do + it 'finishes ok if the server sends a status response' do + call = make_test_call + finished_tag = ActiveCall.client_start_invoke(call, @client_queue, + deadline) + client_call = ActiveCall.new(call, @client_queue, @pass_through, + @pass_through, deadline, + finished_tag: finished_tag) + msg = 'message is a string' + client_call.remote_send(msg) + expect { client_call.writes_done(false) }.to_not raise_error + server_call = expect_server_to_receive(msg) + server_call.remote_send('server_response') + expect(client_call.remote_read).to eq('server_response') + server_call.send_status(StatusCodes::OK, 'status code is OK') + expect { server_call.finished }.to_not raise_error + expect { client_call.finished }.to_not raise_error + end + + it 'finishes ok if the server sends an early status response' do + call = make_test_call + finished_tag = ActiveCall.client_start_invoke(call, @client_queue, + deadline) + client_call = ActiveCall.new(call, @client_queue, @pass_through, + @pass_through, deadline, + finished_tag: finished_tag) + msg = 'message is a string' + client_call.remote_send(msg) + server_call = expect_server_to_receive(msg) + server_call.remote_send('server_response') + server_call.send_status(StatusCodes::OK, 'status code is OK') + expect(client_call.remote_read).to eq('server_response') + expect { client_call.writes_done(false) }.to_not raise_error + expect { server_call.finished }.to_not raise_error + expect { client_call.finished }.to_not raise_error + end + + it 'finishes ok if writes_done is true' do + call = make_test_call + finished_tag = ActiveCall.client_start_invoke(call, @client_queue, + deadline) + client_call = ActiveCall.new(call, @client_queue, @pass_through, + @pass_through, deadline, + finished_tag: finished_tag) + msg = 'message is a string' + client_call.remote_send(msg) + server_call = expect_server_to_receive(msg) + server_call.remote_send('server_response') + server_call.send_status(StatusCodes::OK, 'status code is OK') + expect(client_call.remote_read).to eq('server_response') + expect { client_call.writes_done(true) }.to_not raise_error + expect { server_call.finished }.to_not raise_error + end + + end + + def expect_server_to_receive(sent_text) + c = expect_server_to_be_invoked + expect(c.remote_read).to eq(sent_text) + c + end + + def expect_server_to_be_invoked() + @server.request_call(@server_tag) + ev = @server_queue.next(deadline) + ev.call.accept(@client_queue, @server_finished_tag) + ActiveCall.new(ev.call, @client_queue, @pass_through, + @pass_through, deadline, + finished_tag: @server_finished_tag) + end + + def make_test_call + @ch.create_call('dummy_method', 'dummy_host', deadline) + end + + def deadline + Time.now + 0.25 # in 0.25 seconds; arbitrary + end + + end + +end diff --git a/src/ruby/spec/generic/client_stub_spec.rb b/src/ruby/spec/generic/client_stub_spec.rb new file mode 100644 index 0000000000..c8dee74563 --- /dev/null +++ b/src/ruby/spec/generic/client_stub_spec.rb @@ -0,0 +1,484 @@ +# Copyright 2014, 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' +require 'grpc/generic/active_call' +require 'grpc/generic/client_stub' +require 'xray/thread_dump_signal_handler' +require_relative '../port_picker' + +NOOP = Proc.new { |x| x } + +def wakey_thread(&blk) + awake_mutex, awake_cond = Mutex.new, ConditionVariable.new + t = Thread.new do + blk.call(awake_mutex, awake_cond) + end + awake_mutex.synchronize { awake_cond.wait(awake_mutex) } + t +end + + +include GRPC::StatusCodes + +describe 'ClientStub' do + BadStatus = GRPC::BadStatus + TimeConsts = GRPC::TimeConsts + + before(:each) do + Thread.abort_on_exception = true + @server = nil + @method = 'an_rpc_method' + @pass = OK + @fail = INTERNAL + @cq = GRPC::CompletionQueue.new + end + + after(:each) do + @server.close unless @server.nil? + end + + describe '#new' do + + it 'can be created from a host and args' do + host = new_test_host + opts = {:a_channel_arg => 'an_arg'} + blk = Proc.new do + GRPC::ClientStub.new(host, @cq, **opts) + end + expect(&blk).not_to raise_error + end + + it 'can be created with a default deadline' do + host = new_test_host + opts = {:a_channel_arg => 'an_arg', :deadline => 5} + blk = Proc.new do + GRPC::ClientStub.new(host, @cq, **opts) + end + expect(&blk).not_to raise_error + end + + it 'can be created with an channel override' do + host = new_test_host + opts = {:a_channel_arg => 'an_arg', :channel_override => @ch} + blk = Proc.new do + GRPC::ClientStub.new(host, @cq, **opts) + end + expect(&blk).not_to raise_error + end + + it 'cannot be created with a bad channel override' do + host = new_test_host + blk = Proc.new do + opts = {:a_channel_arg => 'an_arg', :channel_override => Object.new} + GRPC::ClientStub.new(host, @cq, **opts) + end + expect(&blk).to raise_error + end + + end + + describe '#request_response' do + before(:each) do + @sent_msg, @resp = 'a_msg', 'a_reply' + end + + describe 'without a call operation' do + + it 'should send a request to/receive a_reply from a server' do + host = new_test_host + th = run_request_response(host, @sent_msg, @resp, @pass) + stub = GRPC::ClientStub.new(host, @cq) + resp = stub.request_response(@method, @sent_msg, NOOP, NOOP) + expect(resp).to eq(@resp) + th.join + end + + it 'should send a request when configured using an override channel' do + alt_host = new_test_host + th = run_request_response(alt_host, @sent_msg, @resp, @pass) + ch = GRPC::Channel.new(alt_host, nil) + stub = GRPC::ClientStub.new('ignored-host', @cq, + channel_override:ch) + resp = stub.request_response(@method, @sent_msg, NOOP, NOOP) + expect(resp).to eq(@resp) + th.join + end + + it 'should raise an error if the status is not OK' do + host = new_test_host + th = run_request_response(host, @sent_msg, @resp, @fail) + stub = GRPC::ClientStub.new(host, @cq) + blk = Proc.new do + stub.request_response(@method, @sent_msg, NOOP, NOOP) + end + expect(&blk).to raise_error(BadStatus) + th.join + end + + end + + describe 'via a call operation' do + + it 'should send a request to/receive a_reply from a server' do + host = new_test_host + th = run_request_response(host, @sent_msg, @resp, @pass) + stub = GRPC::ClientStub.new(host, @cq) + op = stub.request_response(@method, @sent_msg, NOOP, NOOP, + return_op:true) + expect(op).to be_a(GRPC::ActiveCall::Operation) + resp = op.execute() + expect(resp).to eq(@resp) + th.join + end + + it 'should raise an error if the status is not OK' do + host = new_test_host + th = run_request_response(host, @sent_msg, @resp, @fail) + stub = GRPC::ClientStub.new(host, @cq) + op = stub.request_response(@method, @sent_msg, NOOP, NOOP, + return_op:true) + expect(op).to be_a(GRPC::ActiveCall::Operation) + blk = Proc.new do + op.execute() + end + expect(&blk).to raise_error(BadStatus) + th.join + end + + end + + end + + describe '#client_streamer' do + + before(:each) do + @sent_msgs = Array.new(3) { |i| 'msg_' + (i+1).to_s } + @resp = 'a_reply' + end + + describe 'without a call operation' do + + it 'should send requests to/receive a reply from a server' do + host = new_test_host + th = run_client_streamer(host, @sent_msgs, @resp, @pass) + stub = GRPC::ClientStub.new(host, @cq) + resp = stub.client_streamer(@method, @sent_msgs, NOOP, NOOP) + expect(resp).to eq(@resp) + th.join + end + + it 'should raise an error if the status is not ok' do + host = new_test_host + th = run_client_streamer(host, @sent_msgs, @resp, @fail) + stub = GRPC::ClientStub.new(host, @cq) + blk = Proc.new do + stub.client_streamer(@method, @sent_msgs, NOOP, NOOP) + end + expect(&blk).to raise_error(BadStatus) + th.join + end + + end + + describe 'via a call operation' do + + it 'should send requests to/receive a reply from a server' do + host = new_test_host + th = run_client_streamer(host, @sent_msgs, @resp, @pass) + stub = GRPC::ClientStub.new(host, @cq) + op = stub.client_streamer(@method, @sent_msgs, NOOP, NOOP, + return_op:true) + expect(op).to be_a(GRPC::ActiveCall::Operation) + resp = op.execute() + expect(resp).to eq(@resp) + th.join + end + + it 'should raise an error if the status is not ok' do + host = new_test_host + th = run_client_streamer(host, @sent_msgs, @resp, @fail) + stub = GRPC::ClientStub.new(host, @cq) + op = stub.client_streamer(@method, @sent_msgs, NOOP, NOOP, + return_op:true) + expect(op).to be_a(GRPC::ActiveCall::Operation) + blk = Proc.new do + op.execute() + end + expect(&blk).to raise_error(BadStatus) + th.join + end + + end + + end + + describe '#server_streamer' do + + before(:each) do + @sent_msg = 'a_msg' + @replys = Array.new(3) { |i| 'reply_' + (i+1).to_s } + end + + describe 'without a call operation' do + + it 'should send a request to/receive replies from a server' do + host = new_test_host + th = run_server_streamer(host, @sent_msg, @replys, @pass) + stub = GRPC::ClientStub.new(host, @cq) + e = stub.server_streamer(@method, @sent_msg, NOOP, NOOP) + expect(e).to be_a(Enumerator) + expect(e.collect { |r| r }).to eq(@replys) + th.join + end + + it 'should raise an error if the status is not ok' do + host = new_test_host + th = run_server_streamer(host, @sent_msg, @replys, @fail) + stub = GRPC::ClientStub.new(host, @cq) + e = stub.server_streamer(@method, @sent_msg, NOOP, NOOP) + expect(e).to be_a(Enumerator) + expect { e.collect { |r| r } }.to raise_error(BadStatus) + th.join + end + + end + + describe 'via a call operation' do + + it 'should send a request to/receive replies from a server' do + host = new_test_host + th = run_server_streamer(host, @sent_msg, @replys, @pass) + stub = GRPC::ClientStub.new(host, @cq) + op = stub.server_streamer(@method, @sent_msg, NOOP, NOOP, + return_op:true) + expect(op).to be_a(GRPC::ActiveCall::Operation) + e = op.execute() + expect(e).to be_a(Enumerator) + th.join + end + + it 'should raise an error if the status is not ok' do + host = new_test_host + th = run_server_streamer(host, @sent_msg, @replys, @fail) + stub = GRPC::ClientStub.new(host, @cq) + op = stub.server_streamer(@method, @sent_msg, NOOP, NOOP, + return_op:true) + expect(op).to be_a(GRPC::ActiveCall::Operation) + e = op.execute() + expect(e).to be_a(Enumerator) + expect { e.collect { |r| r } }.to raise_error(BadStatus) + th.join + end + + end + + end + + describe '#bidi_streamer' do + before(:each) do + @sent_msgs = Array.new(3) { |i| 'msg_' + (i+1).to_s } + @replys = Array.new(3) { |i| 'reply_' + (i+1).to_s } + end + + describe 'without a call operation' do + + it 'supports a simple scenario with all requests sent first' do + host = new_test_host + th = run_bidi_streamer_handle_inputs_first(host, @sent_msgs, @replys, + @pass) + stub = GRPC::ClientStub.new(host, @cq) + e = stub.bidi_streamer(@method, @sent_msgs, NOOP, NOOP) + expect(e).to be_a(Enumerator) + expect(e.collect { |r| r }).to eq(@replys) + th.join + end + + it 'supports a simple scenario with a client-initiated ping pong' do + host = new_test_host + th = run_bidi_streamer_echo_ping_pong(host, @sent_msgs, @pass, true) + stub = GRPC::ClientStub.new(host, @cq) + e = stub.bidi_streamer(@method, @sent_msgs, NOOP, NOOP) + expect(e).to be_a(Enumerator) + expect(e.collect { |r| r }).to eq(@sent_msgs) + th.join + end + + # disabled because an unresolved wire-protocol implementation feature + # + # - servers should be able initiate messaging, however, as it stand + # servers don't know if all the client metadata has been sent until + # they receive a message from the client. Without receiving all the + # metadata, the server does not accept the call, so this test hangs. + xit 'supports a simple scenario with a server-initiated ping pong' do + host = new_test_host + th = run_bidi_streamer_echo_ping_pong(host, @sent_msgs, @pass, false) + stub = GRPC::ClientStub.new(host, @cq) + e = stub.bidi_streamer(@method, @sent_msgs, NOOP, NOOP) + expect(e).to be_a(Enumerator) + expect(e.collect { |r| r }).to eq(@sent_msgs) + th.join + end + + end + + describe 'via a call operation' do + + it 'supports a simple scenario with all requests sent first' do + host = new_test_host + th = run_bidi_streamer_handle_inputs_first(host, @sent_msgs, @replys, + @pass) + stub = GRPC::ClientStub.new(host, @cq) + op = stub.bidi_streamer(@method, @sent_msgs, NOOP, NOOP, + return_op:true) + expect(op).to be_a(GRPC::ActiveCall::Operation) + e = op.execute + expect(e).to be_a(Enumerator) + expect(e.collect { |r| r }).to eq(@replys) + th.join + end + + it 'supports a simple scenario with a client-initiated ping pong' do + host = new_test_host + th = run_bidi_streamer_echo_ping_pong(host, @sent_msgs, @pass, true) + stub = GRPC::ClientStub.new(host, @cq) + op = stub.bidi_streamer(@method, @sent_msgs, NOOP, NOOP, + return_op:true) + expect(op).to be_a(GRPC::ActiveCall::Operation) + e = op.execute + expect(e).to be_a(Enumerator) + expect(e.collect { |r| r }).to eq(@sent_msgs) + th.join + end + + # disabled because an unresolved wire-protocol implementation feature + # + # - servers should be able initiate messaging, however, as it stand + # servers don't know if all the client metadata has been sent until + # they receive a message from the client. Without receiving all the + # metadata, the server does not accept the call, so this test hangs. + xit 'supports a simple scenario with a server-initiated ping pong' do + th = run_bidi_streamer_echo_ping_pong(host, @sent_msgs, @pass, false) + stub = GRPC::ClientStub.new(host, @cq) + op = stub.bidi_streamer(@method, @sent_msgs, NOOP, NOOP, + return_op:true) + expect(op).to be_a(GRPC::ActiveCall::Operation) + e = op.execute + expect(e).to be_a(Enumerator) + expect(e.collect { |r| r }).to eq(@sent_msgs) + th.join + end + + end + + end + + def run_server_streamer(hostname, expected_input, replys, status) + wakey_thread do |mtx, cnd| + c = expect_server_to_be_invoked(hostname, mtx, cnd) + expect(c.remote_read).to eq(expected_input) + replys.each { |r| c.remote_send(r) } + c.send_status(status, status == @pass ? 'OK' : 'NOK', true) + end + end + + def run_bidi_streamer_handle_inputs_first(hostname, expected_inputs, replys, + status) + wakey_thread do |mtx, cnd| + c = expect_server_to_be_invoked(hostname, mtx, cnd) + expected_inputs.each { |i| expect(c.remote_read).to eq(i) } + replys.each { |r| c.remote_send(r) } + c.send_status(status, status == @pass ? 'OK' : 'NOK', true) + end + end + + def run_bidi_streamer_echo_ping_pong(hostname, expected_inputs, status, + client_starts) + wakey_thread do |mtx, cnd| + c = expect_server_to_be_invoked(hostname, mtx, cnd) + expected_inputs.each do |i| + if client_starts + expect(c.remote_read).to eq(i) + c.remote_send(i) + else + c.remote_send(i) + expect(c.remote_read).to eq(i) + end + end + c.send_status(status, status == @pass ? 'OK' : 'NOK', true) + end + end + + def run_client_streamer(hostname, expected_inputs, resp, status) + wakey_thread do |mtx, cnd| + c = expect_server_to_be_invoked(hostname, mtx, cnd) + expected_inputs.each { |i| expect(c.remote_read).to eq(i) } + c.remote_send(resp) + c.send_status(status, status == @pass ? 'OK' : 'NOK', true) + end + end + + def run_request_response(hostname, expected_input, resp, status) + wakey_thread do |mtx, cnd| + c = expect_server_to_be_invoked(hostname, mtx, cnd) + expect(c.remote_read).to eq(expected_input) + c.remote_send(resp) + c.send_status(status, status == @pass ? 'OK' : 'NOK', true) + end + end + + def start_test_server(hostname, awake_mutex, awake_cond) + server_queue = GRPC::CompletionQueue.new + @server = GRPC::Server.new(server_queue, nil) + @server.add_http2_port(hostname) + @server.start + @server_tag = Object.new + @server.request_call(@server_tag) + awake_mutex.synchronize { awake_cond.signal } + server_queue + end + + def expect_server_to_be_invoked(hostname, awake_mutex, awake_cond) + server_queue = start_test_server(hostname, awake_mutex, awake_cond) + test_deadline = Time.now + 10 # fail tests after 10 seconds + ev = server_queue.pluck(@server_tag, TimeConsts::INFINITE_FUTURE) + raise OutOfTime if ev.nil? + finished_tag = Object.new + ev.call.accept(server_queue, finished_tag) + GRPC::ActiveCall.new(ev.call, server_queue, NOOP, + NOOP, TimeConsts::INFINITE_FUTURE, + finished_tag: finished_tag) + end + + def new_test_host + port = find_unused_tcp_port + "localhost:#{port}" + end + +end diff --git a/src/ruby/spec/generic/rpc_desc_spec.rb b/src/ruby/spec/generic/rpc_desc_spec.rb new file mode 100644 index 0000000000..141fb1187d --- /dev/null +++ b/src/ruby/spec/generic/rpc_desc_spec.rb @@ -0,0 +1,380 @@ +# Copyright 2014, 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' +require 'grpc/generic/rpc_desc' + + +describe GRPC::RpcDesc do + + RpcDesc = GRPC::RpcDesc + Stream = RpcDesc::Stream + OK = GRPC::StatusCodes::OK + UNKNOWN = GRPC::StatusCodes::UNKNOWN + + before(:each) do + @request_response = RpcDesc.new('rr', Object.new, Object.new, 'encode', + 'decode') + @client_streamer = RpcDesc.new('cs', Stream.new(Object.new), Object.new, + 'encode', 'decode') + @server_streamer = RpcDesc.new('ss', Object.new, Stream.new(Object.new), + 'encode', 'decode') + @bidi_streamer = RpcDesc.new('ss', Stream.new(Object.new), + Stream.new(Object.new), 'encode', 'decode') + @bs_code = GRPC::StatusCodes::INTERNAL + @no_reason = 'no reason given' + @ok_response = Object.new + end + + describe '#run_server_method' do + + describe 'for request responses' do + before(:each) do + @call = double('active_call') + allow(@call).to receive(:single_req_view).and_return(@call) + allow(@call).to receive(:gc) + end + + it 'sends the specified status if BadStatus is raised' do + expect(@call).to receive(:remote_read).once.and_return(Object.new) + expect(@call).to receive(:send_status).once.with(@bs_code, 'NOK') + @request_response.run_server_method(@call, method(:bad_status)) + end + + 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) + @request_response.run_server_method(@call, method(:other_error)) + end + + it 'absorbs EventError with no further action' do + expect(@call).to receive(:remote_read).once.and_raise(GRPC::EventError) + blk = Proc.new do + @request_response.run_server_method(@call, method(:fake_reqresp)) + end + expect(&blk).to_not raise_error + end + + it 'absorbs CallError with no further action' do + expect(@call).to receive(:remote_read).once.and_raise(GRPC::CallError) + blk = Proc.new do + @request_response.run_server_method(@call, method(:fake_reqresp)) + end + expect(&blk).to_not raise_error + end + + it 'sends a response and closes the stream if there no errors' do + req = Object.new + expect(@call).to receive(:remote_read).once.and_return(req) + expect(@call).to receive(:remote_send).once.with(@ok_response) + expect(@call).to receive(:send_status).once.with(OK, 'OK') + expect(@call).to receive(:finished).once + @request_response.run_server_method(@call, method(:fake_reqresp)) + end + + end + + describe 'for client streamers' do + before(:each) do + @call = double('active_call') + allow(@call).to receive(:multi_req_view).and_return(@call) + allow(@call).to receive(:gc) + end + + it 'sends the specified status if BadStatus is raised' do + expect(@call).to receive(:send_status).once.with(@bs_code, 'NOK') + @client_streamer.run_server_method(@call, method(:bad_status_alt)) + end + + it 'sends status UNKNOWN if other StandardErrors are raised' do + expect(@call).to receive(:send_status) .once.with(UNKNOWN, @no_reason) + @client_streamer.run_server_method(@call, method(:other_error_alt)) + end + + it 'absorbs EventError with no further action' do + expect(@call).to receive(:remote_send).once.and_raise(GRPC::EventError) + blk = Proc.new do + @client_streamer.run_server_method(@call, method(:fake_clstream)) + end + expect(&blk).to_not raise_error + end + + it 'absorbs CallError with no further action' do + expect(@call).to receive(:remote_send).once.and_raise(GRPC::CallError) + blk = Proc.new do + @client_streamer.run_server_method(@call, method(:fake_clstream)) + end + expect(&blk).to_not raise_error + end + + it 'sends a response and closes the stream if there no errors' do + req = Object.new + expect(@call).to receive(:remote_send).once.with(@ok_response) + expect(@call).to receive(:send_status).once.with(OK, 'OK') + expect(@call).to receive(:finished).once + @client_streamer.run_server_method(@call, method(:fake_clstream)) + end + + end + + describe 'for server streaming' do + before(:each) do + @call = double('active_call') + allow(@call).to receive(:single_req_view).and_return(@call) + allow(@call).to receive(:gc) + end + + it 'sends the specified status if BadStatus is raised' do + expect(@call).to receive(:remote_read).once.and_return(Object.new) + expect(@call).to receive(:send_status).once.with(@bs_code, 'NOK') + @server_streamer.run_server_method(@call, method(:bad_status)) + end + + 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) + @server_streamer.run_server_method(@call, method(:other_error)) + end + + it 'absorbs EventError with no further action' do + expect(@call).to receive(:remote_read).once.and_raise(GRPC::EventError) + blk = Proc.new do + @server_streamer.run_server_method(@call, method(:fake_svstream)) + end + expect(&blk).to_not raise_error + end + + it 'absorbs CallError with no further action' do + expect(@call).to receive(:remote_read).once.and_raise(GRPC::CallError) + blk = Proc.new do + @server_streamer.run_server_method(@call, method(:fake_svstream)) + end + expect(&blk).to_not raise_error + end + + it 'sends a response and closes the stream if there no errors' do + req = Object.new + expect(@call).to receive(:remote_read).once.and_return(req) + expect(@call).to receive(:remote_send).twice.with(@ok_response) + expect(@call).to receive(:send_status).once.with(OK, 'OK') + expect(@call).to receive(:finished).once + @server_streamer.run_server_method(@call, method(:fake_svstream)) + end + + end + + describe 'for bidi streamers' do + before(:each) do + @call = double('active_call') + enq_th, rwl_th = double('enqueue_th'), ('read_write_loop_th') + allow(enq_th).to receive(:join) + allow(rwl_th).to receive(:join) + allow(@call).to receive(:gc) + end + + it 'sends the specified status if BadStatus is raised' do + e = GRPC::BadStatus.new(@bs_code, 'NOK') + expect(@call).to receive(:run_server_bidi).and_raise(e) + expect(@call).to receive(:send_status).once.with(@bs_code, 'NOK') + @bidi_streamer.run_server_method(@call, method(:bad_status_alt)) + end + + it 'sends status UNKNOWN if other StandardErrors are raised' do + expect(@call).to receive(:run_server_bidi).and_raise(StandardError) + expect(@call).to receive(:send_status).once.with(UNKNOWN, @no_reason) + @bidi_streamer.run_server_method(@call, method(:other_error_alt)) + end + + it 'closes the stream if there no errors' do + req = Object.new + expect(@call).to receive(:run_server_bidi) + expect(@call).to receive(:send_status).once.with(OK, 'OK') + expect(@call).to receive(:finished).once + @bidi_streamer.run_server_method(@call, method(:fake_bidistream)) + end + + end + + end + + describe '#assert_arity_matches' do + def no_arg + end + + def fake_clstream(arg) + end + + def fake_svstream(arg1, arg2) + end + + it 'raises when a request_response does not have 2 args' do + [:fake_clstream, :no_arg].each do |mth| + blk = Proc.new do + @request_response.assert_arity_matches(method(mth)) + end + expect(&blk).to raise_error + end + end + + it 'passes when a request_response has 2 args' do + blk = Proc.new do + @request_response.assert_arity_matches(method(:fake_svstream)) + end + expect(&blk).to_not raise_error + end + + it 'raises when a server_streamer does not have 2 args' do + [:fake_clstream, :no_arg].each do |mth| + blk = Proc.new do + @server_streamer.assert_arity_matches(method(mth)) + end + expect(&blk).to raise_error + end + end + + it 'passes when a server_streamer has 2 args' do + blk = Proc.new do + @server_streamer.assert_arity_matches(method(:fake_svstream)) + end + expect(&blk).to_not raise_error + end + + it 'raises when a client streamer does not have 1 arg' do + [:fake_svstream, :no_arg].each do |mth| + blk = Proc.new do + @client_streamer.assert_arity_matches(method(mth)) + end + expect(&blk).to raise_error + end + end + + it 'passes when a client_streamer has 1 arg' do + blk = Proc.new do + @client_streamer.assert_arity_matches(method(:fake_clstream)) + end + expect(&blk).to_not raise_error + end + + + it 'raises when a bidi streamer does not have 1 arg' do + [:fake_svstream, :no_arg].each do |mth| + blk = Proc.new do + @bidi_streamer.assert_arity_matches(method(mth)) + end + expect(&blk).to raise_error + end + end + + it 'passes when a bidi streamer has 1 arg' do + blk = Proc.new do + @bidi_streamer.assert_arity_matches(method(:fake_clstream)) + end + expect(&blk).to_not raise_error + end + + end + + describe '#is_request_response?' do + + it 'is true only input and output are both not Streams' do + expect(@request_response.is_request_response?).to be(true) + expect(@client_streamer.is_request_response?).to be(false) + expect(@bidi_streamer.is_request_response?).to be(false) + expect(@server_streamer.is_request_response?).to be(false) + end + + end + + describe '#is_client_streamer?' do + + it 'is true only when input is a Stream and output is not a Stream' do + expect(@client_streamer.is_client_streamer?).to be(true) + expect(@request_response.is_client_streamer?).to be(false) + expect(@server_streamer.is_client_streamer?).to be(false) + expect(@bidi_streamer.is_client_streamer?).to be(false) + end + + end + + describe '#is_server_streamer?' do + + it 'is true only when output is a Stream and input is not a Stream' do + expect(@server_streamer.is_server_streamer?).to be(true) + expect(@client_streamer.is_server_streamer?).to be(false) + expect(@request_response.is_server_streamer?).to be(false) + expect(@bidi_streamer.is_server_streamer?).to be(false) + end + + end + + describe '#is_bidi_streamer?' do + + it 'is true only when output is a Stream and input is a Stream' do + expect(@bidi_streamer.is_bidi_streamer?).to be(true) + expect(@server_streamer.is_bidi_streamer?).to be(false) + expect(@client_streamer.is_bidi_streamer?).to be(false) + expect(@request_response.is_bidi_streamer?).to be(false) + end + + end + + def fake_reqresp(req, call) + @ok_response + end + + def fake_clstream(call) + @ok_response + end + + def fake_svstream(req, call) + [@ok_response, @ok_response] + end + + def fake_bidistream(an_array) + return an_array + end + + def bad_status(req, call) + raise GRPC::BadStatus.new(@bs_code, 'NOK') + end + + def other_error(req, call) + raise ArgumentError.new('other error') + end + + def bad_status_alt(call) + raise GRPC::BadStatus.new(@bs_code, 'NOK') + end + + def other_error_alt(call) + raise ArgumentError.new('other error') + 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..8a185df9c7 --- /dev/null +++ b/src/ruby/spec/generic/rpc_server_pool_spec.rb @@ -0,0 +1,153 @@ +# Copyright 2014, 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' +require 'grpc/generic/rpc_server' +require 'xray/thread_dump_signal_handler' + +Pool = GRPC::RpcServer::Pool + +describe Pool do + + 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 '#jobs_waiting' do + + it 'at start, it is zero' do + p = Pool.new(1) + expect(p.jobs_waiting).to be(0) + end + + it 'it increases, with each scheduled job if the pool is not running' do + p = Pool.new(1) + job = Proc.new { } + expect(p.jobs_waiting).to be(0) + 5.times do |i| + p.schedule(&job) + expect(p.jobs_waiting).to be(i + 1) + end + + end + + it 'it decreases as jobs are run' do + p = Pool.new(1) + job = Proc.new { } + expect(p.jobs_waiting).to be(0) + 3.times do |i| + p.schedule(&job) + end + p.start + sleep 2 + expect(p.jobs_waiting).to be(0) + end + + end + + describe '#schedule' do + + it 'throws if the pool is already stopped' do + p = Pool.new(1) + p.stop() + job = Proc.new { } + expect { p.schedule(&job) }.to 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.new { 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.new 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 pre-scheduled jobs' do + p = Pool.new(2) + o, q = Object.new, Queue.new + n = 5 # arbitrary + n.times { p.schedule(o, &q.method(:push)) } + p.start + n.times { expect(q.pop).to be(o) } + p.stop + end + + it 'runs jobs as they are scheduled ' do + p = Pool.new(2) + 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 new file mode 100644 index 0000000000..4e7379bc45 --- /dev/null +++ b/src/ruby/spec/generic/rpc_server_spec.rb @@ -0,0 +1,391 @@ +# Copyright 2014, 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' +require 'grpc/generic/active_call' +require 'grpc/generic/client_stub' +require 'grpc/generic/rpc_server' +require 'grpc/generic/service' +require 'xray/thread_dump_signal_handler' +require_relative '../port_picker' + +class EchoMsg + def marshal + '' + end + + def self.unmarshal(o) + EchoMsg.new + end +end + +class EmptyService + include GRPC::GenericService +end + +class NoRpcImplementation + include GRPC::GenericService + rpc :an_rpc, EchoMsg, EchoMsg +end + +class EchoService + include GRPC::GenericService + rpc :an_rpc, EchoMsg, EchoMsg + + def initialize(default_var='ignored') + end + + def an_rpc(req, call) + logger.info('echo service received a request') + req + end +end + +EchoStub = EchoService.rpc_stub_class + +class SlowService + include GRPC::GenericService + rpc :an_rpc, EchoMsg, EchoMsg + + def initialize(default_var='ignored') + end + + def an_rpc(req, call) + delay = 0.25 + logger.info("starting a slow #{delay} rpc") + sleep delay + req # send back the req as the response + end +end + +SlowStub = SlowService.rpc_stub_class + +module GRPC + + describe RpcServer do + + before(:each) do + @method = 'an_rpc_method' + @pass = 0 + @fail = 1 + @noop = Proc.new { |x| x } + + @server_queue = CompletionQueue.new + port = find_unused_tcp_port + @host = "localhost:#{port}" + @server = GRPC::Server.new(@server_queue, nil) + @server.add_http2_port(@host) + @ch = GRPC::Channel.new(@host, nil) + end + + after(:each) do + @server.close + end + + describe '#new' do + + it 'can be created with just some args' do + opts = {:a_channel_arg => 'an_arg'} + blk = Proc.new do + RpcServer.new(**opts) + end + expect(&blk).not_to raise_error + end + + it 'can be created with a default deadline' do + opts = {:a_channel_arg => 'an_arg', :deadline => 5} + blk = Proc.new do + RpcServer.new(**opts) + end + expect(&blk).not_to raise_error + end + + it 'can be created with a completion queue override' do + opts = { + :a_channel_arg => 'an_arg', + :completion_queue_override => @server_queue + } + blk = Proc.new do + RpcServer.new(**opts) + end + expect(&blk).not_to raise_error + end + + it 'cannot be created with a bad completion queue override' do + blk = Proc.new do + opts = { + :a_channel_arg => 'an_arg', + :completion_queue_override => Object.new + } + RpcServer.new(**opts) + end + expect(&blk).to raise_error + end + + it 'can be created with a server override' do + opts = {:a_channel_arg => 'an_arg', :server_override => @server} + blk = Proc.new do + RpcServer.new(**opts) + end + expect(&blk).not_to raise_error + end + + it 'cannot be created with a bad server override' do + blk = Proc.new do + opts = { + :a_channel_arg => 'an_arg', + :server_override => Object.new + } + RpcServer.new(**opts) + end + expect(&blk).to raise_error + end + + end + + describe '#stopped?' do + + before(:each) do + opts = {:a_channel_arg => 'an_arg', :poll_period => 1} + @srv = RpcServer.new(**opts) + end + + it 'starts out false' do + expect(@srv.stopped?).to be(false) + end + + it 'stays false after a #stop is called before #run' do + @srv.stop + expect(@srv.stopped?).to be(false) + end + + it 'stays false after the server starts running' 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' do + @srv.handle(EchoService) + t = Thread.new { @srv.run } + @srv.wait_till_running + @srv.stop + expect(@srv.stopped?).to be(true) + t.join + end + + end + + describe '#running?' do + + it 'starts out false' do + opts = {:a_channel_arg => 'an_arg', :server_override => @server} + r = RpcServer.new(**opts) + expect(r.running?).to be(false) + end + + it 'is false after run is called with no services registered' do + opts = { + :a_channel_arg => 'an_arg', + :poll_period => 1, + :server_override => @server + } + r = RpcServer.new(**opts) + r.run() + expect(r.running?).to be(false) + end + + it 'is true after run is called with a registered service' do + opts = { + :a_channel_arg => 'an_arg', + :poll_period => 1, + :server_override => @server + } + r = RpcServer.new(**opts) + 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 = {:a_channel_arg => 'an_arg', :poll_period => 1} + @srv = RpcServer.new(**@opts) + 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 the service does not define its rpc methods' do + expect { @srv.handle(NoRpcImplementation) }.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 + + before(:each) do + @client_opts = { + :channel_override => @ch + } + @marshal = EchoService.rpc_descs[:an_rpc].marshal_proc + @unmarshal = EchoService.rpc_descs[:an_rpc].unmarshal_proc(:output) + server_opts = { + :server_override => @server, + :completion_queue_override => @server_queue, + :poll_period => 1 + } + @srv = RpcServer.new(**server_opts) + end + + describe 'when running' do + + it 'should return NOT_FOUND status for requests on unknown methods' do + @srv.handle(EchoService) + t = Thread.new { @srv.run } + @srv.wait_till_running + req = EchoMsg.new + blk = Proc.new do + cq = CompletionQueue.new + stub = ClientStub.new(@host, cq, **@client_opts) + stub.request_response('/unknown', req, @marshal, @unmarshal) + end + expect(&blk).to raise_error BadStatus + @srv.stop + t.join + end + + it 'should obtain responses for multiple sequential requests' do + @srv.handle(EchoService) + t = Thread.new { @srv.run } + @srv.wait_till_running + req = EchoMsg.new + n = 5 # arbitrary + stub = EchoStub.new(@host, **@client_opts) + n.times { |x| expect(stub.an_rpc(req)).to be_a(EchoMsg) } + @srv.stop + t.join + end + + it 'should obtain responses for multiple parallel requests' do + @srv.handle(EchoService) + t = Thread.new { @srv.run } + @srv.wait_till_running + req, q = EchoMsg.new, Queue.new + n = 5 # arbitrary + threads = [] + n.times do |x| + cq = CompletionQueue.new + threads << Thread.new do + stub = EchoStub.new(@host, **@client_opts) + q << stub.an_rpc(req) + end + end + n.times { expect(q.pop).to be_a(EchoMsg) } + @srv.stop + threads.each { |t| t.join } + end + + it 'should return UNAVAILABLE status if there too many jobs' do + opts = { + :a_channel_arg => 'an_arg', + :server_override => @server, + :completion_queue_override => @server_queue, + :pool_size => 1, + :poll_period => 1, + :max_waiting_requests => 0 + } + alt_srv = RpcServer.new(**opts) + alt_srv.handle(SlowService) + t = Thread.new { alt_srv.run } + alt_srv.wait_till_running + req = EchoMsg.new + n = 5 # arbitrary, use as many to ensure the server pool is exceeded + threads = [] + _1_failed_as_unavailable = false + n.times do |x| + threads << Thread.new do + cq = CompletionQueue.new + stub = SlowStub.new(@host, **@client_opts) + begin + stub.an_rpc(req) + rescue BadStatus => e + _1_failed_as_unavailable = e.code == StatusCodes::UNAVAILABLE + end + end + end + threads.each { |t| t.join } + alt_srv.stop + expect(_1_failed_as_unavailable).to be(true) + end + + end + + end + + end + +end diff --git a/src/ruby/spec/generic/service_spec.rb b/src/ruby/spec/generic/service_spec.rb new file mode 100644 index 0000000000..4c76881bcf --- /dev/null +++ b/src/ruby/spec/generic/service_spec.rb @@ -0,0 +1,324 @@ +# Copyright 2014, 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' +require 'grpc/generic/rpc_desc' +require 'grpc/generic/service' + + +class GoodMsg + def marshal + '' + end + + def self.unmarshal(o) + GoodMsg.new + end +end + +class EncodeDecodeMsg + def encode + '' + end + + def self.decode(o) + GoodMsg.new + end +end + +GenericService = GRPC::GenericService +RpcDesc = GRPC::RpcDesc +Dsl = GenericService::Dsl + + +describe 'String#underscore' do + it 'should convert CamelCase to underscore separated' do + expect('AnRPC'.underscore).to eq('an_rpc') + expect('AMethod'.underscore).to eq('a_method') + expect('PrintHTML'.underscore).to eq('print_html') + expect('PrintHTMLBooks'.underscore).to eq('print_html_books') + end +end + +describe Dsl do + + it 'can be included in new classes' do + blk = Proc.new do + c = Class.new { include Dsl } + end + expect(&blk).to_not raise_error + end + +end + +describe GenericService do + + describe 'including it' do + + it 'adds a class method, rpc' do + c = Class.new do + include GenericService + end + expect(c.methods).to include(:rpc) + end + + it 'adds rpc descs using the added class method, #rpc' do + c = Class.new do + include GenericService + rpc :AnRpc, GoodMsg, GoodMsg + end + + expect(c.rpc_descs).to include(:AnRpc) + expect(c.rpc_descs[:AnRpc]).to be_a(RpcDesc) + end + + it 'give subclasses access to #rpc_descs' do + base = Class.new do + include GenericService + rpc :AnRpc, GoodMsg, GoodMsg + end + c = Class.new(base) do + end + expect(c.rpc_descs).to include(:AnRpc) + expect(c.rpc_descs[:AnRpc]).to be_a(RpcDesc) + end + + end + + describe '#include' do + + it 'raises if #rpc is missing an arg' do + blk = Proc.new do + Class.new do + include GenericService + rpc :AnRpc, GoodMsg + end + end + expect(&blk).to raise_error ArgumentError + + blk = Proc.new do + Class.new do + include GenericService + rpc :AnRpc + end + end + expect(&blk).to raise_error ArgumentError + end + + describe 'when #rpc args are incorrect' do + + it 'raises if an arg does not have the marshal or unmarshal methods' do + blk = Proc.new do + Class.new do + include GenericService + rpc :AnRpc, GoodMsg, Object + end + end + expect(&blk).to raise_error ArgumentError + end + + it 'raises if a type arg only has the marshal method' do + class OnlyMarshal + def marshal(o) + o + end + end + + blk = Proc.new do + Class.new do + include GenericService + rpc :AnRpc, OnlyMarshal, GoodMsg + end + end + expect(&blk).to raise_error ArgumentError + end + + it 'raises if a type arg only has the unmarshal method' do + class OnlyUnmarshal + def self.ummarshal(o) + o + end + end + blk = Proc.new do + Class.new do + include GenericService + rpc :AnRpc, GoodMsg, OnlyUnmarshal + end + end + expect(&blk).to raise_error ArgumentError + end + end + + it 'is ok for services that expect the default {un,}marshal methods' do + blk = Proc.new do + Class.new do + include GenericService + rpc :AnRpc, GoodMsg, GoodMsg + end + end + expect(&blk).not_to raise_error + end + + it 'is ok for services that override the default {un,}marshal methods' do + blk = Proc.new do + Class.new do + include GenericService + self.marshal_instance_method = :encode + self.unmarshal_class_method = :decode + rpc :AnRpc, EncodeDecodeMsg, EncodeDecodeMsg + end + end + expect(&blk).not_to raise_error + end + + end + + describe '#rpc_stub_class' do + + it 'generates a client class that defines any of the rpc methods' do + s = Class.new do + include GenericService + rpc :AnRpc, GoodMsg, GoodMsg + rpc :AServerStreamer, GoodMsg, stream(GoodMsg) + rpc :AClientStreamer, stream(GoodMsg), GoodMsg + rpc :ABidiStreamer, stream(GoodMsg), stream(GoodMsg) + end + client_class = s.rpc_stub_class + expect(client_class.instance_methods).to include(:an_rpc) + expect(client_class.instance_methods).to include(:a_server_streamer) + expect(client_class.instance_methods).to include(:a_client_streamer) + expect(client_class.instance_methods).to include(:a_bidi_streamer) + end + + describe 'the generated instances' do + + it 'can be instanciated with just a hostname' do + s = Class.new do + include GenericService + rpc :AnRpc, GoodMsg, GoodMsg + rpc :AServerStreamer, GoodMsg, stream(GoodMsg) + rpc :AClientStreamer, stream(GoodMsg), GoodMsg + rpc :ABidiStreamer, stream(GoodMsg), stream(GoodMsg) + end + client_class = s.rpc_stub_class + expect { client_class.new('fakehostname') }.not_to raise_error + end + + it 'has the methods defined in the service' do + s = Class.new do + include GenericService + rpc :AnRpc, GoodMsg, GoodMsg + rpc :AServerStreamer, GoodMsg, stream(GoodMsg) + rpc :AClientStreamer, stream(GoodMsg), GoodMsg + rpc :ABidiStreamer, stream(GoodMsg), stream(GoodMsg) + end + client_class = s.rpc_stub_class + o = client_class.new('fakehostname') + expect(o.methods).to include(:an_rpc) + expect(o.methods).to include(:a_bidi_streamer) + expect(o.methods).to include(:a_client_streamer) + expect(o.methods).to include(:a_bidi_streamer) + end + + end + + end + + describe '#assert_rpc_descs_have_methods' do + + it 'fails if there is no instance method for an rpc descriptor' do + c1 = Class.new do + include GenericService + rpc :AnRpc, GoodMsg, GoodMsg + end + expect { c1.assert_rpc_descs_have_methods }.to raise_error + + c2 = Class.new do + include GenericService + rpc :AnRpc, GoodMsg, GoodMsg + rpc :AnotherRpc, GoodMsg, GoodMsg + + def an_rpc + end + end + expect { c2.assert_rpc_descs_have_methods }.to raise_error + end + + it 'passes if there are corresponding methods for each descriptor' do + c = Class.new do + include GenericService + rpc :AnRpc, GoodMsg, GoodMsg + rpc :AServerStreamer, GoodMsg, stream(GoodMsg) + rpc :AClientStreamer, stream(GoodMsg), GoodMsg + rpc :ABidiStreamer, stream(GoodMsg), stream(GoodMsg) + + def an_rpc(req, call) + end + + def a_server_streamer(req, call) + end + + def a_client_streamer(call) + end + + def a_bidi_streamer(call) + end + end + expect { c.assert_rpc_descs_have_methods }.to_not raise_error + end + + it 'passes for subclasses of that include GenericService' do + base = Class.new do + include GenericService + rpc :AnRpc, GoodMsg, GoodMsg + + def an_rpc(req, call) + end + end + c = Class.new(base) + expect { c.assert_rpc_descs_have_methods }.to_not raise_error + expect(c.include?(GenericService)).to be(true) + end + + it 'passes if subclasses define the rpc methods' do + base = Class.new do + include GenericService + rpc :AnRpc, GoodMsg, GoodMsg + end + c = Class.new(base) do + def an_rpc(req, call) + end + end + expect { c.assert_rpc_descs_have_methods }.to_not raise_error + expect(c.include?(GenericService)).to be(true) + end + + end + +end diff --git a/src/ruby/spec/metadata_spec.rb b/src/ruby/spec/metadata_spec.rb new file mode 100644 index 0000000000..8465a40fab --- /dev/null +++ b/src/ruby/spec/metadata_spec.rb @@ -0,0 +1,67 @@ +# Copyright 2014, 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' + +describe GRPC::Metadata do + + describe '#new' do + it 'should create instances' do + expect { GRPC::Metadata.new('a key', 'a value') }.to_not raise_error + expect(GRPC::Metadata.new('a key', 'a value')).to be_a(GRPC::Metadata) + end + end + + describe '#key' do + md = GRPC::Metadata.new('a key', 'a value') + it 'should be the constructor value' do + expect(md.key).to eq('a key') + end + end + + describe '#value' do + md = GRPC::Metadata.new('a key', 'a value') + it 'should be the constuctor value' do + expect(md.value).to eq('a value') + end + end + + describe '#dup' do + it 'should create a copy that returns the correct key' do + md = GRPC::Metadata.new('a key', 'a value') + expect(md.dup.key).to eq('a key') + end + + it 'should create a copy that returns the correct value' do + md = GRPC::Metadata.new('a key', 'a value') + expect(md.dup.value).to eq('a value') + end + end + +end diff --git a/src/ruby/spec/port_picker.rb b/src/ruby/spec/port_picker.rb new file mode 100644 index 0000000000..1b52113e10 --- /dev/null +++ b/src/ruby/spec/port_picker.rb @@ -0,0 +1,45 @@ +# Copyright 2014, 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 'socket' + +# @param [Fixnum] the minimum port number to accept +# @param [Fixnum] the maximum port number to accept +# @return [Fixnum ]a free tcp port +def find_unused_tcp_port(min=32768, max=60000) + # Allow the system to assign a port, by specifying 0. + # Loop until a port is assigned in the required range + loop do + socket = Socket.new(:INET, :STREAM, 0) + socket.bind(Addrinfo.tcp('127.0.0.1', 0)) + p = socket.local_address.ip_port + socket.close + return p if p > min and p < 60000 + end +end diff --git a/src/ruby/spec/server_spec.rb b/src/ruby/spec/server_spec.rb new file mode 100644 index 0000000000..598b7cfa7b --- /dev/null +++ b/src/ruby/spec/server_spec.rb @@ -0,0 +1,185 @@ +# Copyright 2014, 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' +require 'port_picker' + +module GRPC + + describe Server do + + before(:each) do + @cq = CompletionQueue.new + end + + describe '#start' do + + it 'runs without failing' do + blk = Proc.new do + s = Server.new(@cq, nil).start + end + expect(&blk).to_not raise_error + end + + it 'fails if the server is closed' do + s = Server.new(@cq, nil) + s.close + expect { s.start }.to raise_error(RuntimeError) + end + + end + + describe '#destroy' do + it 'destroys a server ok' do + s = start_a_server + blk = Proc.new { s.destroy } + expect(&blk).to_not raise_error + end + + it 'can be called more than once without error' do + s = start_a_server + begin + blk = Proc.new { s.destroy } + expect(&blk).to_not raise_error + blk.call + expect(&blk).to_not raise_error + ensure + s.close + end + end + end + + describe '#close' do + it 'closes a server ok' do + s = start_a_server + begin + blk = Proc.new { s.close } + expect(&blk).to_not raise_error + ensure + s.close + end + end + + it 'can be called more than once without error' do + s = start_a_server + blk = Proc.new { s.close } + expect(&blk).to_not raise_error + blk.call + expect(&blk).to_not raise_error + end + end + + describe '#add_http_port' do + + it 'runs without failing' do + blk = Proc.new do + s = Server.new(@cq, nil) + s.add_http2_port('localhost:0') + s.close + end + expect(&blk).to_not raise_error + end + + it 'fails if the server is closed' do + s = Server.new(@cq, nil) + s.close + expect { s.add_http2_port('localhost:0') }.to raise_error(RuntimeError) + end + + end + + describe '#new' do + + it 'takes a completion queue with nil channel args' do + expect { Server.new(@cq, nil) }.to_not raise_error + end + + it 'does not take a hash with bad keys as channel args' do + blk = construct_with_args(Object.new => 1) + expect(&blk).to raise_error TypeError + blk = construct_with_args(1 => 1) + expect(&blk).to raise_error TypeError + end + + it 'does not take a hash with bad values as channel args' do + blk = construct_with_args(:symbol => Object.new) + expect(&blk).to raise_error TypeError + blk = construct_with_args('1' => Hash.new) + expect(&blk).to raise_error TypeError + end + + it 'can take a hash with a symbol key as channel args' do + blk = construct_with_args(:a_symbol => 1) + expect(&blk).to_not raise_error + end + + it 'can take a hash with a string key as channel args' do + blk = construct_with_args('a_symbol' => 1) + expect(&blk).to_not raise_error + end + + it 'can take a hash with a string value as channel args' do + blk = construct_with_args(:a_symbol => '1') + expect(&blk).to_not raise_error + end + + it 'can take a hash with a symbol value as channel args' do + blk = construct_with_args(:a_symbol => :another_symbol) + expect(&blk).to_not raise_error + end + + it 'can take a hash with a numeric value as channel args' do + blk = construct_with_args(:a_symbol => 1) + expect(&blk).to_not raise_error + end + + it 'can take a hash with many args as channel args' do + args = Hash[127.times.collect { |x| [x.to_s, x] } ] + blk = construct_with_args(args) + expect(&blk).to_not raise_error + end + + end + + def construct_with_args(a) + Proc.new { Server.new(@cq, a) } + end + + def start_a_server + port = find_unused_tcp_port + host = "localhost:#{port}" + s = Server.new(@cq, nil) + s.add_http2_port(host) + s.start + s + end + + end + +end diff --git a/src/ruby/spec/spec_helper.rb b/src/ruby/spec/spec_helper.rb new file mode 100644 index 0000000000..3322674e97 --- /dev/null +++ b/src/ruby/spec/spec_helper.rb @@ -0,0 +1,39 @@ +# Copyright 2014, 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 'rspec' +require 'logging' +require 'rspec/logging_helper' + +# Configure RSpec to capture log messages for each test. The output from the +# logs will be stored in the @log_output variable. It is a StringIO instance. +RSpec.configure do |config| + include RSpec::LoggingHelper + config.capture_log_messages +end diff --git a/src/ruby/spec/status_spec.rb b/src/ruby/spec/status_spec.rb new file mode 100644 index 0000000000..83d4efc730 --- /dev/null +++ b/src/ruby/spec/status_spec.rb @@ -0,0 +1,161 @@ +# Copyright 2014, 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' + +module GRPC + + describe StatusCodes do + + before(:each) do + @known_types = { + :OK => 0, + :CANCELLED => 1, + :UNKNOWN => 2, + :INVALID_ARGUMENT => 3, + :DEADLINE_EXCEEDED => 4, + :NOT_FOUND => 5, + :ALREADY_EXISTS => 6, + :PERMISSION_DENIED => 7, + :RESOURCE_EXHAUSTED => 8, + :FAILED_PRECONDITION => 9, + :ABORTED => 10, + :OUT_OF_RANGE => 11, + :UNIMPLEMENTED => 12, + :INTERNAL => 13, + :UNAVAILABLE => 14, + :DATA_LOSS => 15, + :UNAUTHENTICATED => 16 + } + end + + it 'should have symbols for all the known status codes' do + m = StatusCodes + syms_and_codes = m.constants.collect { |c| [c, m.const_get(c)] } + expect(Hash[syms_and_codes]).to eq(@known_types) + end + + end + + describe Status do + + describe '#new' do + it 'should create new instances' do + expect { Status.new(142, 'test details') }.to_not raise_error + end + end + + describe '#details' do + it 'return the detail' do + sts = Status.new(142, 'test details') + expect(sts.details).to eq('test details') + end + end + + describe '#code' do + it 'should return the code' do + sts = Status.new(142, 'test details') + expect(sts.code).to eq(142) + end + end + + describe '#dup' do + it 'should create a copy that returns the correct details' do + sts = Status.new(142, 'test details') + expect(sts.dup.code).to eq(142) + end + + it 'should create a copy that returns the correct code' do + sts = Status.new(142, 'test details') + expect(sts.dup.details).to eq('test details') + end + end + + + end + + describe BadStatus do + + describe '#new' do + it 'should create new instances' do + expect { BadStatus.new(142, 'test details') }.to_not raise_error + end + end + + describe '#details' do + it 'return the detail' do + err = BadStatus.new(142, 'test details') + expect(err.details).to eq('test details') + end + end + + describe '#code' do + it 'should return the code' do + err = BadStatus.new(142, 'test details') + expect(err.code).to eq(142) + end + end + + describe '#dup' do + it 'should create a copy that returns the correct details' do + err = BadStatus.new(142, 'test details') + expect(err.dup.code).to eq(142) + end + + it 'should create a copy that returns the correct code' do + err = BadStatus.new(142, 'test details') + expect(err.dup.details).to eq('test details') + end + end + + describe '#to_status' do + it 'should create a Status with the same code and details' do + err = BadStatus.new(142, 'test details') + sts = err.to_status + expect(sts.code).to eq(142) + expect(sts.details).to eq('test details') + end + + it 'should create a copy that returns the correct code' do + err = BadStatus.new(142, 'test details') + expect(err.dup.details).to eq('test details') + end + end + + describe 'as an exception' do + + it 'can be raised' do + blk = Proc.new { raise BadStatus.new(343, 'status 343') } + expect(&blk).to raise_error(BadStatus) + end + end + + end + +end diff --git a/src/ruby/spec/time_consts_spec.rb b/src/ruby/spec/time_consts_spec.rb new file mode 100644 index 0000000000..2bbcac07c5 --- /dev/null +++ b/src/ruby/spec/time_consts_spec.rb @@ -0,0 +1,95 @@ +# Copyright 2014, 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' + +module GRPC + describe TimeConsts do + + before(:each) do + @known_consts = [:ZERO, :INFINITE_FUTURE, :INFINITE_PAST].sort + end + + it 'should have all the known types' do + expect(TimeConsts.constants.collect.sort).to eq(@known_consts) + end + + describe "#to_time" do + it 'converts each constant to a Time' do + m = TimeConsts + m.constants.each do |c| + expect(m.const_get(c).to_time).to be_a(Time) + end + end + end + + end + + describe '#from_relative_time' do + + it 'cannot handle arbitrary objects' do + expect { TimeConsts.from_relative_time(Object.new) }.to raise_error + end + + it 'preserves TimeConsts' do + m = TimeConsts + m.constants.each do |c| + const = m.const_get(c) + expect(TimeConsts.from_relative_time(const)).to be(const) + end + end + + it 'converts 0 to TimeConsts::ZERO' do + expect(TimeConsts.from_relative_time(0)).to eq(TimeConsts::ZERO) + end + + it 'converts nil to TimeConsts::ZERO' do + expect(TimeConsts.from_relative_time(nil)).to eq(TimeConsts::ZERO) + end + + it 'converts negative values to TimeConsts::INFINITE_FUTURE' do + [-1, -3.2, -1e6].each do |t| + y = TimeConsts.from_relative_time(t) + expect(y).to eq(TimeConsts::INFINITE_FUTURE) + end + end + + it 'converts a positive value to an absolute time' do + epsilon = 1 + [1, 3.2, 1e6].each do |t| + want = Time.now + t + abs = TimeConsts.from_relative_time(t) + expect(abs.to_f).to be_within(epsilon).of(want.to_f) + end + end + + end + +end + |