From 25cf2867af1d5fa88ae1f3eb15ac47ae75be0190 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 13 Aug 2026 16:20:46 +1200 Subject: [PATCH 1/3] Add call factory with deadline handling --- fixtures/protocol/grpc/test_middleware.rb | 8 +------- lib/protocol/grpc/call.rb | 8 ++++++++ test/protocol/grpc/call.rb | 25 +++++++++++++++++++++++ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/fixtures/protocol/grpc/test_middleware.rb b/fixtures/protocol/grpc/test_middleware.rb index d9e7391..9f020c2 100644 --- a/fixtures/protocol/grpc/test_middleware.rb +++ b/fixtures/protocol/grpc/test_middleware.rb @@ -9,7 +9,6 @@ require "protocol/grpc/body/readable" require "protocol/grpc/body/writable" require "protocol/grpc/header" -require "async/deadline" # Test implementation of Middleware with service routing class TestMiddleware < Protocol::GRPC::Middleware @@ -51,11 +50,7 @@ def dispatch(request) response_headers["grpc-encoding"] = encoding end - if timeout = request.headers["grpc-timeout"]&.to_seconds - deadline = Async::Deadline.start(timeout) - end - - call = Protocol::GRPC::Call.new(request, deadline: deadline) + call = Protocol::GRPC::Call.for(request) # Delegate to service handler wrapper result = wrapper.call(input, output, call) @@ -126,4 +121,3 @@ def call(input, output, call) end end end - diff --git a/lib/protocol/grpc/call.rb b/lib/protocol/grpc/call.rb index e43cffb..daf2d11 100644 --- a/lib/protocol/grpc/call.rb +++ b/lib/protocol/grpc/call.rb @@ -10,6 +10,14 @@ module Protocol module GRPC # Represents context for a single RPC call. class Call + def self.for(request, response = nil) + if timeout = request.headers["grpc-timeout"] + deadline = Async::Deadline.start(timeout.to_seconds) + end + + return new(request, response, deadline: deadline) + end + # Initialize a new RPC call context. # @parameter request [Protocol::HTTP::Request] The HTTP request # @parameter response [Protocol::HTTP::Response | Nil] The HTTP response (for setting metadata and trailers) diff --git a/test/protocol/grpc/call.rb b/test/protocol/grpc/call.rb index dcb3319..0266b50 100644 --- a/test/protocol/grpc/call.rb +++ b/test/protocol/grpc/call.rb @@ -10,6 +10,31 @@ describe Protocol::GRPC::Call do let(:headers) {Protocol::HTTP::Headers.new([["authorization", "Bearer token123"]])} let(:request) {Protocol::HTTP::Request.new("https", "localhost", "POST", "/service/method", nil, headers, nil)} + let(:response) {Protocol::HTTP::Response[200, {}, []]} + + with ".for" do + it "creates a call with request and response" do + call = subject.for(request, response) + + expect(call.request).to be == request + expect(call.response).to be == response + end + + it "computes deadline from grpc-timeout" do + headers = Protocol::GRPC::Methods.build_headers(timeout: 0.3) + request = Protocol::HTTP::Request.new("https", "localhost", "POST", "/service/method", nil, headers, nil) + call = subject.for(request, response) + + expect(call.deadline).to be_a(Async::Deadline) + expect(call.time_remaining).to be <= 0.3 + end + + it "does not set a deadline without grpc-timeout" do + call = subject.for(request, response) + + expect(call.deadline).to be_nil + end + end it "has request" do call = subject.new(request) From 6f7c0446c5bf10e6f33b4c2019410169ea31265d Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 13 Aug 2026 16:21:05 +1200 Subject: [PATCH 2/3] Apply RuboCop formatting --- lib/protocol/grpc/call.rb | 4 ++-- test/protocol/grpc/call.rb | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/protocol/grpc/call.rb b/lib/protocol/grpc/call.rb index daf2d11..1639091 100644 --- a/lib/protocol/grpc/call.rb +++ b/lib/protocol/grpc/call.rb @@ -14,10 +14,10 @@ def self.for(request, response = nil) if timeout = request.headers["grpc-timeout"] deadline = Async::Deadline.start(timeout.to_seconds) end - + return new(request, response, deadline: deadline) end - + # Initialize a new RPC call context. # @parameter request [Protocol::HTTP::Request] The HTTP request # @parameter response [Protocol::HTTP::Response | Nil] The HTTP response (for setting metadata and trailers) diff --git a/test/protocol/grpc/call.rb b/test/protocol/grpc/call.rb index 0266b50..0e9d806 100644 --- a/test/protocol/grpc/call.rb +++ b/test/protocol/grpc/call.rb @@ -11,27 +11,27 @@ let(:headers) {Protocol::HTTP::Headers.new([["authorization", "Bearer token123"]])} let(:request) {Protocol::HTTP::Request.new("https", "localhost", "POST", "/service/method", nil, headers, nil)} let(:response) {Protocol::HTTP::Response[200, {}, []]} - + with ".for" do it "creates a call with request and response" do call = subject.for(request, response) - + expect(call.request).to be == request expect(call.response).to be == response end - + it "computes deadline from grpc-timeout" do headers = Protocol::GRPC::Methods.build_headers(timeout: 0.3) request = Protocol::HTTP::Request.new("https", "localhost", "POST", "/service/method", nil, headers, nil) call = subject.for(request, response) - + expect(call.deadline).to be_a(Async::Deadline) expect(call.time_remaining).to be <= 0.3 end - + it "does not set a deadline without grpc-timeout" do call = subject.for(request, response) - + expect(call.deadline).to be_nil end end From 48a3b0137c9185c357b016582bc1a4d3ef76ccf0 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 13 Aug 2026 16:24:20 +1200 Subject: [PATCH 3/3] Document call factory --- lib/protocol/grpc/call.rb | 5 +++++ lib/protocol/grpc/interface.rb | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/lib/protocol/grpc/call.rb b/lib/protocol/grpc/call.rb index 1639091..d958b09 100644 --- a/lib/protocol/grpc/call.rb +++ b/lib/protocol/grpc/call.rb @@ -10,6 +10,11 @@ module Protocol module GRPC # Represents context for a single RPC call. class Call + # Create a new RPC call context for the given request and response. + # Automatically computes a deadline from the `grpc-timeout` request header, if present. + # @parameter request [Protocol::HTTP::Request] The HTTP request + # @parameter response [Protocol::HTTP::Response | Nil] The HTTP response + # @returns [Call] The new call context. def self.for(request, response = nil) if timeout = request.headers["grpc-timeout"] deadline = Async::Deadline.start(timeout.to_seconds) diff --git a/lib/protocol/grpc/interface.rb b/lib/protocol/grpc/interface.rb index 16a0f9a..8aa695f 100644 --- a/lib/protocol/grpc/interface.rb +++ b/lib/protocol/grpc/interface.rb @@ -25,6 +25,12 @@ def initialize(message_class) class Interface # RPC method definition RPC = Struct.new(:name, :request_class, :response_class, :streaming, :method, keyword_init: true) do + # Initialize a new RPC method definition. + # @parameter name [Symbol] The RPC method name. + # @parameter request_class [Class | Streaming | Nil] The request message class. + # @parameter response_class [Class | Streaming | Nil] The response message class. + # @parameter streaming [Symbol] The streaming mode. + # @parameter method [Symbol | Nil] The Ruby method name. def initialize(name:, request_class:, response_class:, streaming: :unary, method: nil) super end