diff --git a/doc/interop-test-descriptions.md b/doc/interop-test-descriptions.md index 1e04966380c..666af185b9f 100644 --- a/doc/interop-test-descriptions.md +++ b/doc/interop-test-descriptions.md @@ -60,6 +60,35 @@ Client asserts: *It may be possible to use UnaryCall instead of EmptyCall, but it is harder to ensure that the proto serialized to zero bytes.* +### cacheable_unary + +This test verifies that gRPC requests marked as cacheable use GET verb instead +of POST, and that server sets appropriate cache control headers for the response +to be cached by a proxy. This test requires that the server is behind +a caching proxy. Use of current timestamp in the request prevents accidental +cache matches left over from previous tests. + +Server features: +* [CacheableUnaryCall][] + +Procedure: + 1. Client calls CacheableUnaryCall with `SimpleRequest` request with payload + set to current timestamp. Timestamp format is irrelevant, and resolution is + in nanoseconds. + Client adds a `x-user-ip` header with value `1.2.3.4` to the request. + This is done since some proxys such as GFE will not cache requests from + localhost. + Client marks the request as cacheable by setting the cacheable flag in the + request context. Longer term this should be driven by the method option + specified in the proto file itself. + 2. Client calls CacheableUnaryCall with `SimpleRequest` request again + immediately with the same payload as the previous request. Cacheable flag is + also set for this request's context. + +Client asserts: +* Both calls were successful +* The payload body of both responses is the same. + ### large_unary This test verifies unary calls succeed in sending messages, and touches on flow @@ -941,6 +970,18 @@ payload body of size `SimpleRequest.response_size` bytes and type as appropriate for the `SimpleRequest.response_type`. If the server does not support the `response_type`, then it should fail the RPC with `INVALID_ARGUMENT`. +### CacheableUnaryCall + +Server gets the default SimpleRequest proto as the request. The content of the +request is ignored. It returns the SimpleResponse proto with the payload set +to current timestamp. The timestamp is an integer representing current time +with nanosecond resolution. This integer is formated as ASCII decimal in the +response. The format is not really important as long as the response payload +is different for each request. In addition it adds + 1. cache control headers such that the response can be cached by proxies in + the response path. Server should be behind a caching proxy for this test + to pass. Currently we set the max-age to 60 seconds. + ### CompressedResponse [CompressedResponse]: #compressedresponse diff --git a/src/proto/grpc/testing/test.proto b/src/proto/grpc/testing/test.proto index 84369db4b8a..b52c4cbad6c 100644 --- a/src/proto/grpc/testing/test.proto +++ b/src/proto/grpc/testing/test.proto @@ -47,6 +47,11 @@ service TestService { // One request followed by one response. rpc UnaryCall(SimpleRequest) returns (SimpleResponse); + // One request followed by one response. Response has cache control + // headers set such that a caching HTTP proxy (such as GFE) can + // satisfy subsequent requests. + rpc CacheableUnaryCall(SimpleRequest) returns (SimpleResponse); + // One request followed by a sequence of responses (streamed download). // The server returns the payload with client desired type and sizes. rpc StreamingOutputCall(StreamingOutputCallRequest) diff --git a/test/cpp/interop/client.cc b/test/cpp/interop/client.cc index 032b378b1a1..999be9d8a3f 100644 --- a/test/cpp/interop/client.cc +++ b/test/cpp/interop/client.cc @@ -149,6 +149,8 @@ int main(int argc, char** argv) { client.DoStatusWithMessage(); } else if (FLAGS_test_case == "custom_metadata") { client.DoCustomMetadata(); + } else if (FLAGS_test_case == "cacheable_unary") { + client.DoCacheableUnary(); } else if (FLAGS_test_case == "all") { client.DoEmpty(); client.DoLargeUnary(); @@ -166,6 +168,7 @@ int main(int argc, char** argv) { client.DoEmptyStream(); client.DoStatusWithMessage(); client.DoCustomMetadata(); + client.DoCacheableUnary(); // service_account_creds and jwt_token_creds can only run with ssl. if (FLAGS_use_tls) { grpc::string json_key = GetServiceAccountJsonKey(); @@ -177,6 +180,7 @@ int main(int argc, char** argv) { // compute_engine_creds only runs in GCE. } else { const char* testcases[] = {"all", + "cacheable_unary", "cancel_after_begin", "cancel_after_first_response", "client_compressed_streaming", diff --git a/test/cpp/interop/interop_client.cc b/test/cpp/interop/interop_client.cc index 6117878a33f..e9a804ccae7 100644 --- a/test/cpp/interop/interop_client.cc +++ b/test/cpp/interop/interop_client.cc @@ -846,6 +846,50 @@ bool InteropClient::DoStatusWithMessage() { return true; } +bool InteropClient::DoCacheableUnary() { + gpr_log(GPR_DEBUG, "Sending RPC with cacheable response"); + + // Create request with current timestamp + gpr_timespec ts = gpr_now(GPR_CLOCK_PRECISE); + std::string timestamp = std::to_string((long long unsigned)ts.tv_nsec); + SimpleRequest request; + request.mutable_payload()->set_body(timestamp.c_str(), timestamp.size()); + + // Request 1 + ClientContext context1; + SimpleResponse response1; + context1.set_cacheable(true); + // Add fake user IP since some proxy's (GFE) won't cache requests from + // localhost. + context1.AddMetadata("x-user-ip", "1.2.3.4"); + Status s1 = + serviceStub_.Get()->CacheableUnaryCall(&context1, request, &response1); + if (!AssertStatusOk(s1)) { + return false; + } + gpr_log(GPR_DEBUG, "response 1 payload: %s", + response1.payload().body().c_str()); + + // Request 2 + ClientContext context2; + SimpleResponse response2; + context2.set_cacheable(true); + context2.AddMetadata("x-user-ip", "1.2.3.4"); + Status s2 = + serviceStub_.Get()->CacheableUnaryCall(&context2, request, &response2); + if (!AssertStatusOk(s2)) { + return false; + } + gpr_log(GPR_DEBUG, "response 2 payload: %s", + response2.payload().body().c_str()); + + // Check that the body is same for both requests. It will be the same if the + // second response is a cached copy of the first response + GPR_ASSERT(response2.payload().body() == response1.payload().body()); + + return true; +} + bool InteropClient::DoCustomMetadata() { const grpc::string kEchoInitialMetadataKey("x-grpc-test-echo-initial"); const grpc::string kInitialMetadataValue("test_initial_metadata_value"); diff --git a/test/cpp/interop/interop_client.h b/test/cpp/interop/interop_client.h index eb886fcb7e2..1e89f0987d5 100644 --- a/test/cpp/interop/interop_client.h +++ b/test/cpp/interop/interop_client.h @@ -79,6 +79,7 @@ class InteropClient { bool DoEmptyStream(); bool DoStatusWithMessage(); bool DoCustomMetadata(); + bool DoCacheableUnary(); // Auth tests. // username is a string containing the user email bool DoJwtTokenCreds(const grpc::string& username); diff --git a/test/cpp/interop/interop_server.cc b/test/cpp/interop/interop_server.cc index c05eb5d1461..58f20aa611a 100644 --- a/test/cpp/interop/interop_server.cc +++ b/test/cpp/interop/interop_server.cc @@ -47,6 +47,7 @@ #include #include +#include "src/core/lib/support/string.h" #include "src/core/lib/transport/byte_stream.h" #include "src/proto/grpc/testing/empty.grpc.pb.h" #include "src/proto/grpc/testing/messages.grpc.pb.h" @@ -153,6 +154,17 @@ class TestServiceImpl : public TestService::Service { return Status::OK; } + // Response contains current timestamp. We ignore everything in the request. + Status CacheableUnaryCall(ServerContext* context, + const SimpleRequest* request, + SimpleResponse* response) { + gpr_timespec ts = gpr_now(GPR_CLOCK_PRECISE); + std::string timestamp = std::to_string((long long unsigned)ts.tv_nsec); + response->mutable_payload()->set_body(timestamp.c_str(), timestamp.size()); + context->AddInitialMetadata("cache-control", "max-age=60, public"); + return Status::OK; + } + Status UnaryCall(ServerContext* context, const SimpleRequest* request, SimpleResponse* response) { MaybeEchoMetadata(context);