aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/ruby
diff options
context:
space:
mode:
Diffstat (limited to 'src/ruby')
-rwxr-xr-xsrc/ruby/.gitignore15
-rwxr-xr-xsrc/ruby/.rspec1
-rwxr-xr-xsrc/ruby/Gemfile13
-rwxr-xr-xsrc/ruby/README.md93
-rwxr-xr-xsrc/ruby/Rakefile38
-rwxr-xr-xsrc/ruby/bin/math.pb.rb65
-rwxr-xr-xsrc/ruby/bin/math.proto50
-rw-r--r--src/ruby/bin/math_client.rb110
-rw-r--r--src/ruby/bin/math_server.rb166
-rw-r--r--src/ruby/bin/noproto_client.rb75
-rw-r--r--src/ruby/bin/noproto_server.rb75
-rw-r--r--src/ruby/ext/grpc/extconf.rb92
-rw-r--r--src/ruby/ext/grpc/rb_byte_buffer.c243
-rw-r--r--src/ruby/ext/grpc/rb_byte_buffer.h54
-rw-r--r--src/ruby/ext/grpc/rb_call.c542
-rw-r--r--src/ruby/ext/grpc/rb_call.h59
-rw-r--r--src/ruby/ext/grpc/rb_channel.c235
-rw-r--r--src/ruby/ext/grpc/rb_channel.h49
-rw-r--r--src/ruby/ext/grpc/rb_channel_args.c157
-rw-r--r--src/ruby/ext/grpc/rb_channel_args.h53
-rw-r--r--src/ruby/ext/grpc/rb_completion_queue.c194
-rw-r--r--src/ruby/ext/grpc/rb_completion_queue.h50
-rw-r--r--src/ruby/ext/grpc/rb_event.c284
-rw-r--r--src/ruby/ext/grpc/rb_event.h55
-rw-r--r--src/ruby/ext/grpc/rb_grpc.c230
-rw-r--r--src/ruby/ext/grpc/rb_grpc.h71
-rw-r--r--src/ruby/ext/grpc/rb_metadata.c215
-rw-r--r--src/ruby/ext/grpc/rb_metadata.h53
-rw-r--r--src/ruby/ext/grpc/rb_server.c226
-rw-r--r--src/ruby/ext/grpc/rb_server.h50
-rw-r--r--src/ruby/ext/grpc/rb_status.c243
-rw-r--r--src/ruby/ext/grpc/rb_status.h53
-rwxr-xr-xsrc/ruby/grpc.gemspec30
-rw-r--r--src/ruby/lib/grpc.rb38
-rw-r--r--src/ruby/lib/grpc/errors.rb68
-rw-r--r--src/ruby/lib/grpc/event.rb38
-rw-r--r--src/ruby/lib/grpc/generic/active_call.rb485
-rw-r--r--src/ruby/lib/grpc/generic/bidi_call.rb320
-rw-r--r--src/ruby/lib/grpc/generic/client_stub.rb358
-rw-r--r--src/ruby/lib/grpc/generic/rpc_desc.rb157
-rw-r--r--src/ruby/lib/grpc/generic/rpc_server.rb408
-rw-r--r--src/ruby/lib/grpc/generic/service.rb247
-rw-r--r--src/ruby/lib/grpc/logconfig.rb40
-rw-r--r--src/ruby/lib/grpc/time_consts.rb69
-rw-r--r--src/ruby/lib/grpc/version.rb34
-rw-r--r--src/ruby/spec/alloc_spec.rb46
-rw-r--r--src/ruby/spec/byte_buffer_spec.rb71
-rw-r--r--src/ruby/spec/call_spec.rb200
-rw-r--r--src/ruby/spec/channel_spec.rb164
-rw-r--r--src/ruby/spec/client_server_spec.rb349
-rw-r--r--src/ruby/spec/completion_queue_spec.rb82
-rw-r--r--src/ruby/spec/event_spec.rb54
-rw-r--r--src/ruby/spec/generic/active_call_spec.rb321
-rw-r--r--src/ruby/spec/generic/client_stub_spec.rb484
-rw-r--r--src/ruby/spec/generic/rpc_desc_spec.rb380
-rw-r--r--src/ruby/spec/generic/rpc_server_pool_spec.rb153
-rw-r--r--src/ruby/spec/generic/rpc_server_spec.rb391
-rw-r--r--src/ruby/spec/generic/service_spec.rb324
-rw-r--r--src/ruby/spec/metadata_spec.rb67
-rw-r--r--src/ruby/spec/port_picker.rb45
-rw-r--r--src/ruby/spec/server_spec.rb185
-rw-r--r--src/ruby/spec/spec_helper.rb39
-rw-r--r--src/ruby/spec/status_spec.rb161
-rw-r--r--src/ruby/spec/time_consts_spec.rb95
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) &params, &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
+