diff options
Diffstat (limited to 'src')
138 files changed, 3924 insertions, 1290 deletions
diff --git a/src/compiler/objective_c_generator.cc b/src/compiler/objective_c_generator.cc index 69b3805bb1..483c6573a8 100644 --- a/src/compiler/objective_c_generator.cc +++ b/src/compiler/objective_c_generator.cc @@ -154,9 +154,9 @@ void PrintAdvancedImplementation(Printer *printer, printer->Print(" responsesWriteable:[GRXWriteable "); if (method->server_streaming()) { - printer->Print("writeableWithStreamHandler:eventHandler]];\n"); + printer->Print("writeableWithEventHandler:eventHandler]];\n"); } else { - printer->Print("writeableWithSingleValueHandler:handler]];\n"); + printer->Print("writeableWithSingleHandler:handler]];\n"); } printer->Print("}\n"); diff --git a/src/core/channel/client_channel.c b/src/core/channel/client_channel.c index a293c93ec6..6c2e6b38a8 100644 --- a/src/core/channel/client_channel.c +++ b/src/core/channel/client_channel.c @@ -527,6 +527,7 @@ static void cc_on_config_changed(void *arg, int iomgr_success) { } if (old_lb_policy != NULL) { + grpc_lb_policy_shutdown(old_lb_policy); GRPC_LB_POLICY_UNREF(old_lb_policy, "channel"); } diff --git a/src/core/security/client_auth_filter.c b/src/core/security/client_auth_filter.c index 0e699874bc..410852da52 100644 --- a/src/core/security/client_auth_filter.c +++ b/src/core/security/client_auth_filter.c @@ -75,11 +75,11 @@ typedef struct { grpc_mdstr *status_key; } channel_data; -static void bubble_up_error(grpc_call_element *elem, const char *error_msg) { +static void bubble_up_error(grpc_call_element *elem, grpc_status_code status, + const char *error_msg) { call_data *calld = elem->call_data; gpr_log(GPR_ERROR, "Client side authentication failure: %s", error_msg); - grpc_transport_stream_op_add_cancellation(&calld->op, - GRPC_STATUS_UNAUTHENTICATED); + grpc_transport_stream_op_add_cancellation(&calld->op, status); grpc_call_next_op(elem, &calld->op); } @@ -94,7 +94,8 @@ static void on_credentials_metadata(void *user_data, grpc_metadata_batch *mdb; size_t i; if (status != GRPC_CREDENTIALS_OK) { - bubble_up_error(elem, "Credentials failed to get metadata."); + bubble_up_error(elem, GRPC_STATUS_UNAUTHENTICATED, + "Credentials failed to get metadata."); return; } GPR_ASSERT(num_md <= MAX_CREDENTIALS_METADATA_COUNT); @@ -154,7 +155,7 @@ static void send_security_metadata(grpc_call_element *elem, if (channel_creds_has_md && call_creds_has_md) { calld->creds = grpc_composite_credentials_create(channel_creds, ctx->creds); if (calld->creds == NULL) { - bubble_up_error(elem, + bubble_up_error(elem, GRPC_STATUS_INVALID_ARGUMENT, "Incompatible credentials set on channel and call."); return; } @@ -182,7 +183,7 @@ static void on_host_checked(void *user_data, grpc_security_status status) { char *error_msg; gpr_asprintf(&error_msg, "Invalid host %s set in :authority metadata.", grpc_mdstr_as_c_string(calld->host)); - bubble_up_error(elem, error_msg); + bubble_up_error(elem, GRPC_STATUS_INVALID_ARGUMENT, error_msg); gpr_free(error_msg); } } @@ -252,7 +253,7 @@ static void auth_start_transport_op(grpc_call_element *elem, gpr_asprintf(&error_msg, "Invalid host %s set in :authority metadata.", call_host); - bubble_up_error(elem, error_msg); + bubble_up_error(elem, GRPC_STATUS_INVALID_ARGUMENT, error_msg); gpr_free(error_msg); } return; /* early exit */ diff --git a/src/core/security/google_default_credentials.c b/src/core/security/google_default_credentials.c index f368819597..d1f228665f 100644 --- a/src/core/security/google_default_credentials.c +++ b/src/core/security/google_default_credentials.c @@ -84,6 +84,8 @@ static void on_compute_engine_detection_http_response( gpr_mu_unlock(GRPC_POLLSET_MU(&detector->pollset)); } +static void destroy_pollset(void *p) { grpc_pollset_destroy(p); } + static int is_stack_running_on_compute_engine(void) { compute_engine_detector detector; grpc_httpcli_request request; @@ -114,12 +116,12 @@ static int is_stack_running_on_compute_engine(void) { while (!detector.is_done) { grpc_pollset_worker worker; grpc_pollset_work(&detector.pollset, &worker, - gpr_inf_future(GPR_CLOCK_REALTIME)); + gpr_inf_future(GPR_CLOCK_MONOTONIC)); } gpr_mu_unlock(GRPC_POLLSET_MU(&detector.pollset)); grpc_httpcli_context_destroy(&context); - grpc_pollset_destroy(&detector.pollset); + grpc_pollset_shutdown(&detector.pollset, destroy_pollset, &detector.pollset); return detector.success; } diff --git a/src/core/surface/call.c b/src/core/surface/call.c index 6e566e6a8f..5839d3ac2e 100644 --- a/src/core/surface/call.c +++ b/src/core/surface/call.c @@ -1539,6 +1539,7 @@ grpc_call_error grpc_call_start_batch(grpc_call *call, const grpc_op *ops, /* Flag validation: currently allow no flags */ if (op->flags != 0) return GRPC_CALL_ERROR_INVALID_FLAGS; req = &reqs[out++]; + if (out > GRPC_IOREQ_OP_COUNT) return GRPC_CALL_ERROR_BATCH_TOO_BIG; req->op = GRPC_IOREQ_SEND_INITIAL_METADATA; req->data.send_metadata.count = op->data.send_initial_metadata.count; req->data.send_metadata.metadata = @@ -1553,6 +1554,7 @@ grpc_call_error grpc_call_start_batch(grpc_call *call, const grpc_op *ops, return GRPC_CALL_ERROR_INVALID_MESSAGE; } req = &reqs[out++]; + if (out > GRPC_IOREQ_OP_COUNT) return GRPC_CALL_ERROR_BATCH_TOO_BIG; req->op = GRPC_IOREQ_SEND_MESSAGE; req->data.send_message = op->data.send_message; req->flags = op->flags; @@ -1564,6 +1566,7 @@ grpc_call_error grpc_call_start_batch(grpc_call *call, const grpc_op *ops, return GRPC_CALL_ERROR_NOT_ON_SERVER; } req = &reqs[out++]; + if (out > GRPC_IOREQ_OP_COUNT) return GRPC_CALL_ERROR_BATCH_TOO_BIG; req->op = GRPC_IOREQ_SEND_CLOSE; req->flags = op->flags; break; @@ -1574,6 +1577,7 @@ grpc_call_error grpc_call_start_batch(grpc_call *call, const grpc_op *ops, return GRPC_CALL_ERROR_NOT_ON_CLIENT; } req = &reqs[out++]; + if (out > GRPC_IOREQ_OP_COUNT) return GRPC_CALL_ERROR_BATCH_TOO_BIG; req->op = GRPC_IOREQ_SEND_TRAILING_METADATA; req->flags = op->flags; req->data.send_metadata.count = @@ -1581,6 +1585,7 @@ grpc_call_error grpc_call_start_batch(grpc_call *call, const grpc_op *ops, req->data.send_metadata.metadata = op->data.send_status_from_server.trailing_metadata; req = &reqs[out++]; + if (out > GRPC_IOREQ_OP_COUNT) return GRPC_CALL_ERROR_BATCH_TOO_BIG; req->op = GRPC_IOREQ_SEND_STATUS; req->data.send_status.code = op->data.send_status_from_server.status; req->data.send_status.details = @@ -1590,6 +1595,7 @@ grpc_call_error grpc_call_start_batch(grpc_call *call, const grpc_op *ops, op->data.send_status_from_server.status_details, 0) : NULL; req = &reqs[out++]; + if (out > GRPC_IOREQ_OP_COUNT) return GRPC_CALL_ERROR_BATCH_TOO_BIG; req->op = GRPC_IOREQ_SEND_CLOSE; break; case GRPC_OP_RECV_INITIAL_METADATA: @@ -1599,6 +1605,7 @@ grpc_call_error grpc_call_start_batch(grpc_call *call, const grpc_op *ops, return GRPC_CALL_ERROR_NOT_ON_SERVER; } req = &reqs[out++]; + if (out > GRPC_IOREQ_OP_COUNT) return GRPC_CALL_ERROR_BATCH_TOO_BIG; req->op = GRPC_IOREQ_RECV_INITIAL_METADATA; req->data.recv_metadata = op->data.recv_initial_metadata; req->data.recv_metadata->count = 0; @@ -1608,6 +1615,7 @@ grpc_call_error grpc_call_start_batch(grpc_call *call, const grpc_op *ops, /* Flag validation: currently allow no flags */ if (op->flags != 0) return GRPC_CALL_ERROR_INVALID_FLAGS; req = &reqs[out++]; + if (out > GRPC_IOREQ_OP_COUNT) return GRPC_CALL_ERROR_BATCH_TOO_BIG; req->op = GRPC_IOREQ_RECV_MESSAGE; req->data.recv_message = op->data.recv_message; req->flags = op->flags; @@ -1619,22 +1627,26 @@ grpc_call_error grpc_call_start_batch(grpc_call *call, const grpc_op *ops, return GRPC_CALL_ERROR_NOT_ON_SERVER; } req = &reqs[out++]; + if (out > GRPC_IOREQ_OP_COUNT) return GRPC_CALL_ERROR_BATCH_TOO_BIG; req->op = GRPC_IOREQ_RECV_STATUS; req->flags = op->flags; req->data.recv_status.set_value = set_status_value_directly; req->data.recv_status.user_data = op->data.recv_status_on_client.status; req = &reqs[out++]; + if (out > GRPC_IOREQ_OP_COUNT) return GRPC_CALL_ERROR_BATCH_TOO_BIG; req->op = GRPC_IOREQ_RECV_STATUS_DETAILS; req->data.recv_status_details.details = op->data.recv_status_on_client.status_details; req->data.recv_status_details.details_capacity = op->data.recv_status_on_client.status_details_capacity; req = &reqs[out++]; + if (out > GRPC_IOREQ_OP_COUNT) return GRPC_CALL_ERROR_BATCH_TOO_BIG; req->op = GRPC_IOREQ_RECV_TRAILING_METADATA; req->data.recv_metadata = op->data.recv_status_on_client.trailing_metadata; req->data.recv_metadata->count = 0; req = &reqs[out++]; + if (out > GRPC_IOREQ_OP_COUNT) return GRPC_CALL_ERROR_BATCH_TOO_BIG; req->op = GRPC_IOREQ_RECV_CLOSE; finish_func = finish_batch_with_close; break; @@ -1642,12 +1654,14 @@ grpc_call_error grpc_call_start_batch(grpc_call *call, const grpc_op *ops, /* Flag validation: currently allow no flags */ if (op->flags != 0) return GRPC_CALL_ERROR_INVALID_FLAGS; req = &reqs[out++]; + if (out > GRPC_IOREQ_OP_COUNT) return GRPC_CALL_ERROR_BATCH_TOO_BIG; req->op = GRPC_IOREQ_RECV_STATUS; req->flags = op->flags; req->data.recv_status.set_value = set_cancelled_value; req->data.recv_status.user_data = op->data.recv_close_on_server.cancelled; req = &reqs[out++]; + if (out > GRPC_IOREQ_OP_COUNT) return GRPC_CALL_ERROR_BATCH_TOO_BIG; req->op = GRPC_IOREQ_RECV_CLOSE; finish_func = finish_batch_with_close; break; diff --git a/src/core/surface/version.c b/src/core/surface/version.c index 4f5d648371..d7aaba3868 100644 --- a/src/core/surface/version.c +++ b/src/core/surface/version.c @@ -37,5 +37,5 @@ #include <grpc/grpc.h> const char *grpc_version_string(void) { - return "0.10.0.0"; + return "0.10.1.0"; } diff --git a/src/core/tsi/transport_security_interface.h b/src/core/tsi/transport_security_interface.h index 936b0c25b0..e27e6b9fc9 100644 --- a/src/core/tsi/transport_security_interface.h +++ b/src/core/tsi/transport_security_interface.h @@ -158,6 +158,8 @@ tsi_result tsi_frame_protector_protect_flush( value is expected to be at most max_protected_frame_size minus overhead which means that max_protected_frame_size is a safe bet. The output value is the number of bytes actually written. + If *unprotected_bytes_size is unchanged, there may be more data remaining + to unprotect, and the caller should call this function again. - This method returns TSI_OK in case of success. Success includes cases where there is not enough data to output a frame in which case diff --git a/src/cpp/client/channel.cc b/src/cpp/client/channel.cc index af7366eb01..1c2eecf786 100644 --- a/src/cpp/client/channel.cc +++ b/src/cpp/client/channel.cc @@ -61,19 +61,25 @@ Channel::~Channel() { grpc_channel_destroy(c_channel_); } Call Channel::CreateCall(const RpcMethod& method, ClientContext* context, CompletionQueue* cq) { - const char* host_str = host_.empty() ? NULL : host_.c_str(); - auto c_call = method.channel_tag() && context->authority().empty() - ? grpc_channel_create_registered_call( - c_channel_, context->propagate_from_call_, - context->propagation_options_.c_bitmask(), cq->cq(), - method.channel_tag(), context->raw_deadline()) - : grpc_channel_create_call( - c_channel_, context->propagate_from_call_, - context->propagation_options_.c_bitmask(), cq->cq(), - method.name(), context->authority().empty() - ? host_str - : context->authority().c_str(), - context->raw_deadline()); + const bool kRegistered = method.channel_tag() && context->authority().empty(); + grpc_call* c_call = NULL; + if (kRegistered) { + c_call = grpc_channel_create_registered_call( + c_channel_, context->propagate_from_call_, + context->propagation_options_.c_bitmask(), cq->cq(), + method.channel_tag(), context->raw_deadline()); + } else { + const char* host_str = NULL; + if (!context->authority().empty()) { + host_str = context->authority().c_str(); + } else if (!host_.empty()) { + host_str = host_.c_str(); + } + c_call = grpc_channel_create_call(c_channel_, context->propagate_from_call_, + context->propagation_options_.c_bitmask(), + cq->cq(), method.name(), host_str, + context->raw_deadline()); + } grpc_census_call_set_context(c_call, context->census_context()); GRPC_TIMER_MARK(GRPC_PTAG_CPP_CALL_CREATED, c_call); context->set_call(c_call, shared_from_this()); diff --git a/src/cpp/client/client_context.cc b/src/cpp/client/client_context.cc index 1ed2d38961..dd86e7b108 100644 --- a/src/cpp/client/client_context.cc +++ b/src/cpp/client/client_context.cc @@ -48,7 +48,6 @@ namespace grpc { ClientContext::ClientContext() : initial_metadata_received_(false), call_(nullptr), - cq_(nullptr), deadline_(gpr_inf_future(GPR_CLOCK_REALTIME)), propagate_from_call_(nullptr) {} @@ -56,14 +55,6 @@ ClientContext::~ClientContext() { if (call_) { grpc_call_destroy(call_); } - if (cq_) { - // Drain cq_. - grpc_completion_queue_shutdown(cq_); - while (grpc_completion_queue_next(cq_, gpr_inf_future(GPR_CLOCK_REALTIME)) - .type != GRPC_QUEUE_SHUTDOWN) - ; - grpc_completion_queue_destroy(cq_); - } } std::unique_ptr<ClientContext> ClientContext::FromServerContext( diff --git a/src/cpp/client/secure_credentials.cc b/src/cpp/client/secure_credentials.cc index 2d6114e06b..6cd6b77fcf 100644 --- a/src/cpp/client/secure_credentials.cc +++ b/src/cpp/client/secure_credentials.cc @@ -34,6 +34,7 @@ #include <grpc/support/log.h> #include <grpc++/channel_arguments.h> +#include <grpc++/impl/grpc_library.h> #include "src/cpp/client/channel.h" #include "src/cpp/client/secure_credentials.h" @@ -61,12 +62,14 @@ std::shared_ptr<Credentials> WrapCredentials(grpc_credentials* creds) { } // namespace std::shared_ptr<Credentials> GoogleDefaultCredentials() { + GrpcLibrary init; // To call grpc_init(). return WrapCredentials(grpc_google_default_credentials_create()); } // Builds SSL Credentials given SSL specific options std::shared_ptr<Credentials> SslCredentials( const SslCredentialsOptions& options) { + GrpcLibrary init; // To call grpc_init(). grpc_ssl_pem_key_cert_pair pem_key_cert_pair = { options.pem_private_key.c_str(), options.pem_cert_chain.c_str()}; @@ -78,6 +81,7 @@ std::shared_ptr<Credentials> SslCredentials( // Builds credentials for use when running in GCE std::shared_ptr<Credentials> ComputeEngineCredentials() { + GrpcLibrary init; // To call grpc_init(). return WrapCredentials(grpc_compute_engine_credentials_create()); } @@ -85,6 +89,7 @@ std::shared_ptr<Credentials> ComputeEngineCredentials() { std::shared_ptr<Credentials> ServiceAccountCredentials( const grpc::string& json_key, const grpc::string& scope, long token_lifetime_seconds) { + GrpcLibrary init; // To call grpc_init(). if (token_lifetime_seconds <= 0) { gpr_log(GPR_ERROR, "Trying to create ServiceAccountCredentials " @@ -100,6 +105,7 @@ std::shared_ptr<Credentials> ServiceAccountCredentials( // Builds JWT credentials. std::shared_ptr<Credentials> ServiceAccountJWTAccessCredentials( const grpc::string& json_key, long token_lifetime_seconds) { + GrpcLibrary init; // To call grpc_init(). if (token_lifetime_seconds <= 0) { gpr_log(GPR_ERROR, "Trying to create JWTCredentials with non-positive lifetime"); @@ -114,6 +120,7 @@ std::shared_ptr<Credentials> ServiceAccountJWTAccessCredentials( // Builds refresh token credentials. std::shared_ptr<Credentials> RefreshTokenCredentials( const grpc::string& json_refresh_token) { + GrpcLibrary init; // To call grpc_init(). return WrapCredentials( grpc_refresh_token_credentials_create(json_refresh_token.c_str())); } @@ -121,6 +128,7 @@ std::shared_ptr<Credentials> RefreshTokenCredentials( // Builds access token credentials. std::shared_ptr<Credentials> AccessTokenCredentials( const grpc::string& access_token) { + GrpcLibrary init; // To call grpc_init(). return WrapCredentials( grpc_access_token_credentials_create(access_token.c_str())); } @@ -129,6 +137,7 @@ std::shared_ptr<Credentials> AccessTokenCredentials( std::shared_ptr<Credentials> IAMCredentials( const grpc::string& authorization_token, const grpc::string& authority_selector) { + GrpcLibrary init; // To call grpc_init(). return WrapCredentials(grpc_iam_credentials_create( authorization_token.c_str(), authority_selector.c_str())); } diff --git a/src/cpp/common/auth_property_iterator.cc b/src/cpp/common/auth_property_iterator.cc index e706c6c921..ba88983515 100644 --- a/src/cpp/common/auth_property_iterator.cc +++ b/src/cpp/common/auth_property_iterator.cc @@ -31,7 +31,7 @@ * */ -#include <grpc++/auth_property_iterator.h> +#include <grpc++/auth_context.h> #include <grpc/grpc_security.h> diff --git a/src/csharp/Grpc.Auth/GoogleCredential.cs b/src/csharp/Grpc.Auth/GoogleCredential.cs deleted file mode 100644 index 9936cf583c..0000000000 --- a/src/csharp/Grpc.Auth/GoogleCredential.cs +++ /dev/null @@ -1,125 +0,0 @@ -#region Copyright notice and license - -// Copyright 2015, Google Inc. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -#endregion - -using System; -using System.Collections.Generic; -using System.IO; -using System.Security.Cryptography; -using System.Threading; -using System.Threading.Tasks; - -using Google.Apis.Auth.OAuth2; -using Google.Apis.Auth.OAuth2.Responses; -using Newtonsoft.Json.Linq; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.Security; - -namespace Grpc.Auth -{ - // TODO(jtattermusch): Remove this class once possible. - /// <summary> - /// A temporary placeholder for Google credential from - /// Google Auth library for .NET. It emulates the usage pattern - /// for Usable auth. - /// </summary> - public class GoogleCredential - { - private const string GoogleApplicationCredentialsEnvName = "GOOGLE_APPLICATION_CREDENTIALS"; - private const string ClientEmailFieldName = "client_email"; - private const string PrivateKeyFieldName = "private_key"; - - private ServiceCredential credential; - - private GoogleCredential(ServiceCredential credential) - { - this.credential = credential; - } - - public static GoogleCredential GetApplicationDefault() - { - return new GoogleCredential(null); - } - - public bool IsCreateScopedRequired - { - get - { - return true; - } - } - - public GoogleCredential CreateScoped(IEnumerable<string> scopes) - { - var credsPath = Environment.GetEnvironmentVariable(GoogleApplicationCredentialsEnvName); - if (credsPath == null) - { - // Default to ComputeCredentials if path to JSON key is not set. - // ComputeCredential is not scoped actually, but for our use case it's - // fine to treat is as such. - return new GoogleCredential(new ComputeCredential(new ComputeCredential.Initializer())); - } - - JObject jsonCredentialParameters = JObject.Parse(File.ReadAllText(credsPath)); - string clientEmail = jsonCredentialParameters.GetValue(ClientEmailFieldName).Value<string>(); - string privateKeyString = jsonCredentialParameters.GetValue(PrivateKeyFieldName).Value<string>(); - - var serviceCredential = new ServiceAccountCredential( - new ServiceAccountCredential.Initializer(clientEmail) - { - Scopes = scopes, - }.FromPrivateKey(privateKeyString)); - return new GoogleCredential(serviceCredential); - } - - public Task<bool> RequestAccessTokenAsync(CancellationToken taskCancellationToken) - { - return credential.RequestAccessTokenAsync(taskCancellationToken); - } - - public TokenResponse Token - { - get - { - return credential.Token; - } - } - - internal ServiceCredential InternalCredential - { - get - { - return credential; - } - } - } -} diff --git a/src/csharp/Grpc.Auth/Grpc.Auth.csproj b/src/csharp/Grpc.Auth/Grpc.Auth.csproj index 8e5036832d..930a34b0c3 100644 --- a/src/csharp/Grpc.Auth/Grpc.Auth.csproj +++ b/src/csharp/Grpc.Auth/Grpc.Auth.csproj @@ -11,7 +11,7 @@ <AssemblyName>Grpc.Auth</AssemblyName> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <DocumentationFile>bin\$(Configuration)\Grpc.Auth.Xml</DocumentationFile> - <NuGetPackageImportStamp>9b408026</NuGetPackageImportStamp> + <NuGetPackageImportStamp>4f8487a9</NuGetPackageImportStamp> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -41,28 +41,32 @@ <AssemblyOriginatorKeyFile>C:\keys\Grpc.snk</AssemblyOriginatorKeyFile> </PropertyGroup> <ItemGroup> - <Reference Include="BouncyCastle.Crypto"> + <Reference Include="BouncyCastle.Crypto, Version=1.7.4137.9688, Culture=neutral, PublicKeyToken=a4292a325f69b123, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> <HintPath>..\packages\BouncyCastle.1.7.0\lib\Net40-Client\BouncyCastle.Crypto.dll</HintPath> </Reference> - <Reference Include="Google.Apis.Auth, Version=1.9.2.27817, Culture=neutral, PublicKeyToken=4b01fa6e34db77ab, processorArchitecture=MSIL"> + <Reference Include="Google.Apis.Auth, Version=1.9.3.19379, Culture=neutral, PublicKeyToken=4b01fa6e34db77ab, processorArchitecture=MSIL"> <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\Google.Apis.Auth.1.9.2\lib\net40\Google.Apis.Auth.dll</HintPath> + <HintPath>..\packages\Google.Apis.Auth.1.9.3\lib\net40\Google.Apis.Auth.dll</HintPath> </Reference> - <Reference Include="Google.Apis.Auth.PlatformServices, Version=1.9.2.27820, Culture=neutral, PublicKeyToken=4b01fa6e34db77ab, processorArchitecture=MSIL"> + <Reference Include="Google.Apis.Auth.PlatformServices, Version=1.9.3.19383, Culture=neutral, PublicKeyToken=4b01fa6e34db77ab, processorArchitecture=MSIL"> <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\Google.Apis.Auth.1.9.2\lib\net40\Google.Apis.Auth.PlatformServices.dll</HintPath> + <HintPath>..\packages\Google.Apis.Auth.1.9.3\lib\net40\Google.Apis.Auth.PlatformServices.dll</HintPath> </Reference> - <Reference Include="Google.Apis.Core, Version=1.9.2.27816, Culture=neutral, PublicKeyToken=4b01fa6e34db77ab, processorArchitecture=MSIL"> + <Reference Include="Google.Apis.Core, Version=1.9.3.19379, Culture=neutral, PublicKeyToken=4b01fa6e34db77ab, processorArchitecture=MSIL"> <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\Google.Apis.Core.1.9.2\lib\portable-net40+sl50+win+wpa81+wp80\Google.Apis.Core.dll</HintPath> + <HintPath>..\packages\Google.Apis.Core.1.9.3\lib\portable-net40+sl50+win+wpa81+wp80\Google.Apis.Core.dll</HintPath> </Reference> - <Reference Include="Microsoft.Threading.Tasks"> + <Reference Include="Microsoft.Threading.Tasks, Version=1.0.12.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> <HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll</HintPath> </Reference> - <Reference Include="Microsoft.Threading.Tasks.Extensions"> + <Reference Include="Microsoft.Threading.Tasks.Extensions, Version=1.0.12.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> <HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll</HintPath> </Reference> - <Reference Include="Microsoft.Threading.Tasks.Extensions.Desktop"> + <Reference Include="Microsoft.Threading.Tasks.Extensions.Desktop, Version=1.0.168.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> <HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll</HintPath> </Reference> <Reference Include="Newtonsoft.Json, Version=7.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> @@ -87,7 +91,6 @@ <Link>Version.cs</Link> </Compile> <Compile Include="Properties\AssemblyInfo.cs" /> - <Compile Include="GoogleCredential.cs" /> <Compile Include="OAuth2Interceptors.cs" /> </ItemGroup> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> diff --git a/src/csharp/Grpc.Auth/OAuth2Interceptors.cs b/src/csharp/Grpc.Auth/OAuth2Interceptors.cs index cc9d2c175f..d628a83246 100644 --- a/src/csharp/Grpc.Auth/OAuth2Interceptors.cs +++ b/src/csharp/Grpc.Auth/OAuth2Interceptors.cs @@ -54,7 +54,7 @@ namespace Grpc.Auth /// </summary> public static MetadataInterceptorDelegate FromCredential(GoogleCredential googleCredential) { - var interceptor = new OAuth2Interceptor(googleCredential.InternalCredential, SystemClock.Default); + var interceptor = new OAuth2Interceptor(googleCredential, SystemClock.Default); return new MetadataInterceptorDelegate(interceptor.InterceptHeaders); } @@ -66,7 +66,7 @@ namespace Grpc.Auth public static MetadataInterceptorDelegate FromAccessToken(string oauth2Token) { Preconditions.CheckNotNull(oauth2Token); - return new MetadataInterceptorDelegate((metadata) => + return new MetadataInterceptorDelegate((authUri, metadata) => { metadata.Add(OAuth2Interceptor.CreateBearerTokenHeader(oauth2Token)); }); @@ -80,10 +80,10 @@ namespace Grpc.Auth private const string AuthorizationHeader = "Authorization"; private const string Schema = "Bearer"; - private ServiceCredential credential; + private ITokenAccess credential; private IClock clock; - public OAuth2Interceptor(ServiceCredential credential, IClock clock) + public OAuth2Interceptor(ITokenAccess credential, IClock clock) { this.credential = credential; this.clock = clock; @@ -94,23 +94,15 @@ namespace Grpc.Auth /// </summary> /// <param name="cancellationToken"></param> /// <returns></returns> - public string GetAccessToken(CancellationToken cancellationToken) + public string GetAccessToken(string authUri, CancellationToken cancellationToken) { - if (credential.Token == null || credential.Token.IsExpired(clock)) - { - // TODO(jtattermusch): Parallel requests will spawn multiple requests to refresh the token once the token expires. - // TODO(jtattermusch): Rethink synchronous wait to obtain the result. - if (!credential.RequestAccessTokenAsync(cancellationToken).Result) - { - throw new InvalidOperationException("The access token has expired but we can't refresh it"); - } - } - return credential.Token.AccessToken; + // TODO(jtattermusch): Rethink synchronous wait to obtain the result. + return credential.GetAccessTokenForRequestAsync(authUri, cancellationToken: cancellationToken).GetAwaiter().GetResult(); } - public void InterceptHeaders(Metadata metadata) + public void InterceptHeaders(string authUri, Metadata metadata) { - var accessToken = GetAccessToken(CancellationToken.None); + var accessToken = GetAccessToken(authUri, CancellationToken.None); metadata.Add(CreateBearerTokenHeader(accessToken)); } diff --git a/src/csharp/Grpc.Auth/app.config b/src/csharp/Grpc.Auth/app.config index 0a82bb4f16..84d7534d65 100644 --- a/src/csharp/Grpc.Auth/app.config +++ b/src/csharp/Grpc.Auth/app.config @@ -10,6 +10,10 @@ <assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-4.2.28.0" newVersion="4.0.0.0" /> </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="Google.Apis.Core" publicKeyToken="4b01fa6e34db77ab" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-1.9.2.38523" newVersion="1.9.2.38523" /> + </dependentAssembly> </assemblyBinding> </runtime> </configuration>
\ No newline at end of file diff --git a/src/csharp/Grpc.Auth/packages.config b/src/csharp/Grpc.Auth/packages.config index 29be953bf3..7a02c95db9 100644 --- a/src/csharp/Grpc.Auth/packages.config +++ b/src/csharp/Grpc.Auth/packages.config @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <packages> <package id="BouncyCastle" version="1.7.0" targetFramework="net45" /> - <package id="Google.Apis.Auth" version="1.9.2" targetFramework="net45" /> - <package id="Google.Apis.Core" version="1.9.2" targetFramework="net45" /> + <package id="Google.Apis.Auth" version="1.9.3" targetFramework="net45" /> + <package id="Google.Apis.Core" version="1.9.3" targetFramework="net45" /> <package id="Microsoft.Bcl" version="1.1.10" targetFramework="net45" /> <package id="Microsoft.Bcl.Async" version="1.0.168" targetFramework="net45" /> <package id="Microsoft.Bcl.Build" version="1.0.21" targetFramework="net45" /> diff --git a/src/csharp/Grpc.Core.Tests/ChannelOptionsTest.cs b/src/csharp/Grpc.Core.Tests/ChannelOptionsTest.cs index df09857efe..52be77c846 100644 --- a/src/csharp/Grpc.Core.Tests/ChannelOptionsTest.cs +++ b/src/csharp/Grpc.Core.Tests/ChannelOptionsTest.cs @@ -67,9 +67,9 @@ namespace Grpc.Core.Internal.Tests [Test] public void ConstructorPreconditions() { - Assert.Throws(typeof(NullReferenceException), () => { new ChannelOption(null, "abc"); }); - Assert.Throws(typeof(NullReferenceException), () => { new ChannelOption(null, 1); }); - Assert.Throws(typeof(NullReferenceException), () => { new ChannelOption("abc", null); }); + Assert.Throws(typeof(ArgumentNullException), () => { new ChannelOption(null, "abc"); }); + Assert.Throws(typeof(ArgumentNullException), () => { new ChannelOption(null, 1); }); + Assert.Throws(typeof(ArgumentNullException), () => { new ChannelOption("abc", null); }); } [Test] diff --git a/src/csharp/Grpc.Core.Tests/ChannelTest.cs b/src/csharp/Grpc.Core.Tests/ChannelTest.cs index 60b45176e5..2787572924 100644 --- a/src/csharp/Grpc.Core.Tests/ChannelTest.cs +++ b/src/csharp/Grpc.Core.Tests/ChannelTest.cs @@ -50,7 +50,7 @@ namespace Grpc.Core.Tests [Test] public void Constructor_RejectsInvalidParams() { - Assert.Throws(typeof(NullReferenceException), () => new Channel(null, Credentials.Insecure)); + Assert.Throws(typeof(ArgumentNullException), () => new Channel(null, Credentials.Insecure)); } [Test] @@ -72,11 +72,11 @@ namespace Grpc.Core.Tests } [Test] - public void Target() + public void ResolvedTarget() { using (var channel = new Channel("127.0.0.1", Credentials.Insecure)) { - Assert.IsTrue(channel.Target.Contains("127.0.0.1")); + Assert.IsTrue(channel.ResolvedTarget.Contains("127.0.0.1")); } } diff --git a/src/csharp/Grpc.Core.Tests/ClientBaseTest.cs b/src/csharp/Grpc.Core.Tests/ClientBaseTest.cs new file mode 100644 index 0000000000..2dc10ebe97 --- /dev/null +++ b/src/csharp/Grpc.Core.Tests/ClientBaseTest.cs @@ -0,0 +1,62 @@ +#region Copyright notice and license + +// Copyright 2015, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#endregion + +using System; +using Grpc.Core; +using Grpc.Core.Internal; +using Grpc.Core.Utils; +using NUnit.Framework; + +namespace Grpc.Core.Tests +{ + public class ClientBaseTest + { + [Test] + public void GetAuthUriBase_Valid() + { + Assert.AreEqual("https://some.googleapi.com/", ClientBase.GetAuthUriBase("some.googleapi.com")); + Assert.AreEqual("https://some.googleapi.com/", ClientBase.GetAuthUriBase("dns:///some.googleapi.com/")); + Assert.AreEqual("https://some.googleapi.com/", ClientBase.GetAuthUriBase("dns:///some.googleapi.com:443/")); + Assert.AreEqual("https://some.googleapi.com/", ClientBase.GetAuthUriBase("some.googleapi.com:443/")); + } + + [Test] + public void GetAuthUriBase_Invalid() + { + Assert.IsNull(ClientBase.GetAuthUriBase("some.googleapi.com:")); + Assert.IsNull(ClientBase.GetAuthUriBase("https://some.googleapi.com/")); + Assert.IsNull(ClientBase.GetAuthUriBase("dns://some.googleapi.com:443")); // just two slashes + Assert.IsNull(ClientBase.GetAuthUriBase("")); + } + } +} diff --git a/src/csharp/Grpc.Core.Tests/ClientServerTest.cs b/src/csharp/Grpc.Core.Tests/ClientServerTest.cs index 64ea21800f..e49fdb5268 100644 --- a/src/csharp/Grpc.Core.Tests/ClientServerTest.cs +++ b/src/csharp/Grpc.Core.Tests/ClientServerTest.cs @@ -46,47 +46,18 @@ namespace Grpc.Core.Tests public class ClientServerTest { const string Host = "127.0.0.1"; - const string ServiceName = "tests.Test"; - - static readonly Method<string, string> EchoMethod = new Method<string, string>( - MethodType.Unary, - ServiceName, - "Echo", - Marshallers.StringMarshaller, - Marshallers.StringMarshaller); - - static readonly Method<string, string> ConcatAndEchoMethod = new Method<string, string>( - MethodType.ClientStreaming, - ServiceName, - "ConcatAndEcho", - Marshallers.StringMarshaller, - Marshallers.StringMarshaller); - - static readonly Method<string, string> NonexistentMethod = new Method<string, string>( - MethodType.Unary, - ServiceName, - "NonexistentMethod", - Marshallers.StringMarshaller, - Marshallers.StringMarshaller); - - static readonly ServerServiceDefinition ServiceDefinition = ServerServiceDefinition.CreateBuilder(ServiceName) - .AddMethod(EchoMethod, EchoHandler) - .AddMethod(ConcatAndEchoMethod, ConcatAndEchoHandler) - .Build(); + MockServiceHelper helper; Server server; Channel channel; [SetUp] public void Init() { - server = new Server - { - Services = { ServiceDefinition }, - Ports = { { Host, ServerPort.PickUnused, ServerCredentials.Insecure } } - }; + helper = new MockServiceHelper(Host); + server = helper.GetServer(); server.Start(); - channel = new Channel(Host, server.Ports.Single().BoundPort, Credentials.Insecure); + channel = helper.GetChannel(); } [TearDown] @@ -103,123 +74,127 @@ namespace Grpc.Core.Tests } [Test] - public void UnaryCall() + public async Task UnaryCall() { - var callDetails = new CallInvocationDetails<string, string>(channel, EchoMethod, new CallOptions()); - Assert.AreEqual("ABC", Calls.BlockingUnaryCall(callDetails, "ABC")); + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => + { + return request; + }); + + Assert.AreEqual("ABC", Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "ABC")); + + Assert.AreEqual("ABC", await Calls.AsyncUnaryCall(helper.CreateUnaryCall(), "ABC")); } [Test] public void UnaryCall_ServerHandlerThrows() { - var callDetails = new CallInvocationDetails<string, string>(channel, EchoMethod, new CallOptions()); - try + helper.UnaryHandler = new UnaryServerMethod<string, string>((request, context) => { - Calls.BlockingUnaryCall(callDetails, "THROW"); - Assert.Fail(); - } - catch (RpcException e) - { - Assert.AreEqual(StatusCode.Unknown, e.Status.StatusCode); - } + throw new Exception("This was thrown on purpose by a test"); + }); + + var ex = Assert.Throws<RpcException>(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "abc")); + Assert.AreEqual(StatusCode.Unknown, ex.Status.StatusCode); + + var ex2 = Assert.Throws<RpcException>(async () => await Calls.AsyncUnaryCall(helper.CreateUnaryCall(), "abc")); + Assert.AreEqual(StatusCode.Unknown, ex2.Status.StatusCode); } [Test] public void UnaryCall_ServerHandlerThrowsRpcException() { - var callDetails = new CallInvocationDetails<string, string>(channel, EchoMethod, new CallOptions()); - try - { - Calls.BlockingUnaryCall(callDetails, "THROW_UNAUTHENTICATED"); - Assert.Fail(); - } - catch (RpcException e) + helper.UnaryHandler = new UnaryServerMethod<string, string>((request, context) => { - Assert.AreEqual(StatusCode.Unauthenticated, e.Status.StatusCode); - } + throw new RpcException(new Status(StatusCode.Unauthenticated, "")); + }); + + var ex = Assert.Throws<RpcException>(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "abc")); + Assert.AreEqual(StatusCode.Unauthenticated, ex.Status.StatusCode); + + var ex2 = Assert.Throws<RpcException>(async () => await Calls.AsyncUnaryCall(helper.CreateUnaryCall(), "abc")); + Assert.AreEqual(StatusCode.Unauthenticated, ex2.Status.StatusCode); } [Test] public void UnaryCall_ServerHandlerSetsStatus() { - var callDetails = new CallInvocationDetails<string, string>(channel, EchoMethod, new CallOptions()); - try + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => { - Calls.BlockingUnaryCall(callDetails, "SET_UNAUTHENTICATED"); - Assert.Fail(); - } - catch (RpcException e) - { - Assert.AreEqual(StatusCode.Unauthenticated, e.Status.StatusCode); - } - } + context.Status = new Status(StatusCode.Unauthenticated, ""); + return ""; + }); - [Test] - public async Task AsyncUnaryCall() - { - var callDetails = new CallInvocationDetails<string, string>(channel, EchoMethod, new CallOptions()); - var result = await Calls.AsyncUnaryCall(callDetails, "ABC"); - Assert.AreEqual("ABC", result); - } + var ex = Assert.Throws<RpcException>(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "abc")); + Assert.AreEqual(StatusCode.Unauthenticated, ex.Status.StatusCode); - [Test] - public async Task AsyncUnaryCall_ServerHandlerThrows() - { - var callDetails = new CallInvocationDetails<string, string>(channel, EchoMethod, new CallOptions()); - try - { - await Calls.AsyncUnaryCall(callDetails, "THROW"); - Assert.Fail(); - } - catch (RpcException e) - { - Assert.AreEqual(StatusCode.Unknown, e.Status.StatusCode); - } + var ex2 = Assert.Throws<RpcException>(async () => await Calls.AsyncUnaryCall(helper.CreateUnaryCall(), "abc")); + Assert.AreEqual(StatusCode.Unauthenticated, ex2.Status.StatusCode); } [Test] public async Task ClientStreamingCall() { - var callDetails = new CallInvocationDetails<string, string>(channel, ConcatAndEchoMethod, new CallOptions()); - var call = Calls.AsyncClientStreamingCall(callDetails); + helper.ClientStreamingHandler = new ClientStreamingServerMethod<string, string>(async (requestStream, context) => + { + string result = ""; + await requestStream.ForEachAsync(async (request) => + { + result += request; + }); + await Task.Delay(100); + return result; + }); - await call.RequestStream.WriteAll(new string[] { "A", "B", "C" }); + var call = Calls.AsyncClientStreamingCall(helper.CreateClientStreamingCall()); + await call.RequestStream.WriteAllAsync(new string[] { "A", "B", "C" }); Assert.AreEqual("ABC", await call.ResponseAsync); } [Test] public async Task ClientStreamingCall_CancelAfterBegin() { + var barrier = new TaskCompletionSource<object>(); + + helper.ClientStreamingHandler = new ClientStreamingServerMethod<string, string>(async (requestStream, context) => + { + barrier.SetResult(null); + await requestStream.ToListAsync(); + return ""; + }); + var cts = new CancellationTokenSource(); - var callDetails = new CallInvocationDetails<string, string>(channel, ConcatAndEchoMethod, new CallOptions(cancellationToken: cts.Token)); - var call = Calls.AsyncClientStreamingCall(callDetails); + var call = Calls.AsyncClientStreamingCall(helper.CreateClientStreamingCall(new CallOptions(cancellationToken: cts.Token))); - // TODO(jtattermusch): we need this to ensure call has been initiated once we cancel it. - await Task.Delay(1000); + await barrier.Task; // make sure the handler has started. cts.Cancel(); - try - { - await call.ResponseAsync; - } - catch (RpcException e) - { - Assert.AreEqual(StatusCode.Cancelled, e.Status.StatusCode); - } + var ex = Assert.Throws<RpcException>(async () => await call.ResponseAsync); + Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode); } [Test] - public void AsyncUnaryCall_EchoMetadata() + public async Task AsyncUnaryCall_EchoMetadata() { + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => + { + foreach (Metadata.Entry metadataEntry in context.RequestHeaders) + { + if (metadataEntry.Key != "user-agent") + { + context.ResponseTrailers.Add(metadataEntry); + } + } + return ""; + }); + var headers = new Metadata { - new Metadata.Entry("ascii-header", "abcdefg"), - new Metadata.Entry("binary-header-bin", new byte[] { 1, 2, 3, 0, 0xff }), + { "ascii-header", "abcdefg" }, + { "binary-header-bin", new byte[] { 1, 2, 3, 0, 0xff } } }; - var callDetails = new CallInvocationDetails<string, string>(channel, EchoMethod, new CallOptions(headers: headers)); - var call = Calls.AsyncUnaryCall(callDetails, "ABC"); - - Assert.AreEqual("ABC", call.ResponseAsync.Result); + var call = Calls.AsyncUnaryCall(helper.CreateUnaryCall(new CallOptions(headers: headers)), "ABC"); + await call; Assert.AreEqual(StatusCode.OK, call.GetStatus().StatusCode); @@ -236,15 +211,18 @@ namespace Grpc.Core.Tests public void UnaryCall_DisposedChannel() { channel.Dispose(); - - var callDetails = new CallInvocationDetails<string, string>(channel, EchoMethod, new CallOptions()); - Assert.Throws(typeof(ObjectDisposedException), () => Calls.BlockingUnaryCall(callDetails, "ABC")); + Assert.Throws(typeof(ObjectDisposedException), () => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "ABC")); } [Test] public void UnaryCallPerformance() { - var callDetails = new CallInvocationDetails<string, string>(channel, EchoMethod, new CallOptions()); + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => + { + return request; + }); + + var callDetails = helper.CreateUnaryCall(); BenchmarkUtil.RunBenchmark(100, 100, () => { Calls.BlockingUnaryCall(callDetails, "ABC"); }); } @@ -252,44 +230,57 @@ namespace Grpc.Core.Tests [Test] public void UnknownMethodHandler() { - var callDetails = new CallInvocationDetails<string, string>(channel, NonexistentMethod, new CallOptions()); - try - { - Calls.BlockingUnaryCall(callDetails, "ABC"); - Assert.Fail(); - } - catch (RpcException e) - { - Assert.AreEqual(StatusCode.Unimplemented, e.Status.StatusCode); - } + var nonexistentMethod = new Method<string, string>( + MethodType.Unary, + MockServiceHelper.ServiceName, + "NonExistentMethod", + Marshallers.StringMarshaller, + Marshallers.StringMarshaller); + + var callDetails = new CallInvocationDetails<string, string>(channel, nonexistentMethod, new CallOptions()); + + var ex = Assert.Throws<RpcException>(() => Calls.BlockingUnaryCall(callDetails, "abc")); + Assert.AreEqual(StatusCode.Unimplemented, ex.Status.StatusCode); } [Test] public void UserAgentStringPresent() { - var callDetails = new CallInvocationDetails<string, string>(channel, EchoMethod, new CallOptions()); - string userAgent = Calls.BlockingUnaryCall(callDetails, "RETURN-USER-AGENT"); + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => + { + return context.RequestHeaders.Where(entry => entry.Key == "user-agent").Single().Value; + }); + + string userAgent = Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "abc"); Assert.IsTrue(userAgent.StartsWith("grpc-csharp/")); } [Test] public void PeerInfoPresent() { - var callDetails = new CallInvocationDetails<string, string>(channel, EchoMethod, new CallOptions()); - string peer = Calls.BlockingUnaryCall(callDetails, "RETURN-PEER"); + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => + { + return context.Peer; + }); + + string peer = Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "abc"); Assert.IsTrue(peer.Contains(Host)); } [Test] public async Task Channel_WaitForStateChangedAsync() { + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => + { + return request; + }); + Assert.Throws(typeof(TaskCanceledException), async () => await channel.WaitForStateChangedAsync(channel.State, DateTime.UtcNow.AddMilliseconds(10))); var stateChangedTask = channel.WaitForStateChangedAsync(channel.State); - var callDetails = new CallInvocationDetails<string, string>(channel, EchoMethod, new CallOptions()); - await Calls.AsyncUnaryCall(callDetails, "abc"); + await Calls.AsyncUnaryCall(helper.CreateUnaryCall(), "abc"); await stateChangedTask; Assert.AreEqual(ChannelState.Ready, channel.State); @@ -300,62 +291,9 @@ namespace Grpc.Core.Tests { await channel.ConnectAsync(); Assert.AreEqual(ChannelState.Ready, channel.State); + await channel.ConnectAsync(DateTime.UtcNow.AddMilliseconds(1000)); Assert.AreEqual(ChannelState.Ready, channel.State); } - - private static async Task<string> EchoHandler(string request, ServerCallContext context) - { - foreach (Metadata.Entry metadataEntry in context.RequestHeaders) - { - if (metadataEntry.Key != "user-agent") - { - context.ResponseTrailers.Add(metadataEntry); - } - } - - if (request == "RETURN-USER-AGENT") - { - return context.RequestHeaders.Where(entry => entry.Key == "user-agent").Single().Value; - } - - if (request == "RETURN-PEER") - { - return context.Peer; - } - - if (request == "THROW") - { - throw new Exception("This was thrown on purpose by a test"); - } - - if (request == "THROW_UNAUTHENTICATED") - { - throw new RpcException(new Status(StatusCode.Unauthenticated, "")); - } - - if (request == "SET_UNAUTHENTICATED") - { - context.Status = new Status(StatusCode.Unauthenticated, ""); - } - - return request; - } - - private static async Task<string> ConcatAndEchoHandler(IAsyncStreamReader<string> requestStream, ServerCallContext context) - { - string result = ""; - await requestStream.ForEach(async (request) => - { - if (request == "THROW") - { - throw new Exception("This was thrown on purpose by a test"); - } - result += request; - }); - // simulate processing takes some time. - await Task.Delay(250); - return result; - } } } diff --git a/src/csharp/Grpc.Core.Tests/CompressionTest.cs b/src/csharp/Grpc.Core.Tests/CompressionTest.cs new file mode 100644 index 0000000000..9547683f60 --- /dev/null +++ b/src/csharp/Grpc.Core.Tests/CompressionTest.cs @@ -0,0 +1,128 @@ +#region Copyright notice and license + +// Copyright 2015, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#endregion + +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; +using Grpc.Core.Internal; +using Grpc.Core.Utils; +using NUnit.Framework; + +namespace Grpc.Core.Tests +{ + public class CompressionTest + { + MockServiceHelper helper; + Server server; + Channel channel; + + [SetUp] + public void Init() + { + helper = new MockServiceHelper(); + + server = helper.GetServer(); + server.Start(); + channel = helper.GetChannel(); + } + + [TearDown] + public void Cleanup() + { + channel.Dispose(); + server.ShutdownAsync().Wait(); + } + + [TestFixtureTearDown] + public void CleanupClass() + { + GrpcEnvironment.Shutdown(); + } + + [Test] + public void WriteOptions_Unary() + { + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => + { + context.WriteOptions = new WriteOptions(WriteFlags.NoCompress); + return request; + }); + + var callOptions = new CallOptions(writeOptions: new WriteOptions(WriteFlags.NoCompress)); + Calls.BlockingUnaryCall(helper.CreateUnaryCall(callOptions), "abc"); + } + + [Test] + public async Task WriteOptions_DuplexStreaming() + { + helper.DuplexStreamingHandler = new DuplexStreamingServerMethod<string, string>(async (requestStream, responseStream, context) => + { + await requestStream.ToListAsync(); + + context.WriteOptions = new WriteOptions(WriteFlags.NoCompress); + + await context.WriteResponseHeadersAsync(new Metadata { { "ascii-header", "abcdefg" } }); + + await responseStream.WriteAsync("X"); + + responseStream.WriteOptions = null; + await responseStream.WriteAsync("Y"); + + responseStream.WriteOptions = new WriteOptions(WriteFlags.NoCompress); + await responseStream.WriteAsync("Z"); + }); + + var callOptions = new CallOptions(writeOptions: new WriteOptions(WriteFlags.NoCompress)); + var call = Calls.AsyncDuplexStreamingCall(helper.CreateDuplexStreamingCall(callOptions)); + + // check that write options from call options are propagated to request stream. + Assert.IsTrue((call.RequestStream.WriteOptions.Flags & WriteFlags.NoCompress) != 0); + + call.RequestStream.WriteOptions = new WriteOptions(); + await call.RequestStream.WriteAsync("A"); + + call.RequestStream.WriteOptions = null; + await call.RequestStream.WriteAsync("B"); + + call.RequestStream.WriteOptions = new WriteOptions(WriteFlags.NoCompress); + await call.RequestStream.WriteAsync("C"); + + await call.RequestStream.CompleteAsync(); + + await call.ResponseStream.ToListAsync(); + } + } +} diff --git a/src/csharp/Grpc.Core.Tests/ContextPropagationTest.cs b/src/csharp/Grpc.Core.Tests/ContextPropagationTest.cs new file mode 100644 index 0000000000..db5f953b0e --- /dev/null +++ b/src/csharp/Grpc.Core.Tests/ContextPropagationTest.cs @@ -0,0 +1,153 @@ +#region Copyright notice and license + +// Copyright 2015, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#endregion + +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; +using Grpc.Core.Internal; +using Grpc.Core.Utils; +using NUnit.Framework; + +namespace Grpc.Core.Tests +{ + public class ContextPropagationTest + { + MockServiceHelper helper; + Server server; + Channel channel; + + [SetUp] + public void Init() + { + helper = new MockServiceHelper(); + + server = helper.GetServer(); + server.Start(); + channel = helper.GetChannel(); + } + + [TearDown] + public void Cleanup() + { + channel.Dispose(); + server.ShutdownAsync().Wait(); + } + + [TestFixtureTearDown] + public void CleanupClass() + { + GrpcEnvironment.Shutdown(); + } + + [Test] + public async Task PropagateCancellation() + { + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => + { + // check that we didn't obtain the default cancellation token. + Assert.IsTrue(context.CancellationToken.CanBeCanceled); + return "PASS"; + }); + + helper.ClientStreamingHandler = new ClientStreamingServerMethod<string, string>(async (requestStream, context) => + { + var propagationToken = context.CreatePropagationToken(); + Assert.IsNotNull(propagationToken.ParentCall); + + var callOptions = new CallOptions(propagationToken: propagationToken); + return await Calls.AsyncUnaryCall(helper.CreateUnaryCall(callOptions), "xyz"); + }); + + var cts = new CancellationTokenSource(); + var call = Calls.AsyncClientStreamingCall(helper.CreateClientStreamingCall(new CallOptions(cancellationToken: cts.Token))); + await call.RequestStream.CompleteAsync(); + Assert.AreEqual("PASS", await call); + } + + [Test] + public async Task PropagateDeadline() + { + var deadline = DateTime.UtcNow.AddDays(7); + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => + { + Assert.IsTrue(context.Deadline < deadline.AddMinutes(1)); + Assert.IsTrue(context.Deadline > deadline.AddMinutes(-1)); + return "PASS"; + }); + + helper.ClientStreamingHandler = new ClientStreamingServerMethod<string, string>(async (requestStream, context) => + { + Assert.Throws(typeof(ArgumentException), () => + { + // Trying to override deadline while propagating deadline from parent call will throw. + Calls.BlockingUnaryCall(helper.CreateUnaryCall( + new CallOptions(deadline: DateTime.UtcNow.AddDays(8), + propagationToken: context.CreatePropagationToken())), ""); + }); + + var callOptions = new CallOptions(propagationToken: context.CreatePropagationToken()); + return await Calls.AsyncUnaryCall(helper.CreateUnaryCall(callOptions), "xyz"); + }); + + var call = Calls.AsyncClientStreamingCall(helper.CreateClientStreamingCall(new CallOptions(deadline: deadline))); + await call.RequestStream.CompleteAsync(); + Assert.AreEqual("PASS", await call); + } + + [Test] + public async Task SuppressDeadlinePropagation() + { + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => + { + Assert.AreEqual(DateTime.MaxValue, context.Deadline); + return "PASS"; + }); + + helper.ClientStreamingHandler = new ClientStreamingServerMethod<string, string>(async (requestStream, context) => + { + Assert.IsTrue(context.CancellationToken.CanBeCanceled); + + var callOptions = new CallOptions(propagationToken: context.CreatePropagationToken(new ContextPropagationOptions(propagateDeadline: false))); + return await Calls.AsyncUnaryCall(helper.CreateUnaryCall(callOptions), "xyz"); + }); + + var cts = new CancellationTokenSource(); + var call = Calls.AsyncClientStreamingCall(helper.CreateClientStreamingCall(new CallOptions(deadline: DateTime.UtcNow.AddDays(7)))); + await call.RequestStream.CompleteAsync(); + Assert.AreEqual("PASS", await call); + } + } +} diff --git a/src/csharp/Grpc.Core.Tests/Grpc.Core.Tests.csproj b/src/csharp/Grpc.Core.Tests/Grpc.Core.Tests.csproj index f2bf459dc5..d6a8f52570 100644 --- a/src/csharp/Grpc.Core.Tests/Grpc.Core.Tests.csproj +++ b/src/csharp/Grpc.Core.Tests/Grpc.Core.Tests.csproj @@ -63,6 +63,7 @@ <Compile Include="..\Grpc.Core\Version.cs"> <Link>Version.cs</Link> </Compile> + <Compile Include="ClientBaseTest.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="ClientServerTest.cs" /> <Compile Include="ServerTest.cs" /> @@ -77,6 +78,10 @@ <Compile Include="TimeoutsTest.cs" /> <Compile Include="NUnitVersionTest.cs" /> <Compile Include="ChannelTest.cs" /> + <Compile Include="MockServiceHelper.cs" /> + <Compile Include="ResponseHeadersTest.cs" /> + <Compile Include="CompressionTest.cs" /> + <Compile Include="ContextPropagationTest.cs" /> </ItemGroup> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> <ItemGroup> diff --git a/src/csharp/Grpc.Core.Tests/GrpcEnvironmentTest.cs b/src/csharp/Grpc.Core.Tests/GrpcEnvironmentTest.cs index 9ae12776f3..4ed93c7eca 100644 --- a/src/csharp/Grpc.Core.Tests/GrpcEnvironmentTest.cs +++ b/src/csharp/Grpc.Core.Tests/GrpcEnvironmentTest.cs @@ -69,5 +69,13 @@ namespace Grpc.Core.Tests Assert.IsFalse(object.ReferenceEquals(env1, env2)); } + + [Test] + public void GetCoreVersionString() + { + var coreVersion = GrpcEnvironment.GetCoreVersionString(); + var parts = coreVersion.Split('.'); + Assert.AreEqual(4, parts.Length); + } } } diff --git a/src/csharp/Grpc.Core.Tests/Internal/MetadataArraySafeHandleTest.cs b/src/csharp/Grpc.Core.Tests/Internal/MetadataArraySafeHandleTest.cs index 46469113c5..33534fdd3c 100644 --- a/src/csharp/Grpc.Core.Tests/Internal/MetadataArraySafeHandleTest.cs +++ b/src/csharp/Grpc.Core.Tests/Internal/MetadataArraySafeHandleTest.cs @@ -53,8 +53,8 @@ namespace Grpc.Core.Internal.Tests { var metadata = new Metadata { - new Metadata.Entry("host", "somehost"), - new Metadata.Entry("header2", "header value"), + { "host", "somehost" }, + { "header2", "header value" }, }; var nativeMetadata = MetadataArraySafeHandle.Create(metadata); nativeMetadata.Dispose(); @@ -65,8 +65,8 @@ namespace Grpc.Core.Internal.Tests { var metadata = new Metadata { - new Metadata.Entry("host", "somehost"), - new Metadata.Entry("header2", "header value"), + { "host", "somehost" }, + { "header2", "header value" } }; var nativeMetadata = MetadataArraySafeHandle.Create(metadata); diff --git a/src/csharp/Grpc.Core.Tests/MockServiceHelper.cs b/src/csharp/Grpc.Core.Tests/MockServiceHelper.cs new file mode 100644 index 0000000000..bb69648d8b --- /dev/null +++ b/src/csharp/Grpc.Core.Tests/MockServiceHelper.cs @@ -0,0 +1,244 @@ +#region Copyright notice and license + +// Copyright 2015, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#endregion + +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; +using Grpc.Core.Internal; +using Grpc.Core.Utils; +using NUnit.Framework; + +namespace Grpc.Core.Tests +{ + /// <summary> + /// Allows setting up a mock service in the client-server tests easily. + /// </summary> + public class MockServiceHelper + { + public const string ServiceName = "tests.Test"; + + public static readonly Method<string, string> UnaryMethod = new Method<string, string>( + MethodType.Unary, + ServiceName, + "Unary", + Marshallers.StringMarshaller, + Marshallers.StringMarshaller); + + public static readonly Method<string, string> ClientStreamingMethod = new Method<string, string>( + MethodType.ClientStreaming, + ServiceName, + "ClientStreaming", + Marshallers.StringMarshaller, + Marshallers.StringMarshaller); + + public static readonly Method<string, string> ServerStreamingMethod = new Method<string, string>( + MethodType.ServerStreaming, + ServiceName, + "ServerStreaming", + Marshallers.StringMarshaller, + Marshallers.StringMarshaller); + + public static readonly Method<string, string> DuplexStreamingMethod = new Method<string, string>( + MethodType.DuplexStreaming, + ServiceName, + "DuplexStreaming", + Marshallers.StringMarshaller, + Marshallers.StringMarshaller); + + readonly string host; + readonly ServerServiceDefinition serviceDefinition; + + UnaryServerMethod<string, string> unaryHandler; + ClientStreamingServerMethod<string, string> clientStreamingHandler; + ServerStreamingServerMethod<string, string> serverStreamingHandler; + DuplexStreamingServerMethod<string, string> duplexStreamingHandler; + + Server server; + Channel channel; + + public MockServiceHelper(string host = null) + { + this.host = host ?? "localhost"; + + serviceDefinition = ServerServiceDefinition.CreateBuilder(ServiceName) + .AddMethod(UnaryMethod, (request, context) => unaryHandler(request, context)) + .AddMethod(ClientStreamingMethod, (requestStream, context) => clientStreamingHandler(requestStream, context)) + .AddMethod(ServerStreamingMethod, (request, responseStream, context) => serverStreamingHandler(request, responseStream, context)) + .AddMethod(DuplexStreamingMethod, (requestStream, responseStream, context) => duplexStreamingHandler(requestStream, responseStream, context)) + .Build(); + + var defaultStatus = new Status(StatusCode.Unknown, "Default mock implementation. Please provide your own."); + + unaryHandler = new UnaryServerMethod<string, string>(async (request, context) => + { + context.Status = defaultStatus; + return ""; + }); + + clientStreamingHandler = new ClientStreamingServerMethod<string, string>(async (requestStream, context) => + { + context.Status = defaultStatus; + return ""; + }); + + serverStreamingHandler = new ServerStreamingServerMethod<string, string>(async (request, responseStream, context) => + { + context.Status = defaultStatus; + }); + + duplexStreamingHandler = new DuplexStreamingServerMethod<string, string>(async (requestStream, responseStream, context) => + { + context.Status = defaultStatus; + }); + } + + /// <summary> + /// Returns the default server for this service and creates one if not yet created. + /// </summary> + public Server GetServer() + { + if (server == null) + { + server = new Server + { + Services = { serviceDefinition }, + Ports = { { Host, ServerPort.PickUnused, ServerCredentials.Insecure } } + }; + } + return server; + } + + /// <summary> + /// Returns the default channel for this service and creates one if not yet created. + /// </summary> + public Channel GetChannel() + { + if (channel == null) + { + channel = new Channel(Host, GetServer().Ports.Single().BoundPort, Credentials.Insecure); + } + return channel; + } + + public CallInvocationDetails<string, string> CreateUnaryCall(CallOptions options = default(CallOptions)) + { + return new CallInvocationDetails<string, string>(channel, UnaryMethod, options); + } + + public CallInvocationDetails<string, string> CreateClientStreamingCall(CallOptions options = default(CallOptions)) + { + return new CallInvocationDetails<string, string>(channel, ClientStreamingMethod, options); + } + + public CallInvocationDetails<string, string> CreateServerStreamingCall(CallOptions options = default(CallOptions)) + { + return new CallInvocationDetails<string, string>(channel, ServerStreamingMethod, options); + } + + public CallInvocationDetails<string, string> CreateDuplexStreamingCall(CallOptions options = default(CallOptions)) + { + return new CallInvocationDetails<string, string>(channel, DuplexStreamingMethod, options); + } + + public string Host + { + get + { + return this.host; + } + } + + public ServerServiceDefinition ServiceDefinition + { + get + { + return this.serviceDefinition; + } + } + + public UnaryServerMethod<string, string> UnaryHandler + { + get + { + return this.unaryHandler; + } + + set + { + unaryHandler = value; + } + } + + public ClientStreamingServerMethod<string, string> ClientStreamingHandler + { + get + { + return this.clientStreamingHandler; + } + + set + { + clientStreamingHandler = value; + } + } + + public ServerStreamingServerMethod<string, string> ServerStreamingHandler + { + get + { + return this.serverStreamingHandler; + } + + set + { + serverStreamingHandler = value; + } + } + + public DuplexStreamingServerMethod<string, string> DuplexStreamingHandler + { + get + { + return this.duplexStreamingHandler; + } + + set + { + duplexStreamingHandler = value; + } + } + } +} diff --git a/src/csharp/Grpc.Core.Tests/ResponseHeadersTest.cs b/src/csharp/Grpc.Core.Tests/ResponseHeadersTest.cs new file mode 100644 index 0000000000..981b8ea3c8 --- /dev/null +++ b/src/csharp/Grpc.Core.Tests/ResponseHeadersTest.cs @@ -0,0 +1,136 @@ +#region Copyright notice and license + +// Copyright 2015, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#endregion + +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; +using Grpc.Core.Internal; +using Grpc.Core.Utils; +using NUnit.Framework; + +namespace Grpc.Core.Tests +{ + /// <summary> + /// Tests for response headers support. + /// </summary> + public class ResponseHeadersTest + { + MockServiceHelper helper; + Server server; + Channel channel; + + Metadata headers; + + [SetUp] + public void Init() + { + helper = new MockServiceHelper(); + + server = helper.GetServer(); + server.Start(); + channel = helper.GetChannel(); + + headers = new Metadata { { "ascii-header", "abcdefg" } }; + } + + [TearDown] + public void Cleanup() + { + channel.Dispose(); + server.ShutdownAsync().Wait(); + } + + [TestFixtureTearDown] + public void CleanupClass() + { + GrpcEnvironment.Shutdown(); + } + + [Test] + public void WriteResponseHeaders_NullNotAllowed() + { + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => + { + Assert.Throws(typeof(ArgumentNullException), async () => await context.WriteResponseHeadersAsync(null)); + return "PASS"; + }); + + Assert.AreEqual("PASS", Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "")); + } + + [Test] + public void WriteResponseHeaders_AllowedOnlyOnce() + { + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => + { + await context.WriteResponseHeadersAsync(headers); + try + { + await context.WriteResponseHeadersAsync(headers); + Assert.Fail(); + } + catch (InvalidOperationException expected) + { + } + return "PASS"; + }); + + Assert.AreEqual("PASS", Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "")); + } + + [Test] + public async Task WriteResponseHeaders_NotAllowedAfterWrite() + { + helper.ServerStreamingHandler = new ServerStreamingServerMethod<string, string>(async (request, responseStream, context) => + { + await responseStream.WriteAsync("A"); + try + { + await context.WriteResponseHeadersAsync(headers); + Assert.Fail(); + } + catch (InvalidOperationException expected) + { + } + await responseStream.WriteAsync("B"); + }); + + var call = Calls.AsyncServerStreamingCall(helper.CreateServerStreamingCall(), ""); + var responses = await call.ResponseStream.ToListAsync(); + CollectionAssert.AreEqual(new[] { "A", "B" }, responses); + } + } +} diff --git a/src/csharp/Grpc.Core.Tests/TimeoutsTest.cs b/src/csharp/Grpc.Core.Tests/TimeoutsTest.cs index fc395b0acd..d875d601b9 100644 --- a/src/csharp/Grpc.Core.Tests/TimeoutsTest.cs +++ b/src/csharp/Grpc.Core.Tests/TimeoutsTest.cs @@ -48,38 +48,18 @@ namespace Grpc.Core.Tests /// </summary> public class TimeoutsTest { - const string Host = "localhost"; - const string ServiceName = "tests.Test"; - - static readonly Method<string, string> TestMethod = new Method<string, string>( - MethodType.Unary, - ServiceName, - "Test", - Marshallers.StringMarshaller, - Marshallers.StringMarshaller); - - static readonly ServerServiceDefinition ServiceDefinition = ServerServiceDefinition.CreateBuilder(ServiceName) - .AddMethod(TestMethod, TestMethodHandler) - .Build(); - - // provides a way how to retrieve an out-of-band result value from server handler - static TaskCompletionSource<string> stringFromServerHandlerTcs; - + MockServiceHelper helper; Server server; Channel channel; [SetUp] public void Init() { - server = new Server - { - Services = { ServiceDefinition }, - Ports = { { Host, ServerPort.PickUnused, ServerCredentials.Insecure } } - }; - server.Start(); - channel = new Channel(Host, server.Ports.Single().BoundPort, Credentials.Insecure); + helper = new MockServiceHelper(); - stringFromServerHandlerTcs = new TaskCompletionSource<string>(); + server = helper.GetServer(); + server.Start(); + channel = helper.GetChannel(); } [TearDown] @@ -98,115 +78,83 @@ namespace Grpc.Core.Tests [Test] public void InfiniteDeadline() { + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => + { + Assert.AreEqual(DateTime.MaxValue, context.Deadline); + return "PASS"; + }); + // no deadline specified, check server sees infinite deadline - var callDetails = new CallInvocationDetails<string, string>(channel, TestMethod, new CallOptions()); - Assert.AreEqual("DATETIME_MAXVALUE", Calls.BlockingUnaryCall(callDetails, "RETURN_DEADLINE")); + Assert.AreEqual("PASS", Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "abc")); // DateTime.MaxValue deadline specified, check server sees infinite deadline - var callDetails2 = new CallInvocationDetails<string, string>(channel, TestMethod, new CallOptions()); - Assert.AreEqual("DATETIME_MAXVALUE", Calls.BlockingUnaryCall(callDetails2, "RETURN_DEADLINE")); + Assert.AreEqual("PASS", Calls.BlockingUnaryCall(helper.CreateUnaryCall(new CallOptions(deadline: DateTime.MaxValue)), "abc")); } [Test] public void DeadlineTransferredToServer() { - var remainingTimeClient = TimeSpan.FromDays(7); - var deadline = DateTime.UtcNow + remainingTimeClient; - Thread.Sleep(1000); - var callDetails = new CallInvocationDetails<string, string>(channel, TestMethod, new CallOptions(deadline: deadline)); - - var serverDeadlineTicksString = Calls.BlockingUnaryCall(callDetails, "RETURN_DEADLINE"); - var serverDeadline = new DateTime(long.Parse(serverDeadlineTicksString), DateTimeKind.Utc); - - // A fairly relaxed check that the deadline set by client and deadline seen by server - // are in agreement. C core takes care of the work with transferring deadline over the wire, - // so we don't need an exact check here. - Assert.IsTrue(Math.Abs((deadline - serverDeadline).TotalMilliseconds) < 5000); + var clientDeadline = DateTime.UtcNow + TimeSpan.FromDays(7); + + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => + { + // A fairly relaxed check that the deadline set by client and deadline seen by server + // are in agreement. C core takes care of the work with transferring deadline over the wire, + // so we don't need an exact check here. + Assert.IsTrue(Math.Abs((clientDeadline - context.Deadline).TotalMilliseconds) < 5000); + return "PASS"; + }); + Calls.BlockingUnaryCall(helper.CreateUnaryCall(new CallOptions(deadline: clientDeadline)), "abc"); } [Test] public void DeadlineInThePast() { - var callDetails = new CallInvocationDetails<string, string>(channel, TestMethod, new CallOptions(deadline: DateTime.MinValue)); - - try + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => { - Calls.BlockingUnaryCall(callDetails, "TIMEOUT"); - Assert.Fail(); - } - catch (RpcException e) - { - // We can't guarantee the status code always DeadlineExceeded. See issue #2685. - Assert.Contains(e.Status.StatusCode, new[] { StatusCode.DeadlineExceeded, StatusCode.Internal }); - } + await Task.Delay(60000); + return "FAIL"; + }); + + var ex = Assert.Throws<RpcException>(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(new CallOptions(deadline: DateTime.MinValue)), "abc")); + // We can't guarantee the status code always DeadlineExceeded. See issue #2685. + Assert.Contains(ex.Status.StatusCode, new[] { StatusCode.DeadlineExceeded, StatusCode.Internal }); } [Test] public void DeadlineExceededStatusOnTimeout() { - var deadline = DateTime.UtcNow.Add(TimeSpan.FromSeconds(5)); - var callDetails = new CallInvocationDetails<string, string>(channel, TestMethod, new CallOptions(deadline: deadline)); - - try + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => { - Calls.BlockingUnaryCall(callDetails, "TIMEOUT"); - Assert.Fail(); - } - catch (RpcException e) - { - // We can't guarantee the status code always DeadlineExceeded. See issue #2685. - Assert.Contains(e.Status.StatusCode, new[] { StatusCode.DeadlineExceeded, StatusCode.Internal }); - } + await Task.Delay(60000); + return "FAIL"; + }); + + var ex = Assert.Throws<RpcException>(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(new CallOptions(deadline: DateTime.UtcNow.Add(TimeSpan.FromSeconds(5)))), "abc")); + // We can't guarantee the status code always DeadlineExceeded. See issue #2685. + Assert.Contains(ex.Status.StatusCode, new[] { StatusCode.DeadlineExceeded, StatusCode.Internal }); } [Test] - public void ServerReceivesCancellationOnTimeout() + public async Task ServerReceivesCancellationOnTimeout() { - var deadline = DateTime.UtcNow.Add(TimeSpan.FromSeconds(5)); - var callDetails = new CallInvocationDetails<string, string>(channel, TestMethod, new CallOptions(deadline: deadline)); + var serverReceivedCancellationTcs = new TaskCompletionSource<bool>(); - try - { - Calls.BlockingUnaryCall(callDetails, "CHECK_CANCELLATION_RECEIVED"); - Assert.Fail(); - } - catch (RpcException e) - { - // We can't guarantee the status code is always DeadlineExceeded. See issue #2685. - Assert.Contains(e.Status.StatusCode, new[] { StatusCode.DeadlineExceeded, StatusCode.Internal }); - } - Assert.AreEqual("CANCELLED", stringFromServerHandlerTcs.Task.Result); - } - - private static async Task<string> TestMethodHandler(string request, ServerCallContext context) - { - if (request == "TIMEOUT") - { - await Task.Delay(60000); - return ""; - } - - if (request == "RETURN_DEADLINE") - { - if (context.Deadline == DateTime.MaxValue) - { - return "DATETIME_MAXVALUE"; - } - - return context.Deadline.Ticks.ToString(); - } - - if (request == "CHECK_CANCELLATION_RECEIVED") + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => { // wait until cancellation token is fired. var tcs = new TaskCompletionSource<object>(); context.CancellationToken.Register(() => { tcs.SetResult(null); }); await tcs.Task; - stringFromServerHandlerTcs.SetResult("CANCELLED"); + serverReceivedCancellationTcs.SetResult(true); return ""; - } + }); + + var ex = Assert.Throws<RpcException>(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(new CallOptions(deadline: DateTime.UtcNow.Add(TimeSpan.FromSeconds(5)))), "abc")); + // We can't guarantee the status code always DeadlineExceeded. See issue #2685. + Assert.Contains(ex.Status.StatusCode, new[] { StatusCode.DeadlineExceeded, StatusCode.Internal }); - return ""; + Assert.IsTrue(await serverReceivedCancellationTcs.Task); } } } diff --git a/src/csharp/Grpc.Core/CallInvocationDetails.cs b/src/csharp/Grpc.Core/CallInvocationDetails.cs index eb23a3a209..6565073fc5 100644 --- a/src/csharp/Grpc.Core/CallInvocationDetails.cs +++ b/src/csharp/Grpc.Core/CallInvocationDetails.cs @@ -40,30 +40,60 @@ namespace Grpc.Core /// <summary> /// Details about a client-side call to be invoked. /// </summary> - public class CallInvocationDetails<TRequest, TResponse> + public struct CallInvocationDetails<TRequest, TResponse> { readonly Channel channel; readonly string method; readonly string host; readonly Marshaller<TRequest> requestMarshaller; readonly Marshaller<TResponse> responseMarshaller; - readonly CallOptions options; + CallOptions options; + /// <summary> + /// Initializes a new instance of the <see cref="Grpc.Core.CallInvocationDetails`2"/> struct. + /// </summary> + /// <param name="channel">Channel to use for this call.</param> + /// <param name="method">Method to call.</param> + /// <param name="options">Call options.</param> public CallInvocationDetails(Channel channel, Method<TRequest, TResponse> method, CallOptions options) : - this(channel, method.FullName, null, method.RequestMarshaller, method.ResponseMarshaller, options) + this(channel, method, null, options) { } + /// <summary> + /// Initializes a new instance of the <see cref="Grpc.Core.CallInvocationDetails`2"/> struct. + /// </summary> + /// <param name="channel">Channel to use for this call.</param> + /// <param name="method">Method to call.</param> + /// <param name="host">Host that contains the method. if <c>null</c>, default host will be used.</param> + /// <param name="options">Call options.</param> + public CallInvocationDetails(Channel channel, Method<TRequest, TResponse> method, string host, CallOptions options) : + this(channel, method.FullName, host, method.RequestMarshaller, method.ResponseMarshaller, options) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="Grpc.Core.CallInvocationDetails`2"/> struct. + /// </summary> + /// <param name="channel">Channel to use for this call.</param> + /// <param name="method">Qualified method name.</param> + /// <param name="host">Host that contains the method.</param> + /// <param name="requestMarshaller">Request marshaller.</param> + /// <param name="responseMarshaller">Response marshaller.</param> + /// <param name="options">Call options.</param> public CallInvocationDetails(Channel channel, string method, string host, Marshaller<TRequest> requestMarshaller, Marshaller<TResponse> responseMarshaller, CallOptions options) { - this.channel = Preconditions.CheckNotNull(channel); - this.method = Preconditions.CheckNotNull(method); + this.channel = Preconditions.CheckNotNull(channel, "channel"); + this.method = Preconditions.CheckNotNull(method, "method"); this.host = host; - this.requestMarshaller = Preconditions.CheckNotNull(requestMarshaller); - this.responseMarshaller = Preconditions.CheckNotNull(responseMarshaller); - this.options = Preconditions.CheckNotNull(options); + this.requestMarshaller = Preconditions.CheckNotNull(requestMarshaller, "requestMarshaller"); + this.responseMarshaller = Preconditions.CheckNotNull(responseMarshaller, "responseMarshaller"); + this.options = options; } + /// <summary> + /// Get channel associated with this call. + /// </summary> public Channel Channel { get @@ -72,6 +102,9 @@ namespace Grpc.Core } } + /// <summary> + /// Gets name of method to be called. + /// </summary> public string Method { get @@ -80,6 +113,9 @@ namespace Grpc.Core } } + /// <summary> + /// Get name of host. + /// </summary> public string Host { get @@ -88,6 +124,9 @@ namespace Grpc.Core } } + /// <summary> + /// Gets marshaller used to serialize requests. + /// </summary> public Marshaller<TRequest> RequestMarshaller { get @@ -96,6 +135,9 @@ namespace Grpc.Core } } + /// <summary> + /// Gets marshaller used to deserialized responses. + /// </summary> public Marshaller<TResponse> ResponseMarshaller { get @@ -104,6 +146,9 @@ namespace Grpc.Core } } + /// <summary> + /// Gets the call options. + /// </summary> public CallOptions Options { get @@ -111,5 +156,16 @@ namespace Grpc.Core return options; } } + + /// <summary> + /// Returns new instance of <see cref="CallInvocationDetails"/> with + /// <c>Options</c> set to the value provided. Values of all other fields are preserved. + /// </summary> + public CallInvocationDetails<TRequest, TResponse> WithOptions(CallOptions options) + { + var newDetails = this; + newDetails.options = options; + return newDetails; + } } } diff --git a/src/csharp/Grpc.Core/CallOptions.cs b/src/csharp/Grpc.Core/CallOptions.cs index 8e9739335f..3dfe80b48c 100644 --- a/src/csharp/Grpc.Core/CallOptions.cs +++ b/src/csharp/Grpc.Core/CallOptions.cs @@ -42,24 +42,30 @@ namespace Grpc.Core /// <summary> /// Options for calls made by client. /// </summary> - public class CallOptions + public struct CallOptions { - readonly Metadata headers; - readonly DateTime deadline; - readonly CancellationToken cancellationToken; + Metadata headers; + DateTime? deadline; + CancellationToken cancellationToken; + WriteOptions writeOptions; + ContextPropagationToken propagationToken; /// <summary> - /// Creates a new instance of <c>CallOptions</c>. + /// Creates a new instance of <c>CallOptions</c> struct. /// </summary> /// <param name="headers">Headers to be sent with the call.</param> /// <param name="deadline">Deadline for the call to finish. null means no deadline.</param> /// <param name="cancellationToken">Can be used to request cancellation of the call.</param> - public CallOptions(Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default(CancellationToken)) + /// <param name="writeOptions">Write options that will be used for this call.</param> + /// <param name="propagationToken">Context propagation token obtained from <see cref="ServerCallContext"/>.</param> + public CallOptions(Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default(CancellationToken), + WriteOptions writeOptions = null, ContextPropagationToken propagationToken = null) { - // TODO(jtattermusch): consider only creating metadata object once it's really needed. - this.headers = headers != null ? headers : new Metadata(); - this.deadline = deadline.HasValue ? deadline.Value : DateTime.MaxValue; + this.headers = headers; + this.deadline = deadline; this.cancellationToken = cancellationToken; + this.writeOptions = writeOptions; + this.propagationToken = propagationToken; } /// <summary> @@ -73,7 +79,7 @@ namespace Grpc.Core /// <summary> /// Call deadline. /// </summary> - public DateTime Deadline + public DateTime? Deadline { get { return deadline; } } @@ -85,5 +91,88 @@ namespace Grpc.Core { get { return cancellationToken; } } + + /// <summary> + /// Write options that will be used for this call. + /// </summary> + public WriteOptions WriteOptions + { + get + { + return this.writeOptions; + } + } + + /// <summary> + /// Token for propagating parent call context. + /// </summary> + public ContextPropagationToken PropagationToken + { + get + { + return this.propagationToken; + } + } + + /// <summary> + /// Returns new instance of <see cref="CallOptions"/> with + /// <c>Headers</c> set to the value provided. Values of all other fields are preserved. + /// </summary> + public CallOptions WithHeaders(Metadata headers) + { + var newOptions = this; + newOptions.headers = headers; + return newOptions; + } + + /// <summary> + /// Returns new instance of <see cref="CallOptions"/> with + /// <c>Deadline</c> set to the value provided. Values of all other fields are preserved. + /// </summary> + public CallOptions WithDeadline(DateTime deadline) + { + var newOptions = this; + newOptions.deadline = deadline; + return newOptions; + } + + /// <summary> + /// Returns new instance of <see cref="CallOptions"/> with + /// <c>CancellationToken</c> set to the value provided. Values of all other fields are preserved. + /// </summary> + public CallOptions WithCancellationToken(CancellationToken cancellationToken) + { + var newOptions = this; + newOptions.cancellationToken = cancellationToken; + return newOptions; + } + + /// <summary> + /// Returns a new instance of <see cref="CallOptions"/> with + /// all previously unset values set to their defaults and deadline and cancellation + /// token propagated when appropriate. + /// </summary> + internal CallOptions Normalize() + { + var newOptions = this; + if (propagationToken != null) + { + if (propagationToken.Options.IsPropagateDeadline) + { + Preconditions.CheckArgument(!newOptions.deadline.HasValue, + "Cannot propagate deadline from parent call. The deadline has already been set explicitly."); + newOptions.deadline = propagationToken.ParentDeadline; + } + if (propagationToken.Options.IsPropagateCancellation) + { + Preconditions.CheckArgument(!newOptions.cancellationToken.CanBeCanceled, + "Cannot propagate cancellation token from parent call. The cancellation token has already been set to a non-default value."); + } + } + + newOptions.headers = newOptions.headers ?? Metadata.Empty; + newOptions.deadline = newOptions.deadline ?? DateTime.MaxValue; + return newOptions; + } } } diff --git a/src/csharp/Grpc.Core/Calls.cs b/src/csharp/Grpc.Core/Calls.cs index 00a8cabf82..7067456638 100644 --- a/src/csharp/Grpc.Core/Calls.cs +++ b/src/csharp/Grpc.Core/Calls.cs @@ -31,8 +31,6 @@ #endregion -using System; -using System.Threading; using System.Threading.Tasks; using Grpc.Core.Internal; @@ -40,9 +38,20 @@ namespace Grpc.Core { /// <summary> /// Helper methods for generated clients to make RPC calls. + /// Most users will use this class only indirectly and will be + /// making calls using client object generated from protocol + /// buffer definition files. /// </summary> public static class Calls { + /// <summary> + /// Invokes a simple remote call in a blocking fashion. + /// </summary> + /// <returns>The response.</returns> + /// <param name="call">The call defintion.</param> + /// <param name="req">Request message.</param> + /// <typeparam name="TRequest">Type of request message.</typeparam> + /// <typeparam name="TResponse">The of response message.</typeparam> public static TResponse BlockingUnaryCall<TRequest, TResponse>(CallInvocationDetails<TRequest, TResponse> call, TRequest req) where TRequest : class where TResponse : class @@ -51,6 +60,14 @@ namespace Grpc.Core return asyncCall.UnaryCall(req); } + /// <summary> + /// Invokes a simple remote call asynchronously. + /// </summary> + /// <returns>An awaitable call object providing access to the response.</returns> + /// <param name="call">The call defintion.</param> + /// <param name="req">Request message.</param> + /// <typeparam name="TRequest">Type of request message.</typeparam> + /// <typeparam name="TResponse">The of response message.</typeparam> public static AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(CallInvocationDetails<TRequest, TResponse> call, TRequest req) where TRequest : class where TResponse : class @@ -60,6 +77,15 @@ namespace Grpc.Core return new AsyncUnaryCall<TResponse>(asyncResult, asyncCall.GetStatus, asyncCall.GetTrailers, asyncCall.Cancel); } + /// <summary> + /// Invokes a server streaming call asynchronously. + /// In server streaming scenario, client sends on request and server responds with a stream of responses. + /// </summary> + /// <returns>A call object providing access to the asynchronous response stream.</returns> + /// <param name="call">The call defintion.</param> + /// <param name="req">Request message.</param> + /// <typeparam name="TRequest">Type of request message.</typeparam> + /// <typeparam name="TResponse">The of response messages.</typeparam> public static AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>(CallInvocationDetails<TRequest, TResponse> call, TRequest req) where TRequest : class where TResponse : class @@ -70,6 +96,13 @@ namespace Grpc.Core return new AsyncServerStreamingCall<TResponse>(responseStream, asyncCall.GetStatus, asyncCall.GetTrailers, asyncCall.Cancel); } + /// <summary> + /// Invokes a client streaming call asynchronously. + /// In client streaming scenario, client sends a stream of requests and server responds with a single response. + /// </summary> + /// <returns>An awaitable call object providing access to the response.</returns> + /// <typeparam name="TRequest">Type of request messages.</typeparam> + /// <typeparam name="TResponse">The of response message.</typeparam> public static AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>(CallInvocationDetails<TRequest, TResponse> call) where TRequest : class where TResponse : class @@ -80,6 +113,15 @@ namespace Grpc.Core return new AsyncClientStreamingCall<TRequest, TResponse>(requestStream, resultTask, asyncCall.GetStatus, asyncCall.GetTrailers, asyncCall.Cancel); } + /// <summary> + /// Invokes a duplex streaming call asynchronously. + /// In duplex streaming scenario, client sends a stream of requests and server responds with a stream of responses. + /// The response stream is completely independent and both side can be sending messages at the same time. + /// </summary> + /// <returns>A call object providing access to the asynchronous request and response streams.</returns> + /// <param name="call">The call definition.</param> + /// <typeparam name="TRequest">Type of request messages.</typeparam> + /// <typeparam name="TResponse">Type of reponse messages.</typeparam> public static AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>(CallInvocationDetails<TRequest, TResponse> call) where TRequest : class where TResponse : class diff --git a/src/csharp/Grpc.Core/Channel.cs b/src/csharp/Grpc.Core/Channel.cs index 9273ea4582..64c6adf2bf 100644 --- a/src/csharp/Grpc.Core/Channel.cs +++ b/src/csharp/Grpc.Core/Channel.cs @@ -49,6 +49,7 @@ namespace Grpc.Core { static readonly ILogger Logger = GrpcEnvironment.Logger.ForType<Channel>(); + readonly string target; readonly GrpcEnvironment environment; readonly ChannelSafeHandle handle; readonly List<ChannelOption> options; @@ -58,12 +59,12 @@ namespace Grpc.Core /// Creates a channel that connects to a specific host. /// Port will default to 80 for an unsecure channel and to 443 for a secure channel. /// </summary> - /// <param name="host">The name or IP address of the host.</param> + /// <param name="target">Target of the channel.</param> /// <param name="credentials">Credentials to secure the channel.</param> /// <param name="options">Channel options.</param> - public Channel(string host, Credentials credentials, IEnumerable<ChannelOption> options = null) + public Channel(string target, Credentials credentials, IEnumerable<ChannelOption> options = null) { - Preconditions.CheckNotNull(host); + this.target = Preconditions.CheckNotNull(target, "target"); this.environment = GrpcEnvironment.GetInstance(); this.options = options != null ? new List<ChannelOption>(options) : new List<ChannelOption>(); @@ -73,11 +74,11 @@ namespace Grpc.Core { if (nativeCredentials != null) { - this.handle = ChannelSafeHandle.CreateSecure(nativeCredentials, host, nativeChannelArgs); + this.handle = ChannelSafeHandle.CreateSecure(nativeCredentials, target, nativeChannelArgs); } else { - this.handle = ChannelSafeHandle.CreateInsecure(host, nativeChannelArgs); + this.handle = ChannelSafeHandle.CreateInsecure(target, nativeChannelArgs); } } } @@ -131,8 +132,8 @@ namespace Grpc.Core return tcs.Task; } - /// <summary> Address of the remote endpoint in URI format.</summary> - public string Target + /// <summary>Resolved address of the remote endpoint in URI format.</summary> + public string ResolvedTarget { get { @@ -140,6 +141,15 @@ namespace Grpc.Core } } + /// <summary>The original target used to create the channel.</summary> + public string Target + { + get + { + return this.target; + } + } + /// <summary> /// Allows explicitly requesting channel to connect without starting an RPC. /// Returned task completes once state Ready was seen. If the deadline is reached, diff --git a/src/csharp/Grpc.Core/ChannelOptions.cs b/src/csharp/Grpc.Core/ChannelOptions.cs index 1e0f90287a..0cb2953f2c 100644 --- a/src/csharp/Grpc.Core/ChannelOptions.cs +++ b/src/csharp/Grpc.Core/ChannelOptions.cs @@ -63,8 +63,8 @@ namespace Grpc.Core public ChannelOption(string name, string stringValue) { this.type = OptionType.String; - this.name = Preconditions.CheckNotNull(name); - this.stringValue = Preconditions.CheckNotNull(stringValue); + this.name = Preconditions.CheckNotNull(name, "name"); + this.stringValue = Preconditions.CheckNotNull(stringValue, "stringValue"); } /// <summary> @@ -75,7 +75,7 @@ namespace Grpc.Core public ChannelOption(string name, int intValue) { this.type = OptionType.Integer; - this.name = Preconditions.CheckNotNull(name); + this.name = Preconditions.CheckNotNull(name, "name"); this.intValue = intValue; } diff --git a/src/csharp/Grpc.Core/ClientBase.cs b/src/csharp/Grpc.Core/ClientBase.cs index 88494bb4ac..f240d777b9 100644 --- a/src/csharp/Grpc.Core/ClientBase.cs +++ b/src/csharp/Grpc.Core/ClientBase.cs @@ -33,23 +33,30 @@ using System; using System.Collections.Generic; +using System.Text.RegularExpressions; using Grpc.Core.Internal; +using Grpc.Core.Utils; namespace Grpc.Core { - public delegate void MetadataInterceptorDelegate(Metadata metadata); + public delegate void MetadataInterceptorDelegate(string authUri, Metadata metadata); /// <summary> /// Base class for client-side stubs. /// </summary> public abstract class ClientBase { + // Regex for removal of the optional DNS scheme, trailing port, and trailing backslash + static readonly Regex ChannelTargetPattern = new Regex(@"^(dns:\/{3})?([^:\/]+)(:\d+)?\/?$"); + readonly Channel channel; + readonly string authUriBase; public ClientBase(Channel channel) { this.channel = channel; + this.authUriBase = GetAuthUriBase(channel.Target); } /// <summary> @@ -63,6 +70,18 @@ namespace Grpc.Core } /// <summary> + /// gRPC supports multiple "hosts" being served by a single server. + /// This property can be used to set the target host explicitly. + /// By default, this will be set to <c>null</c> with the meaning + /// "use default host". + /// </summary> + public string Host + { + get; + set; + } + + /// <summary> /// Channel associated with this client. /// </summary> public Channel Channel @@ -83,10 +102,27 @@ namespace Grpc.Core var interceptor = HeaderInterceptor; if (interceptor != null) { - interceptor(options.Headers); - options.Headers.Freeze(); + if (options.Headers == null) + { + options = options.WithHeaders(new Metadata()); + } + var authUri = authUriBase != null ? authUriBase + method.ServiceName : null; + interceptor(authUri, options.Headers); + } + return new CallInvocationDetails<TRequest, TResponse>(channel, method, Host, options); + } + + /// <summary> + /// Creates Auth URI base from channel's target (the one passed at channel creation). + /// Fully-qualified service name is to be appended to this. + /// </summary> + internal static string GetAuthUriBase(string target) + { + var match = ChannelTargetPattern.Match(target); + if (!match.Success) { + return null; } - return new CallInvocationDetails<TRequest, TResponse>(channel, method, options); + return "https://" + match.Groups[2].Value + "/"; } } } diff --git a/src/csharp/Grpc.Core/OperationFailedException.cs b/src/csharp/Grpc.Core/CompressionLevel.cs index 9b1c24d0c1..399652b85e 100644 --- a/src/csharp/Grpc.Core/OperationFailedException.cs +++ b/src/csharp/Grpc.Core/CompressionLevel.cs @@ -36,12 +36,28 @@ using System; namespace Grpc.Core { /// <summary> - /// Thrown when gRPC operation fails. + /// Compression level based on grpc_compression_level from grpc/compression.h /// </summary> - public class OperationFailedException : Exception + public enum CompressionLevel { - public OperationFailedException(string message) : base(message) - { - } + /// <summary> + /// No compression. + /// </summary> + None = 0, + + /// <summary> + /// Low compression. + /// </summary> + Low, + + /// <summary> + /// Medium compression. + /// </summary> + Medium, + + /// <summary> + /// High compression. + /// </summary> + High, } } diff --git a/src/csharp/Grpc.Core/ContextPropagationToken.cs b/src/csharp/Grpc.Core/ContextPropagationToken.cs new file mode 100644 index 0000000000..2e4bfc9e47 --- /dev/null +++ b/src/csharp/Grpc.Core/ContextPropagationToken.cs @@ -0,0 +1,171 @@ +#region Copyright notice and license + +// Copyright 2015, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#endregion + +using System; +using System.Threading; + +using Grpc.Core.Internal; +using Grpc.Core.Utils; + +namespace Grpc.Core +{ + /// <summary> + /// Token for propagating context of server side handlers to child calls. + /// In situations when a backend is making calls to another backend, + /// it makes sense to propagate properties like deadline and cancellation + /// token of the server call to the child call. + /// C core provides some other contexts (like tracing context) that + /// are not accessible to C# layer, but this token still allows propagating them. + /// </summary> + public class ContextPropagationToken + { + /// <summary> + /// Default propagation mask used by C core. + /// </summary> + private const ContextPropagationFlags DefaultCoreMask = (ContextPropagationFlags)0xffff; + + /// <summary> + /// Default propagation mask used by C# - we want to propagate deadline + /// and cancellation token by our own means. + /// </summary> + internal const ContextPropagationFlags DefaultMask = DefaultCoreMask + & ~ContextPropagationFlags.Deadline & ~ContextPropagationFlags.Cancellation; + + readonly CallSafeHandle parentCall; + readonly DateTime deadline; + readonly CancellationToken cancellationToken; + readonly ContextPropagationOptions options; + + internal ContextPropagationToken(CallSafeHandle parentCall, DateTime deadline, CancellationToken cancellationToken, ContextPropagationOptions options) + { + this.parentCall = Preconditions.CheckNotNull(parentCall); + this.deadline = deadline; + this.cancellationToken = cancellationToken; + this.options = options ?? ContextPropagationOptions.Default; + } + + /// <summary> + /// Gets the native handle of the parent call. + /// </summary> + internal CallSafeHandle ParentCall + { + get + { + return this.parentCall; + } + } + + /// <summary> + /// Gets the parent call's deadline. + /// </summary> + internal DateTime ParentDeadline + { + get + { + return this.deadline; + } + } + + /// <summary> + /// Gets the parent call's cancellation token. + /// </summary> + internal CancellationToken ParentCancellationToken + { + get + { + return this.cancellationToken; + } + } + + /// <summary> + /// Get the context propagation options. + /// </summary> + internal ContextPropagationOptions Options + { + get + { + return this.options; + } + } + } + + /// <summary> + /// Options for <see cref="ContextPropagationToken"/>. + /// </summary> + public class ContextPropagationOptions + { + /// <summary> + /// The context propagation options that will be used by default. + /// </summary> + public static readonly ContextPropagationOptions Default = new ContextPropagationOptions(); + + bool propagateDeadline; + bool propagateCancellation; + + + /// <summary> + /// Creates new context propagation options. + /// </summary> + /// <param name="propagateDeadline">If set to <c>true</c> parent call's deadline will be propagated to the child call.</param> + /// <param name="propagateCancellation">If set to <c>true</c> parent call's cancellation token will be propagated to the child call.</param> + public ContextPropagationOptions(bool propagateDeadline = true, bool propagateCancellation = true) + { + this.propagateDeadline = propagateDeadline; + this.propagateCancellation = propagateCancellation; + } + + /// <value><c>true</c> if parent call's deadline should be propagated to the child call.</value> + public bool IsPropagateDeadline + { + get { return this.propagateDeadline; } + } + + /// <value><c>true</c> if parent call's cancellation token should be propagated to the child call.</value> + public bool IsPropagateCancellation + { + get { return this.propagateCancellation; } + } + } + + /// <summary> + /// Context propagation flags from grpc/grpc.h. + /// </summary> + [Flags] + internal enum ContextPropagationFlags + { + Deadline = 1, + CensusStatsContext = 2, + CensusTracingContext = 4, + Cancellation = 8 + } +} diff --git a/src/csharp/Grpc.Core/Grpc.Core.csproj b/src/csharp/Grpc.Core/Grpc.Core.csproj index 52defd1965..055aff1444 100644 --- a/src/csharp/Grpc.Core/Grpc.Core.csproj +++ b/src/csharp/Grpc.Core/Grpc.Core.csproj @@ -77,14 +77,12 @@ <Compile Include="ServerServiceDefinition.cs" /> <Compile Include="Utils\AsyncStreamExtensions.cs" /> <Compile Include="Utils\BenchmarkUtil.cs" /> - <Compile Include="Utils\ExceptionHelper.cs" /> <Compile Include="Internal\CredentialsSafeHandle.cs" /> <Compile Include="Credentials.cs" /> <Compile Include="Internal\ChannelArgsSafeHandle.cs" /> <Compile Include="Internal\AsyncCompletion.cs" /> <Compile Include="Internal\AsyncCallBase.cs" /> <Compile Include="Internal\AsyncCallServer.cs" /> - <Compile Include="OperationFailedException.cs" /> <Compile Include="Internal\AsyncCall.cs" /> <Compile Include="Utils\Preconditions.cs" /> <Compile Include="Internal\ServerCredentialsSafeHandle.cs" /> @@ -115,6 +113,9 @@ <Compile Include="ChannelState.cs" /> <Compile Include="CallInvocationDetails.cs" /> <Compile Include="CallOptions.cs" /> + <Compile Include="CompressionLevel.cs" /> + <Compile Include="WriteOptions.cs" /> + <Compile Include="ContextPropagationToken.cs" /> </ItemGroup> <ItemGroup> <None Include="Grpc.Core.nuspec" /> diff --git a/src/csharp/Grpc.Core/GrpcEnvironment.cs b/src/csharp/Grpc.Core/GrpcEnvironment.cs index 034a66be3c..30d8c80235 100644 --- a/src/csharp/Grpc.Core/GrpcEnvironment.cs +++ b/src/csharp/Grpc.Core/GrpcEnvironment.cs @@ -53,6 +53,9 @@ namespace Grpc.Core [DllImport("grpc_csharp_ext.dll")] static extern void grpcsharp_shutdown(); + [DllImport("grpc_csharp_ext.dll")] + static extern IntPtr grpcsharp_version_string(); // returns not-owned const char* + static object staticLock = new object(); static GrpcEnvironment instance; @@ -112,7 +115,7 @@ namespace Grpc.Core /// </summary> public static void SetLogger(ILogger customLogger) { - Preconditions.CheckNotNull(customLogger); + Preconditions.CheckNotNull(customLogger, "customLogger"); logger = customLogger; } @@ -164,6 +167,15 @@ namespace Grpc.Core } /// <summary> + /// Gets version of gRPC C core. + /// </summary> + internal static string GetCoreVersionString() + { + var ptr = grpcsharp_version_string(); // the pointer is not owned + return Marshal.PtrToStringAnsi(ptr); + } + + /// <summary> /// Shuts down this environment. /// </summary> private void Close() @@ -180,23 +192,5 @@ namespace Grpc.Core Logger.Info("gRPC shutdown."); } - - /// <summary> - /// Shuts down this environment asynchronously. - /// </summary> - private Task CloseAsync() - { - return Task.Run(() => - { - try - { - Close(); - } - catch (Exception e) - { - Logger.Error(e, "Error occured while shutting down GrpcEnvironment."); - } - }); - } } } diff --git a/src/csharp/Grpc.Core/IAsyncStreamReader.cs b/src/csharp/Grpc.Core/IAsyncStreamReader.cs index 371fbf27ce..c0a0674e50 100644 --- a/src/csharp/Grpc.Core/IAsyncStreamReader.cs +++ b/src/csharp/Grpc.Core/IAsyncStreamReader.cs @@ -43,7 +43,7 @@ namespace Grpc.Core /// A stream of messages to be read. /// </summary> /// <typeparam name="T"></typeparam> - public interface IAsyncStreamReader<TResponse> : IAsyncEnumerator<TResponse> + public interface IAsyncStreamReader<T> : IAsyncEnumerator<T> { // TODO(jtattermusch): consider just using IAsyncEnumerator instead of this interface. } diff --git a/src/csharp/Grpc.Core/IAsyncStreamWriter.cs b/src/csharp/Grpc.Core/IAsyncStreamWriter.cs index 2000210252..4e2acb9c71 100644 --- a/src/csharp/Grpc.Core/IAsyncStreamWriter.cs +++ b/src/csharp/Grpc.Core/IAsyncStreamWriter.cs @@ -50,5 +50,13 @@ namespace Grpc.Core /// </summary> /// <param name="message">the message to be written. Cannot be null.</param> Task WriteAsync(T message); + + /// <summary> + /// Write options that will be used for the next write. + /// If null, default options will be used. + /// Once set, this property maintains its value across subsequent + /// writes. + /// <value>The write options.</value> + WriteOptions WriteOptions { get; set; } } } diff --git a/src/csharp/Grpc.Core/Internal/AsyncCall.cs b/src/csharp/Grpc.Core/Internal/AsyncCall.cs index 414b5c4282..2c3e3d75ea 100644 --- a/src/csharp/Grpc.Core/Internal/AsyncCall.cs +++ b/src/csharp/Grpc.Core/Internal/AsyncCall.cs @@ -50,7 +50,7 @@ namespace Grpc.Core.Internal { static readonly ILogger Logger = GrpcEnvironment.Logger.ForType<AsyncCall<TRequest, TResponse>>(); - readonly CallInvocationDetails<TRequest, TResponse> callDetails; + readonly CallInvocationDetails<TRequest, TResponse> details; // Completion of a pending unary response if not null. TaskCompletionSource<TResponse> unaryResponseTcs; @@ -63,7 +63,8 @@ namespace Grpc.Core.Internal public AsyncCall(CallInvocationDetails<TRequest, TResponse> callDetails) : base(callDetails.RequestMarshaller.Serializer, callDetails.ResponseMarshaller.Deserializer) { - this.callDetails = callDetails; + this.details = callDetails.WithOptions(callDetails.Options.Normalize()); + this.initialMetadataSent = true; // we always send metadata at the very beginning of the call. } // TODO: this method is not Async, so it shouldn't be in AsyncCall class, but @@ -89,11 +90,11 @@ namespace Grpc.Core.Internal readingDone = true; } - using (var metadataArray = MetadataArraySafeHandle.Create(callDetails.Options.Headers)) + using (var metadataArray = MetadataArraySafeHandle.Create(details.Options.Headers)) { using (var ctx = BatchContextSafeHandle.Create()) { - call.StartUnary(payload, ctx, metadataArray); + call.StartUnary(ctx, payload, metadataArray, GetWriteFlagsForCall()); var ev = cq.Pluck(ctx.Handle); bool success = (ev.success != 0); @@ -108,15 +109,9 @@ namespace Grpc.Core.Internal } } - try - { - // Once the blocking call returns, the result should be available synchronously. - return unaryResponseTcs.Task.Result; - } - catch (AggregateException ae) - { - throw ExceptionHelper.UnwrapRpcException(ae); - } + // Once the blocking call returns, the result should be available synchronously. + // Note that GetAwaiter().GetResult() doesn't wrap exceptions in AggregateException. + return unaryResponseTcs.Task.GetAwaiter().GetResult(); } } @@ -130,7 +125,7 @@ namespace Grpc.Core.Internal Preconditions.CheckState(!started); started = true; - Initialize(callDetails.Channel.Environment.CompletionQueue); + Initialize(details.Channel.Environment.CompletionQueue); halfcloseRequested = true; readingDone = true; @@ -138,9 +133,9 @@ namespace Grpc.Core.Internal byte[] payload = UnsafeSerialize(msg); unaryResponseTcs = new TaskCompletionSource<TResponse>(); - using (var metadataArray = MetadataArraySafeHandle.Create(callDetails.Options.Headers)) + using (var metadataArray = MetadataArraySafeHandle.Create(details.Options.Headers)) { - call.StartUnary(payload, HandleUnaryResponse, metadataArray); + call.StartUnary(HandleUnaryResponse, payload, metadataArray, GetWriteFlagsForCall()); } return unaryResponseTcs.Task; } @@ -157,12 +152,12 @@ namespace Grpc.Core.Internal Preconditions.CheckState(!started); started = true; - Initialize(callDetails.Channel.Environment.CompletionQueue); + Initialize(details.Channel.Environment.CompletionQueue); readingDone = true; unaryResponseTcs = new TaskCompletionSource<TResponse>(); - using (var metadataArray = MetadataArraySafeHandle.Create(callDetails.Options.Headers)) + using (var metadataArray = MetadataArraySafeHandle.Create(details.Options.Headers)) { call.StartClientStreaming(HandleUnaryResponse, metadataArray); } @@ -181,16 +176,16 @@ namespace Grpc.Core.Internal Preconditions.CheckState(!started); started = true; - Initialize(callDetails.Channel.Environment.CompletionQueue); + Initialize(details.Channel.Environment.CompletionQueue); halfcloseRequested = true; halfclosed = true; // halfclose not confirmed yet, but it will be once finishedHandler is called. byte[] payload = UnsafeSerialize(msg); - using (var metadataArray = MetadataArraySafeHandle.Create(callDetails.Options.Headers)) + using (var metadataArray = MetadataArraySafeHandle.Create(details.Options.Headers)) { - call.StartServerStreaming(payload, HandleFinished, metadataArray); + call.StartServerStreaming(HandleFinished, payload, metadataArray, GetWriteFlagsForCall()); } } } @@ -206,9 +201,9 @@ namespace Grpc.Core.Internal Preconditions.CheckState(!started); started = true; - Initialize(callDetails.Channel.Environment.CompletionQueue); + Initialize(details.Channel.Environment.CompletionQueue); - using (var metadataArray = MetadataArraySafeHandle.Create(callDetails.Options.Headers)) + using (var metadataArray = MetadataArraySafeHandle.Create(details.Options.Headers)) { call.StartDuplexStreaming(HandleFinished, metadataArray); } @@ -219,9 +214,9 @@ namespace Grpc.Core.Internal /// Sends a streaming request. Only one pending send action is allowed at any given time. /// completionDelegate is called when the operation finishes. /// </summary> - public void StartSendMessage(TRequest msg, AsyncCompletionDelegate<object> completionDelegate) + public void StartSendMessage(TRequest msg, WriteFlags writeFlags, AsyncCompletionDelegate<object> completionDelegate) { - StartSendMessageInternal(msg, completionDelegate); + StartSendMessageInternal(msg, writeFlags, completionDelegate); } /// <summary> @@ -278,6 +273,14 @@ namespace Grpc.Core.Internal } } + public CallInvocationDetails<TRequest, TResponse> Details + { + get + { + return this.details; + } + } + /// <summary> /// On client-side, we only fire readCompletionDelegate once all messages have been read /// and status has been received. @@ -310,14 +313,17 @@ namespace Grpc.Core.Internal protected override void OnReleaseResources() { - callDetails.Channel.Environment.DebugStats.ActiveClientCalls.Decrement(); + details.Channel.Environment.DebugStats.ActiveClientCalls.Decrement(); } private void Initialize(CompletionQueueSafeHandle cq) { - var call = callDetails.Channel.Handle.CreateCall(callDetails.Channel.Environment.CompletionRegistry, cq, - callDetails.Method, callDetails.Host, Timespec.FromDateTime(callDetails.Options.Deadline)); - callDetails.Channel.Environment.DebugStats.ActiveClientCalls.Increment(); + var parentCall = details.Options.PropagationToken != null ? details.Options.PropagationToken.ParentCall : CallSafeHandle.NullInstance; + + var call = details.Channel.Handle.CreateCall(details.Channel.Environment.CompletionRegistry, + parentCall, ContextPropagationToken.DefaultMask, cq, + details.Method, details.Host, Timespec.FromDateTime(details.Options.Deadline.Value)); + details.Channel.Environment.DebugStats.ActiveClientCalls.Increment(); InitializeInternal(call); RegisterCancellationCallback(); } @@ -325,7 +331,7 @@ namespace Grpc.Core.Internal // Make sure that once cancellationToken for this call is cancelled, Cancel() will be called. private void RegisterCancellationCallback() { - var token = callDetails.Options.CancellationToken; + var token = details.Options.CancellationToken; if (token.CanBeCanceled) { token.Register(() => this.Cancel()); @@ -333,6 +339,15 @@ namespace Grpc.Core.Internal } /// <summary> + /// Gets WriteFlags set in callDetails.Options.WriteOptions + /// </summary> + private WriteFlags GetWriteFlagsForCall() + { + var writeOptions = details.Options.WriteOptions; + return writeOptions != null ? writeOptions.Flags : default(WriteFlags); + } + + /// <summary> /// Handler for unary response completion. /// </summary> private void HandleUnaryResponse(bool success, BatchContextSafeHandle ctx) diff --git a/src/csharp/Grpc.Core/Internal/AsyncCallBase.cs b/src/csharp/Grpc.Core/Internal/AsyncCallBase.cs index 38f2a5baeb..6ca4bbdafc 100644 --- a/src/csharp/Grpc.Core/Internal/AsyncCallBase.cs +++ b/src/csharp/Grpc.Core/Internal/AsyncCallBase.cs @@ -71,6 +71,9 @@ namespace Grpc.Core.Internal protected bool halfclosed; protected bool finished; // True if close has been received from the peer. + protected bool initialMetadataSent; + protected long streamingWritesCounter; + public AsyncCallBase(Func<TWrite, byte[]> serializer, Func<byte[], TRead> deserializer) { this.serializer = Preconditions.CheckNotNull(serializer); @@ -123,7 +126,7 @@ namespace Grpc.Core.Internal /// Initiates sending a message. Only one send operation can be active at a time. /// completionDelegate is invoked upon completion. /// </summary> - protected void StartSendMessageInternal(TWrite msg, AsyncCompletionDelegate<object> completionDelegate) + protected void StartSendMessageInternal(TWrite msg, WriteFlags writeFlags, AsyncCompletionDelegate<object> completionDelegate) { byte[] payload = UnsafeSerialize(msg); @@ -132,8 +135,11 @@ namespace Grpc.Core.Internal Preconditions.CheckNotNull(completionDelegate, "Completion delegate cannot be null"); CheckSendingAllowed(); - call.StartSendMessage(payload, HandleSendFinished); + call.StartSendMessage(HandleSendFinished, payload, writeFlags, !initialMetadataSent); + sendCompletionDelegate = completionDelegate; + initialMetadataSent = true; + streamingWritesCounter++; } } @@ -287,7 +293,7 @@ namespace Grpc.Core.Internal if (!success) { - FireCompletion(origCompletionDelegate, null, new OperationFailedException("Send failed")); + FireCompletion(origCompletionDelegate, null, new InvalidOperationException("Send failed")); } else { @@ -312,7 +318,7 @@ namespace Grpc.Core.Internal if (!success) { - FireCompletion(origCompletionDelegate, null, new OperationFailedException("Halfclose failed")); + FireCompletion(origCompletionDelegate, null, new InvalidOperationException("Halfclose failed")); } else { diff --git a/src/csharp/Grpc.Core/Internal/AsyncCallServer.cs b/src/csharp/Grpc.Core/Internal/AsyncCallServer.cs index 513902ee36..3710a65d6b 100644 --- a/src/csharp/Grpc.Core/Internal/AsyncCallServer.cs +++ b/src/csharp/Grpc.Core/Internal/AsyncCallServer.cs @@ -83,9 +83,9 @@ namespace Grpc.Core.Internal /// Sends a streaming response. Only one pending send action is allowed at any given time. /// completionDelegate is called when the operation finishes. /// </summary> - public void StartSendMessage(TResponse msg, AsyncCompletionDelegate<object> completionDelegate) + public void StartSendMessage(TResponse msg, WriteFlags writeFlags, AsyncCompletionDelegate<object> completionDelegate) { - StartSendMessageInternal(msg, completionDelegate); + StartSendMessageInternal(msg, writeFlags, completionDelegate); } /// <summary> @@ -98,6 +98,35 @@ namespace Grpc.Core.Internal } /// <summary> + /// Initiates sending a initial metadata. + /// Even though C-core allows sending metadata in parallel to sending messages, we will treat sending metadata as a send message operation + /// to make things simpler. + /// completionDelegate is invoked upon completion. + /// </summary> + public void StartSendInitialMetadata(Metadata headers, AsyncCompletionDelegate<object> completionDelegate) + { + lock (myLock) + { + Preconditions.CheckNotNull(headers, "metadata"); + Preconditions.CheckNotNull(completionDelegate, "Completion delegate cannot be null"); + + Preconditions.CheckState(!initialMetadataSent, "Response headers can only be sent once per call."); + Preconditions.CheckState(streamingWritesCounter == 0, "Response headers can only be sent before the first write starts."); + CheckSendingAllowed(); + + Preconditions.CheckNotNull(completionDelegate, "Completion delegate cannot be null"); + + using (var metadataArray = MetadataArraySafeHandle.Create(headers)) + { + call.StartSendInitialMetadata(HandleSendFinished, metadataArray); + } + + this.initialMetadataSent = true; + sendCompletionDelegate = completionDelegate; + } + } + + /// <summary> /// Sends call result status, also indicating server is done with streaming responses. /// Only one pending send action is allowed at any given time. /// completionDelegate is called when the operation finishes. @@ -111,7 +140,7 @@ namespace Grpc.Core.Internal using (var metadataArray = MetadataArraySafeHandle.Create(trailers)) { - call.StartSendStatusFromServer(status, HandleHalfclosed, metadataArray); + call.StartSendStatusFromServer(HandleHalfclosed, status, metadataArray, !initialMetadataSent); } halfcloseRequested = true; readingDone = true; diff --git a/src/csharp/Grpc.Core/Internal/CallSafeHandle.cs b/src/csharp/Grpc.Core/Internal/CallSafeHandle.cs index 714749b171..3cb01e29bd 100644 --- a/src/csharp/Grpc.Core/Internal/CallSafeHandle.cs +++ b/src/csharp/Grpc.Core/Internal/CallSafeHandle.cs @@ -42,6 +42,8 @@ namespace Grpc.Core.Internal /// </summary> internal class CallSafeHandle : SafeHandleZeroIsInvalid { + public static readonly CallSafeHandle NullInstance = new CallSafeHandle(); + const uint GRPC_WRITE_BUFFER_HINT = 1; CompletionRegistry completionRegistry; @@ -53,7 +55,7 @@ namespace Grpc.Core.Internal [DllImport("grpc_csharp_ext.dll")] static extern GRPCCallError grpcsharp_call_start_unary(CallSafeHandle call, - BatchContextSafeHandle ctx, byte[] send_buffer, UIntPtr send_buffer_len, MetadataArraySafeHandle metadataArray); + BatchContextSafeHandle ctx, byte[] send_buffer, UIntPtr send_buffer_len, MetadataArraySafeHandle metadataArray, WriteFlags writeFlags); [DllImport("grpc_csharp_ext.dll")] static extern GRPCCallError grpcsharp_call_start_client_streaming(CallSafeHandle call, @@ -62,7 +64,7 @@ namespace Grpc.Core.Internal [DllImport("grpc_csharp_ext.dll")] static extern GRPCCallError grpcsharp_call_start_server_streaming(CallSafeHandle call, BatchContextSafeHandle ctx, byte[] send_buffer, UIntPtr send_buffer_len, - MetadataArraySafeHandle metadataArray); + MetadataArraySafeHandle metadataArray, WriteFlags writeFlags); [DllImport("grpc_csharp_ext.dll")] static extern GRPCCallError grpcsharp_call_start_duplex_streaming(CallSafeHandle call, @@ -70,7 +72,7 @@ namespace Grpc.Core.Internal [DllImport("grpc_csharp_ext.dll")] static extern GRPCCallError grpcsharp_call_send_message(CallSafeHandle call, - BatchContextSafeHandle ctx, byte[] send_buffer, UIntPtr send_buffer_len); + BatchContextSafeHandle ctx, byte[] send_buffer, UIntPtr send_buffer_len, WriteFlags writeFlags, bool sendEmptyInitialMetadata); [DllImport("grpc_csharp_ext.dll")] static extern GRPCCallError grpcsharp_call_send_close_from_client(CallSafeHandle call, @@ -78,7 +80,7 @@ namespace Grpc.Core.Internal [DllImport("grpc_csharp_ext.dll")] static extern GRPCCallError grpcsharp_call_send_status_from_server(CallSafeHandle call, - BatchContextSafeHandle ctx, StatusCode statusCode, string statusMessage, MetadataArraySafeHandle metadataArray); + BatchContextSafeHandle ctx, StatusCode statusCode, string statusMessage, MetadataArraySafeHandle metadataArray, bool sendEmptyInitialMetadata); [DllImport("grpc_csharp_ext.dll")] static extern GRPCCallError grpcsharp_call_recv_message(CallSafeHandle call, @@ -89,6 +91,10 @@ namespace Grpc.Core.Internal BatchContextSafeHandle ctx); [DllImport("grpc_csharp_ext.dll")] + static extern GRPCCallError grpcsharp_call_send_initial_metadata(CallSafeHandle call, + BatchContextSafeHandle ctx, MetadataArraySafeHandle metadataArray); + + [DllImport("grpc_csharp_ext.dll")] static extern CStringSafeHandle grpcsharp_call_get_peer(CallSafeHandle call); [DllImport("grpc_csharp_ext.dll")] @@ -103,17 +109,17 @@ namespace Grpc.Core.Internal this.completionRegistry = completionRegistry; } - public void StartUnary(byte[] payload, BatchCompletionDelegate callback, MetadataArraySafeHandle metadataArray) + public void StartUnary(BatchCompletionDelegate callback, byte[] payload, MetadataArraySafeHandle metadataArray, WriteFlags writeFlags) { var ctx = BatchContextSafeHandle.Create(); completionRegistry.RegisterBatchCompletion(ctx, callback); - grpcsharp_call_start_unary(this, ctx, payload, new UIntPtr((ulong)payload.Length), metadataArray) + grpcsharp_call_start_unary(this, ctx, payload, new UIntPtr((ulong)payload.Length), metadataArray, writeFlags) .CheckOk(); } - public void StartUnary(byte[] payload, BatchContextSafeHandle ctx, MetadataArraySafeHandle metadataArray) + public void StartUnary(BatchContextSafeHandle ctx, byte[] payload, MetadataArraySafeHandle metadataArray, WriteFlags writeFlags) { - grpcsharp_call_start_unary(this, ctx, payload, new UIntPtr((ulong)payload.Length), metadataArray) + grpcsharp_call_start_unary(this, ctx, payload, new UIntPtr((ulong)payload.Length), metadataArray, writeFlags) .CheckOk(); } @@ -124,11 +130,11 @@ namespace Grpc.Core.Internal grpcsharp_call_start_client_streaming(this, ctx, metadataArray).CheckOk(); } - public void StartServerStreaming(byte[] payload, BatchCompletionDelegate callback, MetadataArraySafeHandle metadataArray) + public void StartServerStreaming(BatchCompletionDelegate callback, byte[] payload, MetadataArraySafeHandle metadataArray, WriteFlags writeFlags) { var ctx = BatchContextSafeHandle.Create(); completionRegistry.RegisterBatchCompletion(ctx, callback); - grpcsharp_call_start_server_streaming(this, ctx, payload, new UIntPtr((ulong)payload.Length), metadataArray).CheckOk(); + grpcsharp_call_start_server_streaming(this, ctx, payload, new UIntPtr((ulong)payload.Length), metadataArray, writeFlags).CheckOk(); } public void StartDuplexStreaming(BatchCompletionDelegate callback, MetadataArraySafeHandle metadataArray) @@ -138,11 +144,11 @@ namespace Grpc.Core.Internal grpcsharp_call_start_duplex_streaming(this, ctx, metadataArray).CheckOk(); } - public void StartSendMessage(byte[] payload, BatchCompletionDelegate callback) + public void StartSendMessage(BatchCompletionDelegate callback, byte[] payload, WriteFlags writeFlags, bool sendEmptyInitialMetadata) { var ctx = BatchContextSafeHandle.Create(); completionRegistry.RegisterBatchCompletion(ctx, callback); - grpcsharp_call_send_message(this, ctx, payload, new UIntPtr((ulong)payload.Length)).CheckOk(); + grpcsharp_call_send_message(this, ctx, payload, new UIntPtr((ulong)payload.Length), writeFlags, sendEmptyInitialMetadata).CheckOk(); } public void StartSendCloseFromClient(BatchCompletionDelegate callback) @@ -152,11 +158,11 @@ namespace Grpc.Core.Internal grpcsharp_call_send_close_from_client(this, ctx).CheckOk(); } - public void StartSendStatusFromServer(Status status, BatchCompletionDelegate callback, MetadataArraySafeHandle metadataArray) + public void StartSendStatusFromServer(BatchCompletionDelegate callback, Status status, MetadataArraySafeHandle metadataArray, bool sendEmptyInitialMetadata) { var ctx = BatchContextSafeHandle.Create(); completionRegistry.RegisterBatchCompletion(ctx, callback); - grpcsharp_call_send_status_from_server(this, ctx, status.StatusCode, status.Detail, metadataArray).CheckOk(); + grpcsharp_call_send_status_from_server(this, ctx, status.StatusCode, status.Detail, metadataArray, sendEmptyInitialMetadata).CheckOk(); } public void StartReceiveMessage(BatchCompletionDelegate callback) @@ -173,6 +179,13 @@ namespace Grpc.Core.Internal grpcsharp_call_start_serverside(this, ctx).CheckOk(); } + public void StartSendInitialMetadata(BatchCompletionDelegate callback, MetadataArraySafeHandle metadataArray) + { + var ctx = BatchContextSafeHandle.Create(); + completionRegistry.RegisterBatchCompletion(ctx, callback); + grpcsharp_call_send_initial_metadata(this, ctx, metadataArray).CheckOk(); + } + public void Cancel() { grpcsharp_call_cancel(this).CheckOk(); diff --git a/src/csharp/Grpc.Core/Internal/ChannelSafeHandle.cs b/src/csharp/Grpc.Core/Internal/ChannelSafeHandle.cs index 7324ebdf57..7f03bf4ea5 100644 --- a/src/csharp/Grpc.Core/Internal/ChannelSafeHandle.cs +++ b/src/csharp/Grpc.Core/Internal/ChannelSafeHandle.cs @@ -47,7 +47,7 @@ namespace Grpc.Core.Internal static extern ChannelSafeHandle grpcsharp_secure_channel_create(CredentialsSafeHandle credentials, string target, ChannelArgsSafeHandle channelArgs); [DllImport("grpc_csharp_ext.dll")] - static extern CallSafeHandle grpcsharp_channel_create_call(ChannelSafeHandle channel, CompletionQueueSafeHandle cq, string method, string host, Timespec deadline); + static extern CallSafeHandle grpcsharp_channel_create_call(ChannelSafeHandle channel, CallSafeHandle parentCall, ContextPropagationFlags propagationMask, CompletionQueueSafeHandle cq, string method, string host, Timespec deadline); [DllImport("grpc_csharp_ext.dll")] static extern ChannelState grpcsharp_channel_check_connectivity_state(ChannelSafeHandle channel, int tryToConnect); @@ -76,9 +76,9 @@ namespace Grpc.Core.Internal return grpcsharp_secure_channel_create(credentials, target, channelArgs); } - public CallSafeHandle CreateCall(CompletionRegistry registry, CompletionQueueSafeHandle cq, string method, string host, Timespec deadline) + public CallSafeHandle CreateCall(CompletionRegistry registry, CallSafeHandle parentCall, ContextPropagationFlags propagationMask, CompletionQueueSafeHandle cq, string method, string host, Timespec deadline) { - var result = grpcsharp_channel_create_call(this, cq, method, host, deadline); + var result = grpcsharp_channel_create_call(this, parentCall, propagationMask, cq, method, host, deadline); result.SetCompletionRegistry(registry); return result; } diff --git a/src/csharp/Grpc.Core/Internal/ClientRequestStream.cs b/src/csharp/Grpc.Core/Internal/ClientRequestStream.cs index 58f493463b..013f00ff6f 100644 --- a/src/csharp/Grpc.Core/Internal/ClientRequestStream.cs +++ b/src/csharp/Grpc.Core/Internal/ClientRequestStream.cs @@ -40,16 +40,18 @@ namespace Grpc.Core.Internal internal class ClientRequestStream<TRequest, TResponse> : IClientStreamWriter<TRequest> { readonly AsyncCall<TRequest, TResponse> call; + WriteOptions writeOptions; public ClientRequestStream(AsyncCall<TRequest, TResponse> call) { this.call = call; + this.writeOptions = call.Details.Options.WriteOptions; } public Task WriteAsync(TRequest message) { var taskSource = new AsyncCompletionTaskSource<object>(); - call.StartSendMessage(message, taskSource.CompletionDelegate); + call.StartSendMessage(message, GetWriteFlags(), taskSource.CompletionDelegate); return taskSource.Task; } @@ -59,5 +61,24 @@ namespace Grpc.Core.Internal call.StartSendCloseFromClient(taskSource.CompletionDelegate); return taskSource.Task; } + + public WriteOptions WriteOptions + { + get + { + return this.writeOptions; + } + + set + { + writeOptions = value; + } + } + + private WriteFlags GetWriteFlags() + { + var options = writeOptions; + return options != null ? options.Flags : default(WriteFlags); + } } } diff --git a/src/csharp/Grpc.Core/Internal/ServerCallHandler.cs b/src/csharp/Grpc.Core/Internal/ServerCallHandler.cs index 19f0e3c57f..688f9f6fec 100644 --- a/src/csharp/Grpc.Core/Internal/ServerCallHandler.cs +++ b/src/csharp/Grpc.Core/Internal/ServerCallHandler.cs @@ -75,7 +75,7 @@ namespace Grpc.Core.Internal var responseStream = new ServerResponseStream<TRequest, TResponse>(asyncCall); Status status; - var context = HandlerUtils.NewContext(newRpc, asyncCall.Peer, asyncCall.CancellationToken); + var context = HandlerUtils.NewContext(newRpc, asyncCall.Peer, responseStream, asyncCall.CancellationToken); try { Preconditions.CheckArgument(await requestStream.MoveNext()); @@ -131,7 +131,7 @@ namespace Grpc.Core.Internal var responseStream = new ServerResponseStream<TRequest, TResponse>(asyncCall); Status status; - var context = HandlerUtils.NewContext(newRpc, asyncCall.Peer, asyncCall.CancellationToken); + var context = HandlerUtils.NewContext(newRpc, asyncCall.Peer, responseStream, asyncCall.CancellationToken); try { Preconditions.CheckArgument(await requestStream.MoveNext()); @@ -187,7 +187,7 @@ namespace Grpc.Core.Internal var responseStream = new ServerResponseStream<TRequest, TResponse>(asyncCall); Status status; - var context = HandlerUtils.NewContext(newRpc, asyncCall.Peer, asyncCall.CancellationToken); + var context = HandlerUtils.NewContext(newRpc, asyncCall.Peer, responseStream, asyncCall.CancellationToken); try { var result = await handler(requestStream, context); @@ -247,7 +247,7 @@ namespace Grpc.Core.Internal var responseStream = new ServerResponseStream<TRequest, TResponse>(asyncCall); Status status; - var context = HandlerUtils.NewContext(newRpc, asyncCall.Peer, asyncCall.CancellationToken); + var context = HandlerUtils.NewContext(newRpc, asyncCall.Peer, responseStream, asyncCall.CancellationToken); try { await handler(requestStream, responseStream, context); @@ -304,13 +304,14 @@ namespace Grpc.Core.Internal return new Status(StatusCode.Unknown, "Exception was thrown by handler."); } - public static ServerCallContext NewContext(ServerRpcNew newRpc, string peer, CancellationToken cancellationToken) + public static ServerCallContext NewContext<TRequest, TResponse>(ServerRpcNew newRpc, string peer, ServerResponseStream<TRequest, TResponse> serverResponseStream, CancellationToken cancellationToken) + where TRequest : class + where TResponse : class { DateTime realtimeDeadline = newRpc.Deadline.ToClockType(GPRClockType.Realtime).ToDateTime(); - return new ServerCallContext( - newRpc.Method, newRpc.Host, peer, realtimeDeadline, - newRpc.RequestMetadata, cancellationToken); + return new ServerCallContext(newRpc.Call, newRpc.Method, newRpc.Host, peer, realtimeDeadline, + newRpc.RequestMetadata, cancellationToken, serverResponseStream.WriteResponseHeadersAsync, serverResponseStream); } } } diff --git a/src/csharp/Grpc.Core/Internal/ServerResponseStream.cs b/src/csharp/Grpc.Core/Internal/ServerResponseStream.cs index 756dcee87f..03e39efc02 100644 --- a/src/csharp/Grpc.Core/Internal/ServerResponseStream.cs +++ b/src/csharp/Grpc.Core/Internal/ServerResponseStream.cs @@ -38,11 +38,12 @@ namespace Grpc.Core.Internal /// <summary> /// Writes responses asynchronously to an underlying AsyncCallServer object. /// </summary> - internal class ServerResponseStream<TRequest, TResponse> : IServerStreamWriter<TResponse> + internal class ServerResponseStream<TRequest, TResponse> : IServerStreamWriter<TResponse>, IHasWriteOptions where TRequest : class where TResponse : class { readonly AsyncCallServer<TRequest, TResponse> call; + WriteOptions writeOptions; public ServerResponseStream(AsyncCallServer<TRequest, TResponse> call) { @@ -52,7 +53,7 @@ namespace Grpc.Core.Internal public Task WriteAsync(TResponse message) { var taskSource = new AsyncCompletionTaskSource<object>(); - call.StartSendMessage(message, taskSource.CompletionDelegate); + call.StartSendMessage(message, GetWriteFlags(), taskSource.CompletionDelegate); return taskSource.Task; } @@ -62,5 +63,31 @@ namespace Grpc.Core.Internal call.StartSendStatusFromServer(status, trailers, taskSource.CompletionDelegate); return taskSource.Task; } + + public Task WriteResponseHeadersAsync(Metadata responseHeaders) + { + var taskSource = new AsyncCompletionTaskSource<object>(); + call.StartSendInitialMetadata(responseHeaders, taskSource.CompletionDelegate); + return taskSource.Task; + } + + public WriteOptions WriteOptions + { + get + { + return writeOptions; + } + + set + { + writeOptions = value; + } + } + + private WriteFlags GetWriteFlags() + { + var options = writeOptions; + return options != null ? options.Flags : default(WriteFlags); + } } } diff --git a/src/csharp/Grpc.Core/Internal/Timespec.cs b/src/csharp/Grpc.Core/Internal/Timespec.cs index e83d21f4a4..daf85d5f61 100644 --- a/src/csharp/Grpc.Core/Internal/Timespec.cs +++ b/src/csharp/Grpc.Core/Internal/Timespec.cs @@ -211,7 +211,7 @@ namespace Grpc.Core.Internal return Timespec.InfPast; } - Preconditions.CheckArgument(dateTime.Kind == DateTimeKind.Utc, "dateTime"); + Preconditions.CheckArgument(dateTime.Kind == DateTimeKind.Utc, "dateTime needs of kind DateTimeKind.Utc or be equal to DateTime.MaxValue or DateTime.MinValue."); try { diff --git a/src/csharp/Grpc.Core/KeyCertificatePair.cs b/src/csharp/Grpc.Core/KeyCertificatePair.cs index 5def15a656..6f691975e9 100644 --- a/src/csharp/Grpc.Core/KeyCertificatePair.cs +++ b/src/csharp/Grpc.Core/KeyCertificatePair.cs @@ -54,8 +54,8 @@ namespace Grpc.Core /// <param name="privateKey">PEM encoded private key.</param> public KeyCertificatePair(string certificateChain, string privateKey) { - this.certificateChain = Preconditions.CheckNotNull(certificateChain); - this.privateKey = Preconditions.CheckNotNull(privateKey); + this.certificateChain = Preconditions.CheckNotNull(certificateChain, "certificateChain"); + this.privateKey = Preconditions.CheckNotNull(privateKey, "privateKey"); } /// <summary> diff --git a/src/csharp/Grpc.Core/Logging/ConsoleLogger.cs b/src/csharp/Grpc.Core/Logging/ConsoleLogger.cs index c67765c78d..382481d871 100644 --- a/src/csharp/Grpc.Core/Logging/ConsoleLogger.cs +++ b/src/csharp/Grpc.Core/Logging/ConsoleLogger.cs @@ -42,16 +42,21 @@ namespace Grpc.Core.Logging readonly Type forType; readonly string forTypeString; + /// <summary>Creates a console logger not associated to any specific type.</summary> public ConsoleLogger() : this(null) { } + /// <summary>Creates a console logger that logs messsage specific for given type.</summary> private ConsoleLogger(Type forType) { this.forType = forType; this.forTypeString = forType != null ? forType.FullName + " " : ""; } - + + /// <summary> + /// Returns a logger associated with the specified type. + /// </summary> public ILogger ForType<T>() { if (typeof(T) == forType) @@ -61,31 +66,37 @@ namespace Grpc.Core.Logging return new ConsoleLogger(typeof(T)); } + /// <summary>Logs a message with severity Debug.</summary> public void Debug(string message, params object[] formatArgs) { Log("D", message, formatArgs); } + /// <summary>Logs a message with severity Info.</summary> public void Info(string message, params object[] formatArgs) { Log("I", message, formatArgs); } + /// <summary>Logs a message with severity Warning.</summary> public void Warning(string message, params object[] formatArgs) { Log("W", message, formatArgs); } + /// <summary>Logs a message and an associated exception with severity Warning.</summary> public void Warning(Exception exception, string message, params object[] formatArgs) { Log("W", message + " " + exception, formatArgs); } + /// <summary>Logs a message with severity Error.</summary> public void Error(string message, params object[] formatArgs) { Log("E", message, formatArgs); } + /// <summary>Logs a message and an associated exception with severity Error.</summary> public void Error(Exception exception, string message, params object[] formatArgs) { Log("E", message + " " + exception, formatArgs); diff --git a/src/csharp/Grpc.Core/Logging/ILogger.cs b/src/csharp/Grpc.Core/Logging/ILogger.cs index 0d58f133e3..61e0c91388 100644 --- a/src/csharp/Grpc.Core/Logging/ILogger.cs +++ b/src/csharp/Grpc.Core/Logging/ILogger.cs @@ -42,16 +42,22 @@ namespace Grpc.Core.Logging /// <summary>Returns a logger associated with the specified type.</summary> ILogger ForType<T>(); + /// <summary>Logs a message with severity Debug.</summary> void Debug(string message, params object[] formatArgs); + /// <summary>Logs a message with severity Info.</summary> void Info(string message, params object[] formatArgs); + /// <summary>Logs a message with severity Warning.</summary> void Warning(string message, params object[] formatArgs); + /// <summary>Logs a message and an associated exception with severity Warning.</summary> void Warning(Exception exception, string message, params object[] formatArgs); + /// <summary>Logs a message with severity Error.</summary> void Error(string message, params object[] formatArgs); + /// <summary>Logs a message and an associated exception with severity Error.</summary> void Error(Exception exception, string message, params object[] formatArgs); } } diff --git a/src/csharp/Grpc.Core/Marshaller.cs b/src/csharp/Grpc.Core/Marshaller.cs index 8b1a929074..f38cb0863f 100644 --- a/src/csharp/Grpc.Core/Marshaller.cs +++ b/src/csharp/Grpc.Core/Marshaller.cs @@ -37,19 +37,27 @@ using Grpc.Core.Utils; namespace Grpc.Core { /// <summary> - /// For serializing and deserializing messages. + /// Encapsulates the logic for serializing and deserializing messages. /// </summary> public struct Marshaller<T> { readonly Func<T, byte[]> serializer; readonly Func<byte[], T> deserializer; + /// <summary> + /// Initializes a new marshaller. + /// </summary> + /// <param name="serializer">Function that will be used to serialize messages.</param> + /// <param name="deserializer">Function that will be used to deserialize messages.</param> public Marshaller(Func<T, byte[]> serializer, Func<byte[], T> deserializer) { - this.serializer = Preconditions.CheckNotNull(serializer); - this.deserializer = Preconditions.CheckNotNull(deserializer); + this.serializer = Preconditions.CheckNotNull(serializer, "serializer"); + this.deserializer = Preconditions.CheckNotNull(deserializer, "deserializer"); } + /// <summary> + /// Gets the serializer function. + /// </summary> public Func<T, byte[]> Serializer { get @@ -58,6 +66,9 @@ namespace Grpc.Core } } + /// <summary> + /// Gets the deserializer function. + /// </summary> public Func<byte[], T> Deserializer { get @@ -72,11 +83,17 @@ namespace Grpc.Core /// </summary> public static class Marshallers { + /// <summary> + /// Creates a marshaller from specified serializer and deserializer. + /// </summary> public static Marshaller<T> Create<T>(Func<T, byte[]> serializer, Func<byte[], T> deserializer) { return new Marshaller<T>(serializer, deserializer); } + /// <summary> + /// Returns a marshaller for <c>string</c> type. This is useful for testing. + /// </summary> public static Marshaller<string> StringMarshaller { get diff --git a/src/csharp/Grpc.Core/Metadata.cs b/src/csharp/Grpc.Core/Metadata.cs index 6fd0a7109d..9db2abf46e 100644 --- a/src/csharp/Grpc.Core/Metadata.cs +++ b/src/csharp/Grpc.Core/Metadata.cs @@ -114,6 +114,16 @@ namespace Grpc.Core entries.Add(item); } + public void Add(string key, string value) + { + Add(new Entry(key, value)); + } + + public void Add(string key, byte[] valueBytes) + { + Add(new Entry(key, valueBytes)); + } + public void Clear() { CheckWriteable(); @@ -176,15 +186,15 @@ namespace Grpc.Core public Entry(string key, byte[] valueBytes) { - this.key = Preconditions.CheckNotNull(key); + this.key = Preconditions.CheckNotNull(key, "key"); this.value = null; - this.valueBytes = Preconditions.CheckNotNull(valueBytes); + this.valueBytes = Preconditions.CheckNotNull(valueBytes, "valueBytes"); } public Entry(string key, string value) { - this.key = Preconditions.CheckNotNull(key); - this.value = Preconditions.CheckNotNull(value); + this.key = Preconditions.CheckNotNull(key, "key"); + this.value = Preconditions.CheckNotNull(value, "value"); this.valueBytes = null; } diff --git a/src/csharp/Grpc.Core/Method.cs b/src/csharp/Grpc.Core/Method.cs index cc047ac9f8..4c208b4a26 100644 --- a/src/csharp/Grpc.Core/Method.cs +++ b/src/csharp/Grpc.Core/Method.cs @@ -41,14 +41,21 @@ namespace Grpc.Core /// </summary> public enum MethodType { - Unary, // Unary request, unary response. - ClientStreaming, // Streaming request, unary response. - ServerStreaming, // Unary request, streaming response. - DuplexStreaming // Streaming request, streaming response. + /// <summary>Single request sent from client, single response received from server.</summary> + Unary, + + /// <summary>Stream of request sent from client, single response received from server.</summary> + ClientStreaming, + + /// <summary>Single request sent from client, stream of responses received from server.</summary> + ServerStreaming, + + /// <summary>Both server and client can stream arbitrary number of requests and responses simultaneously.</summary> + DuplexStreaming } /// <summary> - /// A description of a service method. + /// A description of a remote method. /// </summary> public class Method<TRequest, TResponse> { @@ -59,16 +66,27 @@ namespace Grpc.Core readonly Marshaller<TResponse> responseMarshaller; readonly string fullName; + /// <summary> + /// Initializes a new instance of the <c>Method</c> class. + /// </summary> + /// <param name="type">Type of method.</param> + /// <param name="serviceName">Name of service this method belongs to.</param> + /// <param name="name">Unqualified name of the method.</param> + /// <param name="requestMarshaller">Marshaller used for request messages.</param> + /// <param name="responseMarshaller">Marshaller used for response messages.</param> public Method(MethodType type, string serviceName, string name, Marshaller<TRequest> requestMarshaller, Marshaller<TResponse> responseMarshaller) { this.type = type; - this.serviceName = Preconditions.CheckNotNull(serviceName); - this.name = Preconditions.CheckNotNull(name); - this.requestMarshaller = Preconditions.CheckNotNull(requestMarshaller); - this.responseMarshaller = Preconditions.CheckNotNull(responseMarshaller); - this.fullName = GetFullName(serviceName); + this.serviceName = Preconditions.CheckNotNull(serviceName, "serviceName"); + this.name = Preconditions.CheckNotNull(name, "name"); + this.requestMarshaller = Preconditions.CheckNotNull(requestMarshaller, "requestMarshaller"); + this.responseMarshaller = Preconditions.CheckNotNull(responseMarshaller, "responseMarshaller"); + this.fullName = GetFullName(serviceName, name); } + /// <summary> + /// Gets the type of the method. + /// </summary> public MethodType Type { get @@ -77,6 +95,9 @@ namespace Grpc.Core } } + /// <summary> + /// Gets the name of the service to which this method belongs. + /// </summary> public string ServiceName { get @@ -85,6 +106,9 @@ namespace Grpc.Core } } + /// <summary> + /// Gets the unqualified name of the method. + /// </summary> public string Name { get @@ -93,6 +117,9 @@ namespace Grpc.Core } } + /// <summary> + /// Gets the marshaller used for request messages. + /// </summary> public Marshaller<TRequest> RequestMarshaller { get @@ -101,6 +128,9 @@ namespace Grpc.Core } } + /// <summary> + /// Gets the marshaller used for response messages. + /// </summary> public Marshaller<TResponse> ResponseMarshaller { get @@ -108,7 +138,11 @@ namespace Grpc.Core return this.responseMarshaller; } } - + + /// <summary> + /// Gets the fully qualified name of the method. On the server side, methods are dispatched + /// based on this name. + /// </summary> public string FullName { get @@ -120,9 +154,9 @@ namespace Grpc.Core /// <summary> /// Gets full name of the method including the service name. /// </summary> - internal string GetFullName(string serviceName) + internal static string GetFullName(string serviceName, string methodName) { - return "/" + Preconditions.CheckNotNull(serviceName) + "/" + this.Name; + return "/" + serviceName + "/" + methodName; } } } diff --git a/src/csharp/Grpc.Core/RpcException.cs b/src/csharp/Grpc.Core/RpcException.cs index c58578286b..cac417e626 100644 --- a/src/csharp/Grpc.Core/RpcException.cs +++ b/src/csharp/Grpc.Core/RpcException.cs @@ -36,22 +36,34 @@ using System; namespace Grpc.Core { /// <summary> - /// Thrown when remote procedure call fails. + /// Thrown when remote procedure call fails. Every <c>RpcException</c> is associated with a resulting <see cref="Status"/> of the call. /// </summary> public class RpcException : Exception { private readonly Status status; + /// <summary> + /// Creates a new <c>RpcException</c> associated with given status. + /// </summary> + /// <param name="status">Resulting status of a call.</param> public RpcException(Status status) : base(status.ToString()) { this.status = status; } + /// <summary> + /// Creates a new <c>RpcException</c> associated with given status and message. + /// </summary> + /// <param name="status">Resulting status of a call.</param> + /// <param name="message">The exception message.</param> public RpcException(Status status, string message) : base(message) { this.status = status; } + /// <summary> + /// Resulting status of the call. + /// </summary> public Status Status { get diff --git a/src/csharp/Grpc.Core/Server.cs b/src/csharp/Grpc.Core/Server.cs index eb5b043d1c..c76f126026 100644 --- a/src/csharp/Grpc.Core/Server.cs +++ b/src/csharp/Grpc.Core/Server.cs @@ -192,7 +192,7 @@ namespace Grpc.Core { lock (myLock) { - Preconditions.CheckNotNull(serverPort.Credentials); + Preconditions.CheckNotNull(serverPort.Credentials, "serverPort"); Preconditions.CheckState(!startRequested); var address = string.Format("{0}:{1}", serverPort.Host, serverPort.Port); int boundPort; diff --git a/src/csharp/Grpc.Core/ServerCallContext.cs b/src/csharp/Grpc.Core/ServerCallContext.cs index 032b1390db..75d81c64f3 100644 --- a/src/csharp/Grpc.Core/ServerCallContext.cs +++ b/src/csharp/Grpc.Core/ServerCallContext.cs @@ -36,15 +36,16 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Grpc.Core.Internal; + namespace Grpc.Core { /// <summary> /// Context for a server-side call. /// </summary> - public sealed class ServerCallContext + public class ServerCallContext { - // TODO(jtattermusch): expose method to send initial metadata back to client - + private readonly CallSafeHandle callHandle; private readonly string method; private readonly string host; private readonly string peer; @@ -54,15 +55,34 @@ namespace Grpc.Core private readonly Metadata responseTrailers = new Metadata(); private Status status = Status.DefaultSuccess; + private Func<Metadata, Task> writeHeadersFunc; + private IHasWriteOptions writeOptionsHolder; - public ServerCallContext(string method, string host, string peer, DateTime deadline, Metadata requestHeaders, CancellationToken cancellationToken) + internal ServerCallContext(CallSafeHandle callHandle, string method, string host, string peer, DateTime deadline, Metadata requestHeaders, CancellationToken cancellationToken, + Func<Metadata, Task> writeHeadersFunc, IHasWriteOptions writeOptionsHolder) { + this.callHandle = callHandle; this.method = method; this.host = host; this.peer = peer; this.deadline = deadline; this.requestHeaders = requestHeaders; this.cancellationToken = cancellationToken; + this.writeHeadersFunc = writeHeadersFunc; + this.writeOptionsHolder = writeOptionsHolder; + } + + public Task WriteResponseHeadersAsync(Metadata responseHeaders) + { + return writeHeadersFunc(responseHeaders); + } + + /// <summary> + /// Creates a propagation token to be used to propagate call context to a child call. + /// </summary> + public ContextPropagationToken CreatePropagationToken(ContextPropagationOptions options = null) + { + return new ContextPropagationToken(callHandle, deadline, cancellationToken, options); } /// <summary>Name of method called in this RPC.</summary> @@ -110,7 +130,7 @@ namespace Grpc.Core } } - ///<summary>Cancellation token signals when call is cancelled.</summary> + /// <summary>Cancellation token signals when call is cancelled.</summary> public CancellationToken CancellationToken { get @@ -141,5 +161,31 @@ namespace Grpc.Core status = value; } } + + /// <summary> + /// Allows setting write options for the following write. + /// For streaming response calls, this property is also exposed as on IServerStreamWriter for convenience. + /// Both properties are backed by the same underlying value. + /// </summary> + public WriteOptions WriteOptions + { + get + { + return writeOptionsHolder.WriteOptions; + } + + set + { + writeOptionsHolder.WriteOptions = value; + } + } + } + + /// <summary> + /// Allows sharing write options between ServerCallContext and other objects. + /// </summary> + public interface IHasWriteOptions + { + WriteOptions WriteOptions { get; set; } } } diff --git a/src/csharp/Grpc.Core/ServerCredentials.cs b/src/csharp/Grpc.Core/ServerCredentials.cs index c11a1ede08..3c6703d30e 100644 --- a/src/csharp/Grpc.Core/ServerCredentials.cs +++ b/src/csharp/Grpc.Core/ServerCredentials.cs @@ -91,7 +91,7 @@ namespace Grpc.Core { this.keyCertificatePairs = new List<KeyCertificatePair>(keyCertificatePairs).AsReadOnly(); Preconditions.CheckArgument(this.keyCertificatePairs.Count > 0, - "At least one KeyCertificatePair needs to be provided"); + "At least one KeyCertificatePair needs to be provided."); if (forceClientAuth) { Preconditions.CheckNotNull(rootCertificates, diff --git a/src/csharp/Grpc.Core/ServerMethods.cs b/src/csharp/Grpc.Core/ServerMethods.cs index d457770203..1f119a80ff 100644 --- a/src/csharp/Grpc.Core/ServerMethods.cs +++ b/src/csharp/Grpc.Core/ServerMethods.cs @@ -31,12 +31,8 @@ #endregion -using System; -using System.Threading; using System.Threading.Tasks; -using Grpc.Core.Internal; - namespace Grpc.Core { /// <summary> diff --git a/src/csharp/Grpc.Core/ServerPort.cs b/src/csharp/Grpc.Core/ServerPort.cs index 55e4bd0062..598404d045 100644 --- a/src/csharp/Grpc.Core/ServerPort.cs +++ b/src/csharp/Grpc.Core/ServerPort.cs @@ -62,9 +62,9 @@ namespace Grpc.Core /// <param name="credentials">credentials to use to secure this port.</param> public ServerPort(string host, int port, ServerCredentials credentials) { - this.host = Preconditions.CheckNotNull(host); + this.host = Preconditions.CheckNotNull(host, "host"); this.port = port; - this.credentials = Preconditions.CheckNotNull(credentials); + this.credentials = Preconditions.CheckNotNull(credentials, "credentials"); } /// <summary> diff --git a/src/csharp/Grpc.Core/ServerServiceDefinition.cs b/src/csharp/Grpc.Core/ServerServiceDefinition.cs index a00d156e52..94b0a320c3 100644 --- a/src/csharp/Grpc.Core/ServerServiceDefinition.cs +++ b/src/csharp/Grpc.Core/ServerServiceDefinition.cs @@ -79,7 +79,7 @@ namespace Grpc.Core where TRequest : class where TResponse : class { - callHandlers.Add(method.GetFullName(serviceName), ServerCalls.UnaryCall(method, handler)); + callHandlers.Add(method.FullName, ServerCalls.UnaryCall(method, handler)); return this; } @@ -89,7 +89,7 @@ namespace Grpc.Core where TRequest : class where TResponse : class { - callHandlers.Add(method.GetFullName(serviceName), ServerCalls.ClientStreamingCall(method, handler)); + callHandlers.Add(method.FullName, ServerCalls.ClientStreamingCall(method, handler)); return this; } @@ -99,7 +99,7 @@ namespace Grpc.Core where TRequest : class where TResponse : class { - callHandlers.Add(method.GetFullName(serviceName), ServerCalls.ServerStreamingCall(method, handler)); + callHandlers.Add(method.FullName, ServerCalls.ServerStreamingCall(method, handler)); return this; } @@ -109,7 +109,7 @@ namespace Grpc.Core where TRequest : class where TResponse : class { - callHandlers.Add(method.GetFullName(serviceName), ServerCalls.DuplexStreamingCall(method, handler)); + callHandlers.Add(method.FullName, ServerCalls.DuplexStreamingCall(method, handler)); return this; } diff --git a/src/csharp/Grpc.Core/Status.cs b/src/csharp/Grpc.Core/Status.cs index 754f6cb3ca..6bd8dc820b 100644 --- a/src/csharp/Grpc.Core/Status.cs +++ b/src/csharp/Grpc.Core/Status.cs @@ -29,13 +29,12 @@ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #endregion -using System; -using System.Runtime.InteropServices; +using Grpc.Core.Utils; namespace Grpc.Core { /// <summary> - /// Represents RPC result. + /// Represents RPC result, which consists of <see cref="StatusCode"/> and an optional detail string. /// </summary> public struct Status { @@ -52,6 +51,11 @@ namespace Grpc.Core readonly StatusCode statusCode; readonly string detail; + /// <summary> + /// Creates a new instance of <c>Status</c>. + /// </summary> + /// <param name="statusCode">Status code.</param> + /// <param name="detail">Detail.</param> public Status(StatusCode statusCode, string detail) { this.statusCode = statusCode; @@ -80,6 +84,9 @@ namespace Grpc.Core } } + /// <summary> + /// Returns a <see cref="System.String"/> that represents the current <see cref="Grpc.Core.Status"/>. + /// </summary> public override string ToString() { return string.Format("Status(StatusCode={0}, Detail=\"{1}\")", statusCode, detail); diff --git a/src/csharp/Grpc.Core/StatusCode.cs b/src/csharp/Grpc.Core/StatusCode.cs index a9696fa469..90606955af 100644 --- a/src/csharp/Grpc.Core/StatusCode.cs +++ b/src/csharp/Grpc.Core/StatusCode.cs @@ -31,8 +31,6 @@ #endregion -using System; - namespace Grpc.Core { /// <summary> @@ -41,101 +39,101 @@ namespace Grpc.Core /// </summary> public enum StatusCode { - /* Not an error; returned on success */ + /// <summary>Not an error; returned on success.</summary> OK = 0, - /* The operation was cancelled (typically by the caller). */ + + /// <summary>The operation was cancelled (typically by the caller).</summary> Cancelled = 1, - /* Unknown error. An example of where this error may be returned is - if a Status value received from another address space belongs to - an error-space that is not known in this address space. Also - errors raised by APIs that do not return enough error information - may be converted to this error. */ + + /// <summary> + /// Unknown error. An example of where this error may be returned is + /// if a Status value received from another address space belongs to + /// an error-space that is not known in this address space. Also + /// errors raised by APIs that do not return enough error information + /// may be converted to this error. + /// </summary> Unknown = 2, - /* Client specified an invalid argument. Note that this differs - from FAILED_PRECONDITION. INVALID_ARGUMENT indicates arguments - that are problematic regardless of the state of the system - (e.g., a malformed file name). */ + + /// <summary> + /// Client specified an invalid argument. Note that this differs + /// from FAILED_PRECONDITION. INVALID_ARGUMENT indicates arguments + /// that are problematic regardless of the state of the system + /// (e.g., a malformed file name). + /// </summary> InvalidArgument = 3, - /* Deadline expired before operation could complete. For operations - that change the state of the system, this error may be returned - even if the operation has completed successfully. For example, a - successful response from a server could have been delayed long - enough for the deadline to expire. */ + + /// <summary> + /// Deadline expired before operation could complete. For operations + /// that change the state of the system, this error may be returned + /// even if the operation has completed successfully. For example, a + /// successful response from a server could have been delayed long + /// enough for the deadline to expire. + /// </summary> DeadlineExceeded = 4, - /* Some requested entity (e.g., file or directory) was not found. */ + + /// <summary>Some requested entity (e.g., file or directory) was not found.</summary> NotFound = 5, - /* Some entity that we attempted to create (e.g., file or directory) - already exists. */ + + /// <summary>Some entity that we attempted to create (e.g., file or directory) already exists.</summary> AlreadyExists = 6, - /* The caller does not have permission to execute the specified - operation. PERMISSION_DENIED must not be used for rejections - caused by exhausting some resource (use RESOURCE_EXHAUSTED - instead for those errors). PERMISSION_DENIED must not be - used if the caller can not be identified (use UNAUTHENTICATED - instead for those errors). */ + + /// <summary> + /// The caller does not have permission to execute the specified + /// operation. PERMISSION_DENIED must not be used for rejections + /// caused by exhausting some resource (use RESOURCE_EXHAUSTED + /// instead for those errors). PERMISSION_DENIED must not be + /// used if the caller can not be identified (use UNAUTHENTICATED + /// instead for those errors). + /// </summary> PermissionDenied = 7, - /* The request does not have valid authentication credentials for the - operation. */ + + /// <summary>The request does not have valid authentication credentials for the operation.</summary> Unauthenticated = 16, - /* Some resource has been exhausted, perhaps a per-user quota, or - perhaps the entire file system is out of space. */ + + /// <summary> + /// Some resource has been exhausted, perhaps a per-user quota, or + /// perhaps the entire file system is out of space. + /// </summary> ResourceExhausted = 8, - /* Operation was rejected because the system is not in a state - required for the operation's execution. For example, directory - to be deleted may be non-empty, an rmdir operation is applied to - a non-directory, etc. - - A litmus test that may help a service implementor in deciding - between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE: - (a) Use UNAVAILABLE if the client can retry just the failing call. - (b) Use ABORTED if the client should retry at a higher-level - (e.g., restarting a read-modify-write sequence). - (c) Use FAILED_PRECONDITION if the client should not retry until - the system state has been explicitly fixed. E.g., if an "rmdir" - fails because the directory is non-empty, FAILED_PRECONDITION - should be returned since the client should not retry unless - they have first fixed up the directory by deleting files from it. - (d) Use FAILED_PRECONDITION if the client performs conditional - REST Get/Update/Delete on a resource and the resource on the - server does not match the condition. E.g., conflicting - read-modify-write on the same resource. */ + + /// <summary> + /// Operation was rejected because the system is not in a state + /// required for the operation's execution. For example, directory + /// to be deleted may be non-empty, an rmdir operation is applied to + /// a non-directory, etc. + /// </summary> FailedPrecondition = 9, - /* The operation was aborted, typically due to a concurrency issue - like sequencer check failures, transaction aborts, etc. - See litmus test above for deciding between FAILED_PRECONDITION, - ABORTED, and UNAVAILABLE. */ + /// <summary> + /// The operation was aborted, typically due to a concurrency issue + /// like sequencer check failures, transaction aborts, etc. + /// </summary> Aborted = 10, - /* Operation was attempted past the valid range. E.g., seeking or - reading past end of file. - - Unlike INVALID_ARGUMENT, this error indicates a problem that may - be fixed if the system state changes. For example, a 32-bit file - system will generate INVALID_ARGUMENT if asked to read at an - offset that is not in the range [0,2^32-1], but it will generate - OUT_OF_RANGE if asked to read from an offset past the current - file size. - - There is a fair bit of overlap between FAILED_PRECONDITION and - OUT_OF_RANGE. We recommend using OUT_OF_RANGE (the more specific - error) when it applies so that callers who are iterating through - a space can easily look for an OUT_OF_RANGE error to detect when - they are done. */ + + /// <summary> + /// Operation was attempted past the valid range. E.g., seeking or + /// reading past end of file. + /// </summary> OutOfRange = 11, - /* Operation is not implemented or not supported/enabled in this service. */ + + /// <summary>Operation is not implemented or not supported/enabled in this service.</summary> Unimplemented = 12, - /* Internal errors. Means some invariants expected by underlying - system has been broken. If you see one of these errors, - something is very broken. */ + + /// <summary> + /// Internal errors. Means some invariants expected by underlying + /// system has been broken. If you see one of these errors, + /// something is very broken. + /// </summary> Internal = 13, - /* The service is currently unavailable. This is a most likely a - transient condition and may be corrected by retrying with - a backoff. - See litmus test above for deciding between FAILED_PRECONDITION, - ABORTED, and UNAVAILABLE. */ + /// <summary> + /// The service is currently unavailable. This is a most likely a + /// transient condition and may be corrected by retrying with + /// a backoff. + /// </summary> Unavailable = 14, - /* Unrecoverable data loss or corruption. */ + + /// <summary>Unrecoverable data loss or corruption.</summary> DataLoss = 15 } } diff --git a/src/csharp/Grpc.Core/Utils/AsyncStreamExtensions.cs b/src/csharp/Grpc.Core/Utils/AsyncStreamExtensions.cs index 8a748b45a8..cdf1e51026 100644 --- a/src/csharp/Grpc.Core/Utils/AsyncStreamExtensions.cs +++ b/src/csharp/Grpc.Core/Utils/AsyncStreamExtensions.cs @@ -33,7 +33,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; namespace Grpc.Core.Utils @@ -46,7 +45,7 @@ namespace Grpc.Core.Utils /// <summary> /// Reads the entire stream and executes an async action for each element. /// </summary> - public static async Task ForEach<T>(this IAsyncStreamReader<T> streamReader, Func<T, Task> asyncAction) + public static async Task ForEachAsync<T>(this IAsyncStreamReader<T> streamReader, Func<T, Task> asyncAction) where T : class { while (await streamReader.MoveNext()) @@ -58,7 +57,7 @@ namespace Grpc.Core.Utils /// <summary> /// Reads the entire stream and creates a list containing all the elements read. /// </summary> - public static async Task<List<T>> ToList<T>(this IAsyncStreamReader<T> streamReader) + public static async Task<List<T>> ToListAsync<T>(this IAsyncStreamReader<T> streamReader) where T : class { var result = new List<T>(); @@ -73,7 +72,7 @@ namespace Grpc.Core.Utils /// Writes all elements from given enumerable to the stream. /// Completes the stream afterwards unless close = false. /// </summary> - public static async Task WriteAll<T>(this IClientStreamWriter<T> streamWriter, IEnumerable<T> elements, bool complete = true) + public static async Task WriteAllAsync<T>(this IClientStreamWriter<T> streamWriter, IEnumerable<T> elements, bool complete = true) where T : class { foreach (var element in elements) @@ -89,7 +88,7 @@ namespace Grpc.Core.Utils /// <summary> /// Writes all elements from given enumerable to the stream. /// </summary> - public static async Task WriteAll<T>(this IServerStreamWriter<T> streamWriter, IEnumerable<T> elements) + public static async Task WriteAllAsync<T>(this IServerStreamWriter<T> streamWriter, IEnumerable<T> elements) where T : class { foreach (var element in elements) diff --git a/src/csharp/Grpc.Core/Utils/BenchmarkUtil.cs b/src/csharp/Grpc.Core/Utils/BenchmarkUtil.cs index 82653c3a1f..eb3a5b16e3 100644 --- a/src/csharp/Grpc.Core/Utils/BenchmarkUtil.cs +++ b/src/csharp/Grpc.Core/Utils/BenchmarkUtil.cs @@ -39,6 +39,9 @@ using System.Threading.Tasks; namespace Grpc.Core.Utils { + /// <summary> + /// Utility methods to run microbenchmarks. + /// </summary> public static class BenchmarkUtil { /// <summary> diff --git a/src/csharp/Grpc.Core/Utils/Preconditions.cs b/src/csharp/Grpc.Core/Utils/Preconditions.cs index aeb5d210a7..374262f87a 100644 --- a/src/csharp/Grpc.Core/Utils/Preconditions.cs +++ b/src/csharp/Grpc.Core/Utils/Preconditions.cs @@ -32,17 +32,16 @@ #endregion using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading.Tasks; namespace Grpc.Core.Utils { + /// <summary> + /// Utility methods to simplify checking preconditions in the code. + /// </summary> public static class Preconditions { /// <summary> - /// Throws ArgumentException if condition is false. + /// Throws <see cref="ArgumentException"/> if condition is false. /// </summary> public static void CheckArgument(bool condition) { @@ -53,7 +52,7 @@ namespace Grpc.Core.Utils } /// <summary> - /// Throws ArgumentException with given message if condition is false. + /// Throws <see cref="ArgumentException"/> with given message if condition is false. /// </summary> public static void CheckArgument(bool condition, string errorMessage) { @@ -64,31 +63,31 @@ namespace Grpc.Core.Utils } /// <summary> - /// Throws NullReferenceException if reference is null. + /// Throws <see cref="ArgumentNullException"/> if reference is null. /// </summary> public static T CheckNotNull<T>(T reference) { if (reference == null) { - throw new NullReferenceException(); + throw new ArgumentNullException(); } return reference; } /// <summary> - /// Throws NullReferenceException with given message if reference is null. + /// Throws <see cref="ArgumentNullException"/> if reference is null. /// </summary> - public static T CheckNotNull<T>(T reference, string errorMessage) + public static T CheckNotNull<T>(T reference, string paramName) { if (reference == null) { - throw new NullReferenceException(errorMessage); + throw new ArgumentNullException(paramName); } return reference; } /// <summary> - /// Throws InvalidOperationException if condition is false. + /// Throws <see cref="InvalidOperationException"/> if condition is false. /// </summary> public static void CheckState(bool condition) { @@ -99,7 +98,7 @@ namespace Grpc.Core.Utils } /// <summary> - /// Throws InvalidOperationException with given message if condition is false. + /// Throws <see cref="InvalidOperationException"/> with given message if condition is false. /// </summary> public static void CheckState(bool condition, string errorMessage) { diff --git a/src/csharp/Grpc.Core/Version.cs b/src/csharp/Grpc.Core/Version.cs index b5cb652945..d02b301cac 100644 --- a/src/csharp/Grpc.Core/Version.cs +++ b/src/csharp/Grpc.Core/Version.cs @@ -1,5 +1,37 @@ +#region Copyright notice and license + +// Copyright 2015, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#endregion + using System.Reflection; -using System.Runtime.CompilerServices; // The current version of gRPC C#. -[assembly: AssemblyVersion(Grpc.Core.VersionInfo.CurrentVersion + ".*")] +[assembly: AssemblyVersion(Grpc.Core.VersionInfo.CurrentVersion + ".0")] diff --git a/src/csharp/Grpc.Core/VersionInfo.cs b/src/csharp/Grpc.Core/VersionInfo.cs index eb55af8a13..eda821bc31 100644 --- a/src/csharp/Grpc.Core/VersionInfo.cs +++ b/src/csharp/Grpc.Core/VersionInfo.cs @@ -1,12 +1,45 @@ -using System.Reflection; -using System.Runtime.CompilerServices; +#region Copyright notice and license + +// Copyright 2015, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#endregion namespace Grpc.Core { + /// <summary> + /// Provides info about current version of gRPC. + /// </summary> public static class VersionInfo { /// <summary> - /// Current version of gRPC + /// Current version of gRPC C# /// </summary> public const string CurrentVersion = "0.7.0"; } diff --git a/src/csharp/Grpc.Core/Utils/ExceptionHelper.cs b/src/csharp/Grpc.Core/WriteOptions.cs index c4d6bee058..7ef3189d76 100644 --- a/src/csharp/Grpc.Core/Utils/ExceptionHelper.cs +++ b/src/csharp/Grpc.Core/WriteOptions.cs @@ -33,25 +33,50 @@ using System; -namespace Grpc.Core.Utils +namespace Grpc.Core { - public static class ExceptionHelper + /// <summary> + /// Flags for write operations. + /// </summary> + [Flags] + public enum WriteFlags { /// <summary> - /// If inner exceptions contain RpcException, rethrows it. - /// Otherwise, rethrows the original aggregate exception. - /// Always throws, the exception return type is here only to make the. + /// Hint that the write may be buffered and need not go out on the wire immediately. + /// gRPC is free to buffer the message until the next non-buffered + /// write, or until write stream completion, but it need not buffer completely or at all. /// </summary> - public static Exception UnwrapRpcException(AggregateException ae) + BufferHint = 0x1, + + /// <summary> + /// Force compression to be disabled for a particular write. + /// </summary> + NoCompress = 0x2 + } + + /// <summary> + /// Options for write operations. + /// </summary> + public class WriteOptions + { + /// <summary> + /// Default write options. + /// </summary> + public static readonly WriteOptions Default = new WriteOptions(); + + private WriteFlags flags; + + public WriteOptions(WriteFlags flags = default(WriteFlags)) + { + this.flags = flags; + } + + public WriteFlags Flags { - foreach (var e in ae.InnerExceptions) + get { - if (e is RpcException) - { - throw e; - } + return this.flags; } - throw ae; } } } diff --git a/src/csharp/Grpc.Examples.Tests/MathClientServerTests.cs b/src/csharp/Grpc.Examples.Tests/MathClientServerTests.cs index a2348defc3..91c82b586b 100644 --- a/src/csharp/Grpc.Examples.Tests/MathClientServerTests.cs +++ b/src/csharp/Grpc.Examples.Tests/MathClientServerTests.cs @@ -92,15 +92,8 @@ namespace Math.Tests [Test] public void DivByZero() { - try - { - DivReply response = client.Div(new DivArgs { Dividend = 0, Divisor = 0 }); - Assert.Fail(); - } - catch (RpcException e) - { - Assert.AreEqual(StatusCode.Unknown, e.Status.StatusCode); - } + var ex = Assert.Throws<RpcException>(() => client.Div(new DivArgs { Dividend = 0, Divisor = 0 })); + Assert.AreEqual(StatusCode.Unknown, ex.Status.StatusCode); } [Test] @@ -116,7 +109,7 @@ namespace Math.Tests { using (var call = client.Fib(new FibArgs { Limit = 6 })) { - var responses = await call.ResponseStream.ToList(); + var responses = await call.ResponseStream.ToListAsync(); CollectionAssert.AreEqual(new List<long> { 1, 1, 2, 3, 5, 8 }, responses.ConvertAll((n) => n.Num_)); } @@ -157,15 +150,10 @@ namespace Math.Tests using (var call = client.Fib(new FibArgs { Limit = 0 }, deadline: DateTime.UtcNow.AddMilliseconds(500))) { - try - { - await call.ResponseStream.ToList(); - Assert.Fail(); - } - catch (RpcException e) - { - Assert.AreEqual(StatusCode.DeadlineExceeded, e.Status.StatusCode); - } + var ex = Assert.Throws<RpcException>(async () => await call.ResponseStream.ToListAsync()); + + // We can't guarantee the status code always DeadlineExceeded. See issue #2685. + Assert.Contains(ex.Status.StatusCode, new[] { StatusCode.DeadlineExceeded, StatusCode.Internal }); } } @@ -177,7 +165,7 @@ namespace Math.Tests { var numbers = new List<long> { 10, 20, 30 }.ConvertAll(n => new Num{ Num_ = n }); - await call.RequestStream.WriteAll(numbers); + await call.RequestStream.WriteAllAsync(numbers); var result = await call.ResponseAsync; Assert.AreEqual(60, result.Num_); } @@ -195,8 +183,8 @@ namespace Math.Tests using (var call = client.DivMany()) { - await call.RequestStream.WriteAll(divArgsList); - var result = await call.ResponseStream.ToList(); + await call.RequestStream.WriteAllAsync(divArgsList); + var result = await call.ResponseStream.ToListAsync(); CollectionAssert.AreEqual(new long[] { 3, 4, 3 }, result.ConvertAll((divReply) => divReply.Quotient)); CollectionAssert.AreEqual(new long[] { 1, 16, 1 }, result.ConvertAll((divReply) => divReply.Remainder)); diff --git a/src/csharp/Grpc.Examples/MathExamples.cs b/src/csharp/Grpc.Examples/MathExamples.cs index e25384766f..8009ccbbfa 100644 --- a/src/csharp/Grpc.Examples/MathExamples.cs +++ b/src/csharp/Grpc.Examples/MathExamples.cs @@ -54,7 +54,7 @@ namespace Math { using (var call = client.Fib(new FibArgs { Limit = 5 })) { - List<Num> result = await call.ResponseStream.ToList(); + List<Num> result = await call.ResponseStream.ToListAsync(); Console.WriteLine("Fib Result: " + string.Join("|", result)); } } @@ -70,7 +70,7 @@ namespace Math using (var call = client.Sum()) { - await call.RequestStream.WriteAll(numbers); + await call.RequestStream.WriteAllAsync(numbers); Console.WriteLine("Sum Result: " + await call.ResponseAsync); } } @@ -85,8 +85,8 @@ namespace Math }; using (var call = client.DivMany()) { - await call.RequestStream.WriteAll(divArgsList); - Console.WriteLine("DivMany Result: " + string.Join("|", await call.ResponseStream.ToList())); + await call.RequestStream.WriteAllAsync(divArgsList); + Console.WriteLine("DivMany Result: " + string.Join("|", await call.ResponseStream.ToListAsync())); } } @@ -102,7 +102,7 @@ namespace Math Num sum; using (var sumCall = client.Sum()) { - await sumCall.RequestStream.WriteAll(numbers); + await sumCall.RequestStream.WriteAllAsync(numbers); sum = await sumCall.ResponseAsync; } diff --git a/src/csharp/Grpc.Examples/MathServiceImpl.cs b/src/csharp/Grpc.Examples/MathServiceImpl.cs index 788ce90343..71dc655e46 100644 --- a/src/csharp/Grpc.Examples/MathServiceImpl.cs +++ b/src/csharp/Grpc.Examples/MathServiceImpl.cs @@ -75,7 +75,7 @@ namespace Math public async Task<Num> Sum(IAsyncStreamReader<Num> requestStream, ServerCallContext context) { long sum = 0; - await requestStream.ForEach(async num => + await requestStream.ForEachAsync(async num => { sum += num.Num_; }); @@ -84,10 +84,7 @@ namespace Math public async Task DivMany(IAsyncStreamReader<DivArgs> requestStream, IServerStreamWriter<DivReply> responseStream, ServerCallContext context) { - await requestStream.ForEach(async divArgs => - { - await responseStream.WriteAsync(DivInternal(divArgs)); - }); + await requestStream.ForEachAsync(async divArgs => await responseStream.WriteAsync(DivInternal(divArgs))); } static DivReply DivInternal(DivArgs args) diff --git a/src/csharp/Grpc.HealthCheck.Tests/HealthServiceImplTest.cs b/src/csharp/Grpc.HealthCheck.Tests/HealthServiceImplTest.cs index 04660cd396..8de8645cd1 100644 --- a/src/csharp/Grpc.HealthCheck.Tests/HealthServiceImplTest.cs +++ b/src/csharp/Grpc.HealthCheck.Tests/HealthServiceImplTest.cs @@ -92,11 +92,11 @@ namespace Grpc.HealthCheck.Tests public void NullsRejected() { var impl = new HealthServiceImpl(); - Assert.Throws(typeof(NullReferenceException), () => impl.SetStatus(null, "", HealthCheckResponse.Types.ServingStatus.SERVING)); - Assert.Throws(typeof(NullReferenceException), () => impl.SetStatus("", null, HealthCheckResponse.Types.ServingStatus.SERVING)); + Assert.Throws(typeof(ArgumentNullException), () => impl.SetStatus(null, "", HealthCheckResponse.Types.ServingStatus.SERVING)); + Assert.Throws(typeof(ArgumentNullException), () => impl.SetStatus("", null, HealthCheckResponse.Types.ServingStatus.SERVING)); - Assert.Throws(typeof(NullReferenceException), () => impl.ClearStatus(null, "")); - Assert.Throws(typeof(NullReferenceException), () => impl.ClearStatus("", null)); + Assert.Throws(typeof(ArgumentNullException), () => impl.ClearStatus(null, "")); + Assert.Throws(typeof(ArgumentNullException), () => impl.ClearStatus("", null)); } private static HealthCheckResponse.Types.ServingStatus GetStatusHelper(HealthServiceImpl impl, string host, string service) diff --git a/src/csharp/Grpc.IntegrationTesting.Client/app.config b/src/csharp/Grpc.IntegrationTesting.Client/app.config index 0a82bb4f16..84d7534d65 100644 --- a/src/csharp/Grpc.IntegrationTesting.Client/app.config +++ b/src/csharp/Grpc.IntegrationTesting.Client/app.config @@ -10,6 +10,10 @@ <assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-4.2.28.0" newVersion="4.0.0.0" /> </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="Google.Apis.Core" publicKeyToken="4b01fa6e34db77ab" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-1.9.2.38523" newVersion="1.9.2.38523" /> + </dependentAssembly> </assemblyBinding> </runtime> </configuration>
\ No newline at end of file diff --git a/src/csharp/Grpc.IntegrationTesting.Server/app.config b/src/csharp/Grpc.IntegrationTesting.Server/app.config index 0a82bb4f16..84d7534d65 100644 --- a/src/csharp/Grpc.IntegrationTesting.Server/app.config +++ b/src/csharp/Grpc.IntegrationTesting.Server/app.config @@ -10,6 +10,10 @@ <assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-4.2.28.0" newVersion="4.0.0.0" /> </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="Google.Apis.Core" publicKeyToken="4b01fa6e34db77ab" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-1.9.2.38523" newVersion="1.9.2.38523" /> + </dependentAssembly> </assemblyBinding> </runtime> </configuration>
\ No newline at end of file diff --git a/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj b/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj index a8d85e4ef6..b115eac909 100644 --- a/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj +++ b/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj @@ -8,7 +8,7 @@ <RootNamespace>Grpc.IntegrationTesting</RootNamespace> <AssemblyName>Grpc.IntegrationTesting</AssemblyName> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> - <NuGetPackageImportStamp>041c163e</NuGetPackageImportStamp> + <NuGetPackageImportStamp>6566287f</NuGetPackageImportStamp> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -38,52 +38,51 @@ <AssemblyOriginatorKeyFile>C:\keys\Grpc.snk</AssemblyOriginatorKeyFile> </PropertyGroup> <ItemGroup> - <Reference Include="BouncyCastle.Crypto"> + <Reference Include="BouncyCastle.Crypto, Version=1.7.4137.9688, Culture=neutral, PublicKeyToken=a4292a325f69b123, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> <HintPath>..\packages\BouncyCastle.1.7.0\lib\Net40-Client\BouncyCastle.Crypto.dll</HintPath> </Reference> <Reference Include="Google.Protobuf"> <HintPath>..\packages\Google.Protobuf.3.0.0-a20150813-2093749ca\lib\portable-net45+netcore45+wpa81+wp8\Google.Protobuf.dll</HintPath> </Reference> - <Reference Include="nunit.framework"> - <HintPath>..\packages\NUnit.2.6.4\lib\nunit.framework.dll</HintPath> + <Reference Include="Google.Apis.Auth, Version=1.9.3.19379, Culture=neutral, PublicKeyToken=4b01fa6e34db77ab, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\packages\Google.Apis.Auth.1.9.3\lib\net40\Google.Apis.Auth.dll</HintPath> </Reference> - <Reference Include="System" /> - <Reference Include="System.Interactive.Async"> - <HintPath>..\packages\Ix-Async.1.2.3\lib\net45\System.Interactive.Async.dll</HintPath> + <Reference Include="Google.Apis.Auth.PlatformServices, Version=1.9.3.19383, Culture=neutral, PublicKeyToken=4b01fa6e34db77ab, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\packages\Google.Apis.Auth.1.9.3\lib\net40\Google.Apis.Auth.PlatformServices.dll</HintPath> </Reference> - <Reference Include="System.Net" /> - <Reference Include="System.Net.Http" /> - <Reference Include="System.Net.Http.WebRequest" /> - <Reference Include="Microsoft.Threading.Tasks"> + <Reference Include="Google.Apis.Core, Version=1.9.3.19379, Culture=neutral, PublicKeyToken=4b01fa6e34db77ab, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\packages\Google.Apis.Core.1.9.3\lib\portable-net40+sl50+win+wpa81+wp80\Google.Apis.Core.dll</HintPath> + </Reference> + <Reference Include="Microsoft.Threading.Tasks, Version=1.0.12.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> <HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll</HintPath> </Reference> - <Reference Include="Microsoft.Threading.Tasks.Extensions"> + <Reference Include="Microsoft.Threading.Tasks.Extensions, Version=1.0.12.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> <HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll</HintPath> </Reference> - <Reference Include="Microsoft.Threading.Tasks.Extensions.Desktop"> + <Reference Include="Microsoft.Threading.Tasks.Extensions.Desktop, Version=1.0.168.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> <HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll</HintPath> </Reference> - <Reference Include="System.Collections.Immutable"> - <HintPath>..\packages\System.Collections.Immutable.1.1.36\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll</HintPath> - </Reference> - <Reference Include="Google.Apis.Auth"> - <HintPath>..\packages\Google.Apis.Auth.1.9.2\lib\net40\Google.Apis.Auth.dll</HintPath> - </Reference> - <Reference Include="Google.Apis.Auth.PlatformServices"> - <HintPath>..\packages\Google.Apis.Auth.1.9.2\lib\net40\Google.Apis.Auth.PlatformServices.dll</HintPath> - </Reference> - <Reference Include="Google.Apis.Core"> - <HintPath>..\packages\Google.Apis.Core.1.9.2\lib\portable-net40+sl50+win+wpa81+wp80\Google.Apis.Core.dll</HintPath> - </Reference> - <Reference Include="Newtonsoft.Json"> + <Reference Include="Newtonsoft.Json, Version=7.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> <HintPath>..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll</HintPath> </Reference> - <Reference Include="System.Net.Http.Extensions"> - <HintPath>..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Extensions.dll</HintPath> + <Reference Include="nunit.framework"> + <HintPath>..\packages\NUnit.2.6.4\lib\nunit.framework.dll</HintPath> </Reference> - <Reference Include="System.Net.Http.Primitives"> - <HintPath>..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Primitives.dll</HintPath> + <Reference Include="System" /> + <Reference Include="System.Interactive.Async"> + <HintPath>..\packages\Ix-Async.1.2.3\lib\net45\System.Interactive.Async.dll</HintPath> </Reference> + <Reference Include="System.Net" /> + <Reference Include="System.Net.Http" /> + <Reference Include="System.Net.Http.WebRequest" /> </ItemGroup> <ItemGroup> <Compile Include="..\Grpc.Core\Version.cs"> diff --git a/src/csharp/Grpc.IntegrationTesting/InteropClient.cs b/src/csharp/Grpc.IntegrationTesting/InteropClient.cs index 93d0bbe772..3ea1539230 100644 --- a/src/csharp/Grpc.IntegrationTesting/InteropClient.cs +++ b/src/csharp/Grpc.IntegrationTesting/InteropClient.cs @@ -43,6 +43,7 @@ using Grpc.Core.Utils; using Grpc.Testing; using NUnit.Framework; using Google.Protobuf; +using Google.Apis.Auth.OAuth2; namespace Grpc.IntegrationTesting { @@ -97,10 +98,10 @@ namespace Grpc.IntegrationTesting } var interopClient = new InteropClient(options); - interopClient.Run(); + interopClient.Run().Wait(); } - private void Run() + private async Task Run() { Credentials credentials = null; if (options.useTls) @@ -120,17 +121,7 @@ namespace Grpc.IntegrationTesting using (Channel channel = new Channel(options.serverHost, options.serverPort.Value, credentials, channelOptions)) { TestService.TestServiceClient client = new TestService.TestServiceClient(channel); - if (options.testCase == "service_account_creds" || options.testCase == "compute_engine_creds") - { - var credential = GoogleCredential.GetApplicationDefault(); - if (credential.IsCreateScopedRequired) - { - credential = credential.CreateScoped(new[] { AuthScope }); - } - client.HeaderInterceptor = OAuth2Interceptors.FromCredential(credential); - } - - RunTestCaseAsync(options.testCase, client).Wait(); + await RunTestCaseAsync(options.testCase, client); } GrpcEnvironment.Shutdown(); } @@ -158,16 +149,19 @@ namespace Grpc.IntegrationTesting await RunEmptyStreamAsync(client); break; case "service_account_creds": - RunServiceAccountCreds(client); + await RunServiceAccountCredsAsync(client); break; case "compute_engine_creds": - RunComputeEngineCreds(client); + await RunComputeEngineCredsAsync(client); + break; + case "jwt_token_creds": + await RunJwtTokenCredsAsync(client); break; case "oauth2_auth_token": - RunOAuth2AuthToken(client); + await RunOAuth2AuthTokenAsync(client); break; case "per_rpc_creds": - RunPerRpcCreds(client); + await RunPerRpcCredsAsync(client); break; case "cancel_after_begin": await RunCancelAfterBeginAsync(client); @@ -216,7 +210,7 @@ namespace Grpc.IntegrationTesting using (var call = client.StreamingInputCall()) { - await call.RequestStream.WriteAll(bodySizes); + await call.RequestStream.WriteAllAsync(bodySizes); var response = await call.ResponseAsync; Assert.AreEqual(74922, response.AggregatedPayloadSize); @@ -238,7 +232,7 @@ namespace Grpc.IntegrationTesting using (var call = client.StreamingOutputCall(request)) { - var responseList = await call.ResponseStream.ToList(); + var responseList = await call.ResponseStream.ToListAsync(); foreach (var res in responseList) { Assert.AreEqual(PayloadType.COMPRESSABLE, res.Payload.Type); @@ -312,15 +306,19 @@ namespace Grpc.IntegrationTesting { await call.RequestStream.CompleteAsync(); - var responseList = await call.ResponseStream.ToList(); + var responseList = await call.ResponseStream.ToListAsync(); Assert.AreEqual(0, responseList.Count); } Console.WriteLine("Passed!"); } - public static void RunServiceAccountCreds(TestService.ITestServiceClient client) + public static async Task RunServiceAccountCredsAsync(TestService.TestServiceClient client) { Console.WriteLine("running service_account_creds"); + var credential = await GoogleCredential.GetApplicationDefaultAsync(); + credential = credential.CreateScoped(new[] { AuthScope }); + client.HeaderInterceptor = OAuth2Interceptors.FromCredential(credential); + var request = new SimpleRequest { ResponseType = PayloadType.COMPRESSABLE, @@ -339,9 +337,13 @@ namespace Grpc.IntegrationTesting Console.WriteLine("Passed!"); } - public static void RunComputeEngineCreds(TestService.ITestServiceClient client) + public static async Task RunComputeEngineCredsAsync(TestService.TestServiceClient client) { Console.WriteLine("running compute_engine_creds"); + var credential = await GoogleCredential.GetApplicationDefaultAsync(); + Assert.IsFalse(credential.IsCreateScopedRequired); + client.HeaderInterceptor = OAuth2Interceptors.FromCredential(credential); + var request = new SimpleRequest { ResponseType = PayloadType.COMPRESSABLE, @@ -360,12 +362,36 @@ namespace Grpc.IntegrationTesting Console.WriteLine("Passed!"); } - public static void RunOAuth2AuthToken(TestService.TestServiceClient client) + public static async Task RunJwtTokenCredsAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running jwt_token_creds"); + var credential = await GoogleCredential.GetApplicationDefaultAsync(); + // check this a credential with scope support, but don't add the scope. + Assert.IsTrue(credential.IsCreateScopedRequired); + client.HeaderInterceptor = OAuth2Interceptors.FromCredential(credential); + + var request = new SimpleRequest + { + ResponseType = PayloadType.COMPRESSABLE, + ResponseSize = 314159, + Payload = CreateZerosPayload(271828), + FillUsername = true, + FillOauthScope = true + }; + + var response = client.UnaryCall(request); + + Assert.AreEqual(PayloadType.COMPRESSABLE, response.Payload.Type); + Assert.AreEqual(314159, response.Payload.Body.Length); + Assert.AreEqual(ServiceAccountUser, response.Username); + Console.WriteLine("Passed!"); + } + + public static async Task RunOAuth2AuthTokenAsync(TestService.TestServiceClient client) { Console.WriteLine("running oauth2_auth_token"); - var credential = GoogleCredential.GetApplicationDefault().CreateScoped(new[] { AuthScope }); - Assert.IsTrue(credential.RequestAccessTokenAsync(CancellationToken.None).Result); - string oauth2Token = credential.Token.AccessToken; + ITokenAccess credential = (await GoogleCredential.GetApplicationDefaultAsync()).CreateScoped(new[] { AuthScope }); + string oauth2Token = await credential.GetAccessTokenForRequestAsync(); client.HeaderInterceptor = OAuth2Interceptors.FromAccessToken(oauth2Token); @@ -382,13 +408,12 @@ namespace Grpc.IntegrationTesting Console.WriteLine("Passed!"); } - public static void RunPerRpcCreds(TestService.TestServiceClient client) + public static async Task RunPerRpcCredsAsync(TestService.TestServiceClient client) { Console.WriteLine("running per_rpc_creds"); - var credential = GoogleCredential.GetApplicationDefault().CreateScoped(new[] { AuthScope }); - Assert.IsTrue(credential.RequestAccessTokenAsync(CancellationToken.None).Result); - string oauth2Token = credential.Token.AccessToken; + ITokenAccess credential = (await GoogleCredential.GetApplicationDefaultAsync()).CreateScoped(new[] { AuthScope }); + string oauth2Token = await credential.GetAccessTokenForRequestAsync(); var headerInterceptor = OAuth2Interceptors.FromAccessToken(oauth2Token); var request = new SimpleRequest @@ -398,7 +423,7 @@ namespace Grpc.IntegrationTesting }; var headers = new Metadata(); - headerInterceptor(headers); + headerInterceptor("", headers); var response = client.UnaryCall(request, headers: headers); Assert.AreEqual(AuthScopeResponse, response.OauthScope); @@ -417,15 +442,8 @@ namespace Grpc.IntegrationTesting await Task.Delay(1000); cts.Cancel(); - try - { - var response = await call.ResponseAsync; - Assert.Fail(); - } - catch (RpcException e) - { - Assert.AreEqual(StatusCode.Cancelled, e.Status.StatusCode); - } + var ex = Assert.Throws<RpcException>(async () => await call.ResponseAsync); + Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode); } Console.WriteLine("Passed!"); } @@ -450,15 +468,8 @@ namespace Grpc.IntegrationTesting cts.Cancel(); - try - { - await call.ResponseStream.MoveNext(); - Assert.Fail(); - } - catch (RpcException e) - { - Assert.AreEqual(StatusCode.Cancelled, e.Status.StatusCode); - } + var ex = Assert.Throws<RpcException>(async () => await call.ResponseStream.MoveNext()); + Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode); } Console.WriteLine("Passed!"); } diff --git a/src/csharp/Grpc.IntegrationTesting/TestServiceImpl.cs b/src/csharp/Grpc.IntegrationTesting/TestServiceImpl.cs index 4ab1266f25..c5bfcf08c0 100644 --- a/src/csharp/Grpc.IntegrationTesting/TestServiceImpl.cs +++ b/src/csharp/Grpc.IntegrationTesting/TestServiceImpl.cs @@ -69,7 +69,7 @@ namespace Grpc.Testing public async Task<StreamingInputCallResponse> StreamingInputCall(IAsyncStreamReader<StreamingInputCallRequest> requestStream, ServerCallContext context) { int sum = 0; - await requestStream.ForEach(async request => + await requestStream.ForEachAsync(async request => { sum += request.Payload.Body.Length; }); @@ -78,7 +78,7 @@ namespace Grpc.Testing public async Task FullDuplexCall(IAsyncStreamReader<StreamingOutputCallRequest> requestStream, IServerStreamWriter<StreamingOutputCallResponse> responseStream, ServerCallContext context) { - await requestStream.ForEach(async request => + await requestStream.ForEachAsync(async request => { foreach (var responseParam in request.ResponseParameters) { diff --git a/src/csharp/Grpc.IntegrationTesting/app.config b/src/csharp/Grpc.IntegrationTesting/app.config index 0a82bb4f16..84d7534d65 100644 --- a/src/csharp/Grpc.IntegrationTesting/app.config +++ b/src/csharp/Grpc.IntegrationTesting/app.config @@ -10,6 +10,10 @@ <assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-4.2.28.0" newVersion="4.0.0.0" /> </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="Google.Apis.Core" publicKeyToken="4b01fa6e34db77ab" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-1.9.2.38523" newVersion="1.9.2.38523" /> + </dependentAssembly> </assemblyBinding> </runtime> </configuration>
\ No newline at end of file diff --git a/src/csharp/Grpc.IntegrationTesting/packages.config b/src/csharp/Grpc.IntegrationTesting/packages.config index b6ed3c8532..54b594bb63 100644 --- a/src/csharp/Grpc.IntegrationTesting/packages.config +++ b/src/csharp/Grpc.IntegrationTesting/packages.config @@ -1,9 +1,10 @@ <?xml version="1.0" encoding="utf-8"?> <packages> <package id="BouncyCastle" version="1.7.0" targetFramework="net45" /> - <package id="Google.Apis.Auth" version="1.9.2" targetFramework="net45" /> - <package id="Google.Apis.Core" version="1.9.2" targetFramework="net45" /> <package id="Google.Protobuf" version="3.0.0-a20150813-2093749ca" targetFramework="net45" /> + <package id="Google.Apis.Auth" version="1.9.3" targetFramework="net45" /> + <package id="Google.Apis.Core" version="1.9.3" targetFramework="net45" /> + <package id="Google.ProtocolBuffers" version="2.4.1.521" targetFramework="net45" /> <package id="Ix-Async" version="1.2.3" targetFramework="net45" /> <package id="Microsoft.Bcl" version="1.1.10" targetFramework="net45" /> <package id="Microsoft.Bcl.Async" version="1.0.168" targetFramework="net45" /> diff --git a/src/csharp/doc/README.md b/src/csharp/doc/README.md new file mode 100644 index 0000000000..585500b5ca --- /dev/null +++ b/src/csharp/doc/README.md @@ -0,0 +1,2 @@ + +SandCastle project files to generate HTML reference documentation.
\ No newline at end of file diff --git a/src/csharp/doc/grpc_csharp_public.shfbproj b/src/csharp/doc/grpc_csharp_public.shfbproj new file mode 100644 index 0000000000..05c93f4a13 --- /dev/null +++ b/src/csharp/doc/grpc_csharp_public.shfbproj @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <!-- The configuration and platform will be used to determine which assemblies to include from solution and + project documentation sources --> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <SchemaVersion>2.0</SchemaVersion> + <ProjectGuid>{77e3da09-fc92-486f-a90a-99ca788e8b59}</ProjectGuid> + <SHFBSchemaVersion>2015.6.5.0</SHFBSchemaVersion> + <!-- AssemblyName, Name, and RootNamespace are not used by SHFB but Visual Studio adds them anyway --> + <AssemblyName>Documentation</AssemblyName> + <RootNamespace>Documentation</RootNamespace> + <Name>Documentation</Name> + <!-- SHFB properties --> + <FrameworkVersion>.NET Framework 4.5</FrameworkVersion> + <OutputPath>..\..\..\doc\ref\csharp\html</OutputPath> + <Language>en-US</Language> + <DocumentationSources> + <DocumentationSource sourceFile="..\Grpc.Auth\Grpc.Auth.csproj" /> +<DocumentationSource sourceFile="..\Grpc.Core\Grpc.Core.csproj" /></DocumentationSources> + <BuildAssemblerVerbosity>OnlyWarningsAndErrors</BuildAssemblerVerbosity> + <HelpFileFormat>Website</HelpFileFormat> + <IndentHtml>False</IndentHtml> + <KeepLogFile>True</KeepLogFile> + <DisableCodeBlockComponent>False</DisableCodeBlockComponent> + <CleanIntermediates>True</CleanIntermediates> + <HelpFileVersion>1.0.0.0</HelpFileVersion> + <MaximumGroupParts>2</MaximumGroupParts> + <NamespaceGrouping>False</NamespaceGrouping> + <SyntaxFilters>Standard</SyntaxFilters> + <SdkLinkTarget>Blank</SdkLinkTarget> + <RootNamespaceContainer>True</RootNamespaceContainer> + <PresentationStyle>VS2013</PresentationStyle> + <Preliminary>False</Preliminary> + <NamingMethod>MemberName</NamingMethod> + <HelpTitle>gRPC C#</HelpTitle> + <ContentPlacement>AboveNamespaces</ContentPlacement> + <HtmlHelpName>Documentation</HtmlHelpName> + </PropertyGroup> + <!-- There are no properties for these groups. AnyCPU needs to appear in order for Visual Studio to perform + the build. The others are optional common platform types that may appear. --> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' "> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' "> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x64' "> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x64' "> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|Win32' "> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|Win32' "> + </PropertyGroup> + <!-- Import the SHFB build targets --> + <Import Project="$(SHFBROOT)\SandcastleHelpFileBuilder.targets" /> + <!-- The pre-build and post-build event properties must appear *after* the targets file import in order to be + evaluated correctly. --> + <PropertyGroup> + <PreBuildEvent> + </PreBuildEvent> + <PostBuildEvent> + </PostBuildEvent> + <RunPostBuildEvent>OnBuildSuccess</RunPostBuildEvent> + </PropertyGroup> +</Project>
\ No newline at end of file diff --git a/src/csharp/ext/grpc_csharp_ext.c b/src/csharp/ext/grpc_csharp_ext.c index 048887bc12..9379ae01f1 100644 --- a/src/csharp/ext/grpc_csharp_ext.c +++ b/src/csharp/ext/grpc_csharp_ext.c @@ -376,10 +376,12 @@ GPR_EXPORT void GPR_CALLTYPE grpcsharp_channel_destroy(grpc_channel *channel) { } GPR_EXPORT grpc_call *GPR_CALLTYPE -grpcsharp_channel_create_call(grpc_channel *channel, grpc_completion_queue *cq, +grpcsharp_channel_create_call(grpc_channel *channel, grpc_call *parent_call, + gpr_uint32 propagation_mask, + grpc_completion_queue *cq, const char *method, const char *host, gpr_timespec deadline) { - return grpc_channel_create_call(channel, NULL, GRPC_PROPAGATE_DEFAULTS, cq, + return grpc_channel_create_call(channel, parent_call, propagation_mask, cq, method, host, deadline); } @@ -497,7 +499,7 @@ GPR_EXPORT void GPR_CALLTYPE grpcsharp_call_destroy(grpc_call *call) { GPR_EXPORT grpc_call_error GPR_CALLTYPE grpcsharp_call_start_unary(grpc_call *call, grpcsharp_batch_context *ctx, const char *send_buffer, size_t send_buffer_len, - grpc_metadata_array *initial_metadata) { + grpc_metadata_array *initial_metadata, gpr_uint32 write_flags) { /* TODO: don't use magic number */ grpc_op ops[6]; ops[0].op = GRPC_OP_SEND_INITIAL_METADATA; @@ -511,7 +513,7 @@ grpcsharp_call_start_unary(grpc_call *call, grpcsharp_batch_context *ctx, ops[1].op = GRPC_OP_SEND_MESSAGE; ctx->send_message = string_to_byte_buffer(send_buffer, send_buffer_len); ops[1].data.send_message = ctx->send_message; - ops[1].flags = 0; + ops[1].flags = write_flags; ops[2].op = GRPC_OP_SEND_CLOSE_FROM_CLIENT; ops[2].flags = 0; @@ -578,7 +580,7 @@ grpcsharp_call_start_client_streaming(grpc_call *call, GPR_EXPORT grpc_call_error GPR_CALLTYPE grpcsharp_call_start_server_streaming( grpc_call *call, grpcsharp_batch_context *ctx, const char *send_buffer, - size_t send_buffer_len, grpc_metadata_array *initial_metadata) { + size_t send_buffer_len, grpc_metadata_array *initial_metadata, gpr_uint32 write_flags) { /* TODO: don't use magic number */ grpc_op ops[5]; ops[0].op = GRPC_OP_SEND_INITIAL_METADATA; @@ -592,7 +594,7 @@ GPR_EXPORT grpc_call_error GPR_CALLTYPE grpcsharp_call_start_server_streaming( ops[1].op = GRPC_OP_SEND_MESSAGE; ctx->send_message = string_to_byte_buffer(send_buffer, send_buffer_len); ops[1].data.send_message = ctx->send_message; - ops[1].flags = 0; + ops[1].flags = write_flags; ops[2].op = GRPC_OP_SEND_CLOSE_FROM_CLIENT; ops[2].flags = 0; @@ -651,15 +653,22 @@ grpcsharp_call_start_duplex_streaming(grpc_call *call, GPR_EXPORT grpc_call_error GPR_CALLTYPE grpcsharp_call_send_message(grpc_call *call, grpcsharp_batch_context *ctx, - const char *send_buffer, size_t send_buffer_len) { + const char *send_buffer, size_t send_buffer_len, + gpr_uint32 write_flags, + gpr_int32 send_empty_initial_metadata) { /* TODO: don't use magic number */ - grpc_op ops[1]; + grpc_op ops[2]; + size_t nops = send_empty_initial_metadata ? 2 : 1; ops[0].op = GRPC_OP_SEND_MESSAGE; ctx->send_message = string_to_byte_buffer(send_buffer, send_buffer_len); ops[0].data.send_message = ctx->send_message; - ops[0].flags = 0; + ops[0].flags = write_flags; + ops[1].op = GRPC_OP_SEND_INITIAL_METADATA; + ops[1].data.send_initial_metadata.count = 0; + ops[1].data.send_initial_metadata.metadata = NULL; + ops[1].flags = 0; - return grpc_call_start_batch(call, ops, sizeof(ops) / sizeof(ops[0]), ctx); + return grpc_call_start_batch(call, ops, nops, ctx); } GPR_EXPORT grpc_call_error GPR_CALLTYPE @@ -675,9 +684,11 @@ grpcsharp_call_send_close_from_client(grpc_call *call, GPR_EXPORT grpc_call_error GPR_CALLTYPE grpcsharp_call_send_status_from_server( grpc_call *call, grpcsharp_batch_context *ctx, grpc_status_code status_code, - const char *status_details, grpc_metadata_array *trailing_metadata) { + const char *status_details, grpc_metadata_array *trailing_metadata, + gpr_int32 send_empty_initial_metadata) { /* TODO: don't use magic number */ - grpc_op ops[1]; + grpc_op ops[2]; + size_t nops = send_empty_initial_metadata ? 2 : 1; ops[0].op = GRPC_OP_SEND_STATUS_FROM_SERVER; ops[0].data.send_status_from_server.status = status_code; ops[0].data.send_status_from_server.status_details = @@ -689,8 +700,12 @@ GPR_EXPORT grpc_call_error GPR_CALLTYPE grpcsharp_call_send_status_from_server( ops[0].data.send_status_from_server.trailing_metadata = ctx->send_status_from_server.trailing_metadata.metadata; ops[0].flags = 0; + ops[1].op = GRPC_OP_SEND_INITIAL_METADATA; + ops[1].data.send_initial_metadata.count = 0; + ops[1].data.send_initial_metadata.metadata = NULL; + ops[1].flags = 0; - return grpc_call_start_batch(call, ops, sizeof(ops) / sizeof(ops[0]), ctx); + return grpc_call_start_batch(call, ops, nops, ctx); } GPR_EXPORT grpc_call_error GPR_CALLTYPE @@ -706,16 +721,28 @@ grpcsharp_call_recv_message(grpc_call *call, grpcsharp_batch_context *ctx) { GPR_EXPORT grpc_call_error GPR_CALLTYPE grpcsharp_call_start_serverside(grpc_call *call, grpcsharp_batch_context *ctx) { /* TODO: don't use magic number */ - grpc_op ops[2]; - ops[0].op = GRPC_OP_SEND_INITIAL_METADATA; - ops[0].data.send_initial_metadata.count = 0; - ops[0].data.send_initial_metadata.metadata = NULL; + grpc_op ops[1]; + ops[0].op = GRPC_OP_RECV_CLOSE_ON_SERVER; + ops[0].data.recv_close_on_server.cancelled = + (&ctx->recv_close_on_server_cancelled); ops[0].flags = 0; - ops[1].op = GRPC_OP_RECV_CLOSE_ON_SERVER; - ops[1].data.recv_close_on_server.cancelled = - (&ctx->recv_close_on_server_cancelled); - ops[1].flags = 0; + return grpc_call_start_batch(call, ops, sizeof(ops) / sizeof(ops[0]), ctx); +} + +GPR_EXPORT grpc_call_error GPR_CALLTYPE +grpcsharp_call_send_initial_metadata(grpc_call *call, + grpcsharp_batch_context *ctx, + grpc_metadata_array *initial_metadata) { + /* TODO: don't use magic number */ + grpc_op ops[1]; + ops[0].op = GRPC_OP_SEND_INITIAL_METADATA; + grpcsharp_metadata_array_move(&(ctx->send_initial_metadata), + initial_metadata); + ops[0].data.send_initial_metadata.count = ctx->send_initial_metadata.count; + ops[0].data.send_initial_metadata.metadata = + ctx->send_initial_metadata.metadata; + ops[0].flags = 0; return grpc_call_start_batch(call, ops, sizeof(ops) / sizeof(ops[0]), ctx); } @@ -849,6 +876,11 @@ GPR_EXPORT void GPR_CALLTYPE grpcsharp_redirect_log(grpcsharp_log_func func) { typedef void(GPR_CALLTYPE *test_callback_funcptr)(gpr_int32 success); +/* Version info */ +GPR_EXPORT const char *GPR_CALLTYPE grpcsharp_version_string() { + return grpc_version_string(); +} + /* For testing */ GPR_EXPORT void GPR_CALLTYPE grpcsharp_test_callback(test_callback_funcptr callback) { diff --git a/src/node/binding.gyp b/src/node/binding.gyp index 6ba233388a..734dc8410b 100644 --- a/src/node/binding.gyp +++ b/src/node/binding.gyp @@ -11,7 +11,8 @@ '-pedantic', '-g', '-zdefs', - '-Werror' + '-Werror', + '-Wno-error=deprecated-declarations' ], 'ldflags': [ '-g' diff --git a/src/node/examples/perf_test.js b/src/node/examples/perf_test.js index 0f38725f72..214b9384d5 100644 --- a/src/node/examples/perf_test.js +++ b/src/node/examples/perf_test.js @@ -63,7 +63,7 @@ function runTest(iterations, callback) { var timeDiff = process.hrtime(startTime); intervals[i] = timeDiff[0] * 1000000 + timeDiff[1] / 1000; next(i+1); - }, {}, deadline); + }, {}, {deadline: deadline}); } } next(0); diff --git a/src/node/ext/call.cc b/src/node/ext/call.cc index fe585a0d4f..c5c8313385 100644 --- a/src/node/ext/call.cc +++ b/src/node/ext/call.cc @@ -510,10 +510,21 @@ NAN_METHOD(Call::New) { NanUtf8String method(args[1]); double deadline = args[2]->NumberValue(); grpc_channel *wrapped_channel = channel->GetWrappedChannel(); - grpc_call *wrapped_call = grpc_channel_create_call( - wrapped_channel, NULL, GRPC_PROPAGATE_DEFAULTS, - CompletionQueueAsyncWorker::GetQueue(), *method, channel->GetHost(), - MillisecondsToTimespec(deadline)); + grpc_call *wrapped_call; + if (args[3]->IsString()) { + NanUtf8String host_override(args[3]); + wrapped_call = grpc_channel_create_call( + wrapped_channel, NULL, GRPC_PROPAGATE_DEFAULTS, + CompletionQueueAsyncWorker::GetQueue(), *method, + *host_override, MillisecondsToTimespec(deadline)); + } else if (args[3]->IsUndefined() || args[3]->IsNull()) { + wrapped_call = grpc_channel_create_call( + wrapped_channel, NULL, GRPC_PROPAGATE_DEFAULTS, + CompletionQueueAsyncWorker::GetQueue(), *method, + NULL, MillisecondsToTimespec(deadline)); + } else { + return NanThrowTypeError("Call's fourth argument must be a string"); + } call = new Call(wrapped_call); args.This()->SetHiddenValue(NanNew("channel_"), channel_object); } diff --git a/src/node/ext/channel.cc b/src/node/ext/channel.cc index d02ed95672..457a58c057 100644 --- a/src/node/ext/channel.cc +++ b/src/node/ext/channel.cc @@ -59,14 +59,12 @@ using v8::Value; NanCallback *Channel::constructor; Persistent<FunctionTemplate> Channel::fun_tpl; -Channel::Channel(grpc_channel *channel, NanUtf8String *host) - : wrapped_channel(channel), host(host) {} +Channel::Channel(grpc_channel *channel) : wrapped_channel(channel) {} Channel::~Channel() { if (wrapped_channel != NULL) { grpc_channel_destroy(wrapped_channel); } - delete host; } void Channel::Init(Handle<Object> exports) { @@ -91,8 +89,6 @@ bool Channel::HasInstance(Handle<Value> val) { grpc_channel *Channel::GetWrappedChannel() { return this->wrapped_channel; } -char *Channel::GetHost() { return **this->host; } - NAN_METHOD(Channel::New) { NanScope(); @@ -103,8 +99,7 @@ NAN_METHOD(Channel::New) { } grpc_channel *wrapped_channel; // Owned by the Channel object - NanUtf8String *host = new NanUtf8String(args[0]); - NanUtf8String *host_override = NULL; + NanUtf8String host(args[0]); grpc_credentials *creds; if (!Credentials::HasInstance(args[1])) { return NanThrowTypeError( @@ -116,12 +111,9 @@ NAN_METHOD(Channel::New) { grpc_channel_args *channel_args_ptr; if (args[2]->IsUndefined()) { channel_args_ptr = NULL; - wrapped_channel = grpc_insecure_channel_create(**host, NULL); + wrapped_channel = grpc_insecure_channel_create(*host, NULL); } else if (args[2]->IsObject()) { Handle<Object> args_hash(args[2]->ToObject()->Clone()); - if (args_hash->HasOwnProperty(NanNew(GRPC_SSL_TARGET_NAME_OVERRIDE_ARG))) { - host_override = new NanUtf8String(args_hash->Get(NanNew(GRPC_SSL_TARGET_NAME_OVERRIDE_ARG))); - } Handle<Array> keys(args_hash->GetOwnPropertyNames()); grpc_channel_args channel_args; channel_args.num_args = keys->Length(); @@ -153,20 +145,15 @@ NAN_METHOD(Channel::New) { return NanThrowTypeError("Channel expects a string and an object"); } if (creds == NULL) { - wrapped_channel = grpc_insecure_channel_create(**host, channel_args_ptr); + wrapped_channel = grpc_insecure_channel_create(*host, channel_args_ptr); } else { wrapped_channel = - grpc_secure_channel_create(creds, **host, channel_args_ptr); + grpc_secure_channel_create(creds, *host, channel_args_ptr); } if (channel_args_ptr != NULL) { free(channel_args_ptr->args); } - Channel *channel; - if (host_override == NULL) { - channel = new Channel(wrapped_channel, host); - } else { - channel = new Channel(wrapped_channel, host_override); - } + Channel *channel = new Channel(wrapped_channel); channel->Wrap(args.This()); NanReturnValue(args.This()); } else { diff --git a/src/node/ext/channel.h b/src/node/ext/channel.h index 6725ebb03f..e2182cb45c 100644 --- a/src/node/ext/channel.h +++ b/src/node/ext/channel.h @@ -53,11 +53,8 @@ class Channel : public ::node::ObjectWrap { /* Returns the grpc_channel struct that this object wraps */ grpc_channel *GetWrappedChannel(); - /* Return the hostname that this channel connects to */ - char *GetHost(); - private: - explicit Channel(grpc_channel *channel, NanUtf8String *host); + explicit Channel(grpc_channel *channel); ~Channel(); // Prevent copying @@ -71,7 +68,6 @@ class Channel : public ::node::ObjectWrap { static v8::Persistent<v8::FunctionTemplate> fun_tpl; grpc_channel *wrapped_channel; - NanUtf8String *host; }; } // namespace node diff --git a/src/node/interop/interop_client.js b/src/node/interop/interop_client.js index 236b36616c..6d6f9a349e 100644 --- a/src/node/interop/interop_client.js +++ b/src/node/interop/interop_client.js @@ -67,11 +67,8 @@ function zeroBuffer(size) { * primarily for use with mocha */ function emptyUnary(client, done) { - var call = client.emptyCall({}, function(err, resp) { + client.emptyCall({}, function(err, resp) { assert.ifError(err); - }); - call.on('status', function(status) { - assert.strictEqual(status.code, grpc.status.OK); if (done) { done(); } @@ -92,13 +89,10 @@ function largeUnary(client, done) { body: zeroBuffer(271828) } }; - var call = client.unaryCall(arg, function(err, resp) { + client.unaryCall(arg, function(err, resp) { assert.ifError(err); assert.strictEqual(resp.payload.type, 'COMPRESSABLE'); assert.strictEqual(resp.payload.body.length, 314159); - }); - call.on('status', function(status) { - assert.strictEqual(status.code, grpc.status.OK); if (done) { done(); } @@ -115,9 +109,6 @@ function clientStreaming(client, done) { var call = client.streamingInputCall(function(err, resp) { assert.ifError(err); assert.strictEqual(resp.aggregated_payload_size, 74922); - }); - call.on('status', function(status) { - assert.strictEqual(status.code, grpc.status.OK); if (done) { done(); } @@ -268,7 +259,7 @@ function cancelAfterFirstResponse(client, done) { function timeoutOnSleepingServer(client, done) { var deadline = new Date(); deadline.setMilliseconds(deadline.getMilliseconds() + 1); - var call = client.fullDuplexCall(null, deadline); + var call = client.fullDuplexCall(null, {deadline: deadline}); call.write({ payload: {body: zeroBuffer(27182)} }); @@ -302,15 +293,14 @@ function authTest(expected_user, scope, client, done) { fill_username: true, fill_oauth_scope: true }; - var call = client.unaryCall(arg, function(err, resp) { + client.unaryCall(arg, function(err, resp) { assert.ifError(err); assert.strictEqual(resp.payload.type, 'COMPRESSABLE'); assert.strictEqual(resp.payload.body.length, 314159); assert.strictEqual(resp.username, expected_user); - assert.strictEqual(resp.oauth_scope, AUTH_SCOPE_RESPONSE); - }); - call.on('status', function(status) { - assert.strictEqual(status.code, grpc.status.OK); + if (scope) { + assert.strictEqual(resp.oauth_scope, AUTH_SCOPE_RESPONSE); + } if (done) { done(); } @@ -340,17 +330,14 @@ function oauth2Test(expected_user, scope, per_rpc, client, done) { }; var makeTestCall = function(error, client_metadata) { assert.ifError(error); - var call = client.unaryCall(arg, function(err, resp) { + client.unaryCall(arg, function(err, resp) { assert.ifError(err); assert.strictEqual(resp.username, expected_user); assert.strictEqual(resp.oauth_scope, AUTH_SCOPE_RESPONSE); - }); - call.on('status', function(status) { - assert.strictEqual(status.code, grpc.status.OK); if (done) { done(); } - }); + }, client_metadata); }; if (per_rpc) { updateMetadata('', {}, makeTestCall); @@ -358,7 +345,6 @@ function oauth2Test(expected_user, scope, per_rpc, client, done) { client.updateMetadata = updateMetadata; makeTestCall(null, {}); } - }); }); } @@ -409,6 +395,7 @@ function runTest(address, host_override, test_case, tls, test_ca, done) { creds = grpc.Credentials.createSsl(ca_data); if (host_override) { options['grpc.ssl_target_name_override'] = host_override; + options['grpc.default_authority'] = host_override; } } else { creds = grpc.Credentials.createInsecure(); diff --git a/src/node/src/client.js b/src/node/src/client.js index b2b4423707..5cde438572 100644 --- a/src/node/src/client.js +++ b/src/node/src/client.js @@ -208,6 +208,25 @@ ClientWritableStream.prototype.getPeer = getPeer; ClientDuplexStream.prototype.getPeer = getPeer; /** + * Get a call object built with the provided options. Keys for options are + * 'deadline', which takes a date or number, and 'host', which takes a string + * and overrides the hostname to connect to. + * @param {Object} options Options map. + */ +function getCall(channel, method, options) { + var deadline; + var host; + if (options) { + deadline = options.deadline; + host = options.host; + } + if (deadline === undefined) { + deadline = Infinity; + } + return new grpc.Call(channel, method, deadline, host); +} + +/** * Get a function that can make unary requests to the specified method. * @param {string} method The name of the method to request * @param {function(*):Buffer} serialize The serialization function for inputs @@ -226,17 +245,13 @@ function makeUnaryRequestFunction(method, serialize, deserialize) { * response is received * @param {array=} metadata Array of metadata key/value pairs to add to the * call - * @param {(number|Date)=} deadline The deadline for processing this request. - * Defaults to infinite future + * @param {Object=} options Options map * @return {EventEmitter} An event emitter for stream related events */ - function makeUnaryRequest(argument, callback, metadata, deadline) { + function makeUnaryRequest(argument, callback, metadata, options) { /* jshint validthis: true */ - if (deadline === undefined) { - deadline = Infinity; - } var emitter = new EventEmitter(); - var call = new grpc.Call(this.channel, method, deadline); + var call = getCall(this.channel, method, options); if (metadata === null || metadata === undefined) { metadata = {}; } @@ -300,16 +315,12 @@ function makeClientStreamRequestFunction(method, serialize, deserialize) { * response is received * @param {array=} metadata Array of metadata key/value pairs to add to the * call - * @param {(number|Date)=} deadline The deadline for processing this request. - * Defaults to infinite future + * @param {Object=} options Options map * @return {EventEmitter} An event emitter for stream related events */ - function makeClientStreamRequest(callback, metadata, deadline) { + function makeClientStreamRequest(callback, metadata, options) { /* jshint validthis: true */ - if (deadline === undefined) { - deadline = Infinity; - } - var call = new grpc.Call(this.channel, method, deadline); + var call = getCall(this.channel, method, options); if (metadata === null || metadata === undefined) { metadata = {}; } @@ -374,16 +385,12 @@ function makeServerStreamRequestFunction(method, serialize, deserialize) { * serialize * @param {array=} metadata Array of metadata key/value pairs to add to the * call - * @param {(number|Date)=} deadline The deadline for processing this request. - * Defaults to infinite future + * @param {Object} options Options map * @return {EventEmitter} An event emitter for stream related events */ - function makeServerStreamRequest(argument, metadata, deadline) { + function makeServerStreamRequest(argument, metadata, options) { /* jshint validthis: true */ - if (deadline === undefined) { - deadline = Infinity; - } - var call = new grpc.Call(this.channel, method, deadline); + var call = getCall(this.channel, method, options); if (metadata === null || metadata === undefined) { metadata = {}; } @@ -446,16 +453,12 @@ function makeBidiStreamRequestFunction(method, serialize, deserialize) { * @this {SurfaceClient} Client object. Must have a channel member. * @param {array=} metadata Array of metadata key/value pairs to add to the * call - * @param {(number|Date)=} deadline The deadline for processing this request. - * Defaults to infinite future + * @param {Options} options Options map * @return {EventEmitter} An event emitter for stream related events */ - function makeBidiStreamRequest(metadata, deadline) { + function makeBidiStreamRequest(metadata, options) { /* jshint validthis: true */ - if (deadline === undefined) { - deadline = Infinity; - } - var call = new grpc.Call(this.channel, method, deadline); + var call = getCall(this.channel, method, options); if (metadata === null || metadata === undefined) { metadata = {}; } @@ -523,7 +526,7 @@ var requester_makers = { * requestSerialize: function to serialize request objects * responseDeserialize: function to deserialize response objects * @param {Object} methods An object mapping method names to method attributes - * @param {string} serviceName The name of the service + * @param {string} serviceName The fully qualified name of the service * @return {function(string, Object)} New client constructor */ exports.makeClientConstructor = function(methods, serviceName) { @@ -548,8 +551,10 @@ exports.makeClientConstructor = function(methods, serviceName) { } options['grpc.primary_user_agent'] = 'grpc-node/' + version; this.channel = new grpc.Channel(address, credentials, options); - this.server_address = address.replace(/\/$/, ''); - this.auth_uri = this.server_address + '/' + serviceName; + // Remove the optional DNS scheme, trailing port, and trailing backslash + address = address.replace(/^(dns:\/{3})?([^:\/]+)(:\d+)?\/?$/, '$2'); + this.server_address = address; + this.auth_uri = 'https://' + this.server_address + '/' + serviceName; this.updateMetadata = updateMetadata; } @@ -587,7 +592,8 @@ exports.makeClientConstructor = function(methods, serviceName) { */ exports.makeProtobufClientConstructor = function(service) { var method_attrs = common.getProtobufServiceAttrs(service, service.name); - var Client = exports.makeClientConstructor(method_attrs); + var Client = exports.makeClientConstructor( + method_attrs, common.fullyQualifiedName(service)); Client.service = service; return Client; }; diff --git a/src/node/test/call_test.js b/src/node/test/call_test.js index 48d859a8ec..8d0f20b074 100644 --- a/src/node/test/call_test.js +++ b/src/node/test/call_test.js @@ -84,6 +84,11 @@ describe('call', function() { new grpc.Call(channel, 'method', 0); }); }); + it('should accept an optional fourth string parameter', function() { + assert.doesNotThrow(function() { + new grpc.Call(channel, 'method', new Date(), 'host_override'); + }); + }); it('should fail with a closed channel', function() { var local_channel = new grpc.Channel('hostname', insecureCreds); local_channel.close(); diff --git a/src/node/test/end_to_end_test.js b/src/node/test/end_to_end_test.js index ea41dfc28c..7574d98b8a 100644 --- a/src/node/test/end_to_end_test.js +++ b/src/node/test/end_to_end_test.js @@ -74,8 +74,6 @@ describe('end-to-end', function() { }); it('should start and end a request without error', function(complete) { var done = multiDone(complete, 2); - var deadline = new Date(); - deadline.setSeconds(deadline.getSeconds() + 3); var status_text = 'xyz'; var call = new grpc.Call(channel, 'dummy_method', @@ -126,8 +124,6 @@ describe('end-to-end', function() { }); it('should successfully send and receive metadata', function(complete) { var done = multiDone(complete, 2); - var deadline = new Date(); - deadline.setSeconds(deadline.getSeconds() + 3); var status_text = 'xyz'; var call = new grpc.Call(channel, 'dummy_method', @@ -184,8 +180,6 @@ describe('end-to-end', function() { var req_text = 'client_request'; var reply_text = 'server_response'; var done = multiDone(complete, 2); - var deadline = new Date(); - deadline.setSeconds(deadline.getSeconds() + 3); var status_text = 'success'; var call = new grpc.Call(channel, 'dummy_method', @@ -241,8 +235,6 @@ describe('end-to-end', function() { it('should send multiple messages', function(complete) { var done = multiDone(complete, 2); var requests = ['req1', 'req2']; - var deadline = new Date(); - deadline.setSeconds(deadline.getSeconds() + 3); var status_text = 'xyz'; var call = new grpc.Call(channel, 'dummy_method', diff --git a/src/objective-c/GRPCClient/GRPCCall+OAuth2.m b/src/objective-c/GRPCClient/GRPCCall+OAuth2.m index ed39d4b0f7..83b0de18e3 100644 --- a/src/objective-c/GRPCClient/GRPCCall+OAuth2.m +++ b/src/objective-c/GRPCClient/GRPCCall+OAuth2.m @@ -40,7 +40,7 @@ static NSString * const kChallengeHeader = @"www-authenticate"; @implementation GRPCCall (OAuth2) - (NSString *)oauth2AccessToken { - NSString *headerValue = self.requestMetadata[kAuthorizationHeader]; + NSString *headerValue = self.requestHeaders[kAuthorizationHeader]; if ([headerValue hasPrefix:kBearerPrefix]) { return [headerValue substringFromIndex:kBearerPrefix.length]; } else { @@ -50,14 +50,14 @@ static NSString * const kChallengeHeader = @"www-authenticate"; - (void)setOauth2AccessToken:(NSString *)token { if (token) { - self.requestMetadata[kAuthorizationHeader] = [kBearerPrefix stringByAppendingString:token]; + self.requestHeaders[kAuthorizationHeader] = [kBearerPrefix stringByAppendingString:token]; } else { - [self.requestMetadata removeObjectForKey:kAuthorizationHeader]; + [self.requestHeaders removeObjectForKey:kAuthorizationHeader]; } } - (NSString *)oauth2ChallengeHeader { - return self.responseMetadata[kChallengeHeader]; + return self.responseHeaders[kChallengeHeader]; } @end diff --git a/src/objective-c/GRPCClient/GRPCCall.h b/src/objective-c/GRPCClient/GRPCCall.h index 4a8b7fff48..4eda499b1a 100644 --- a/src/objective-c/GRPCClient/GRPCCall.h +++ b/src/objective-c/GRPCClient/GRPCCall.h @@ -48,8 +48,10 @@ #import <Foundation/Foundation.h> #import <RxLibrary/GRXWriter.h> -// Key used in |NSError|'s |userInfo| dictionary to store the response metadata sent by the server. -extern id const kGRPCStatusMetadataKey; +// Keys used in |NSError|'s |userInfo| dictionary to store the response headers and trailers sent by +// the server. +extern id const kGRPCHeadersKey; +extern id const kGRPCTrailersKey; // Represents a single gRPC remote call. @interface GRPCCall : GRXWriter @@ -57,43 +59,49 @@ extern id const kGRPCStatusMetadataKey; // These HTTP headers will be passed to the server as part of this call. Each HTTP header is a // name-value pair with string names and either string or binary values. // -// The passed dictionary has to use NSString keys, corresponding to the header names. The -// value associated to each can be a NSString object or a NSData object. E.g.: +// The passed dictionary has to use NSString keys, corresponding to the header names. The value +// associated to each can be a NSString object or a NSData object. E.g.: // -// call.requestMetadata = @{@"Authorization": @"Bearer ..."}; +// call.requestHeaders = @{@"authorization": @"Bearer ..."}; // -// call.requestMetadata[@"SomeBinaryHeader"] = someData; +// call.requestHeaders[@"my-header-bin"] = someData; // -// After the call is started, modifying this won't have any effect. +// After the call is started, trying to modify this property is an error. // // For convenience, the property is initialized to an empty NSMutableDictionary, and the setter // accepts (and copies) both mutable and immutable dictionaries. -- (NSMutableDictionary *)requestMetadata; // nonatomic -- (void)setRequestMetadata:(NSDictionary *)requestMetadata; // nonatomic, copy +- (NSMutableDictionary *)requestHeaders; // nonatomic +- (void)setRequestHeaders:(NSDictionary *)requestHeaders; // nonatomic, copy -// This dictionary is populated with the HTTP headers received from the server. When the RPC ends, -// the HTTP trailers received are added to the dictionary too. It has the same structure as the -// request metadata dictionary. +// This dictionary is populated with the HTTP headers received from the server. This happens before +// any response message is received from the server. It has the same structure as the request +// headers dictionary: Keys are NSString header names; names ending with the suffix "-bin" have a +// NSData value; the others have a NSString value. // -// The first time this object calls |writeValue| on the writeable passed to |startWithWriteable|, -// the |responseMetadata| dictionary already contains the response headers. When it calls -// |writesFinishedWithError|, the dictionary contains both the response headers and trailers. -@property(atomic, readonly) NSDictionary *responseMetadata; +// The value of this property is nil until all response headers are received, and will change before +// any of -writeValue: or -writesFinishedWithError: are sent to the writeable. +@property(atomic, readonly) NSDictionary *responseHeaders; + +// Same as responseHeaders, but populated with the HTTP trailers received from the server before the +// call finishes. +// +// The value of this property is nil until all response trailers are received, and will change +// before -writesFinishedWithError: is sent to the writeable. +@property(atomic, readonly) NSDictionary *responseTrailers; // The request writer has to write NSData objects into the provided Writeable. The server will -// receive each of those separately and in order. -// A gRPC call might not complete until the request writer finishes. On the other hand, the -// request finishing doesn't necessarily make the call to finish, as the server might continue -// sending messages to the response side of the call indefinitely (depending on the semantics of -// the specific remote method called). +// receive each of those separately and in order as distinct messages. +// A gRPC call might not complete until the request writer finishes. On the other hand, the request +// finishing doesn't necessarily make the call to finish, as the server might continue sending +// messages to the response side of the call indefinitely (depending on the semantics of the +// specific remote method called). // To finish a call right away, invoke cancel. - (instancetype)initWithHost:(NSString *)host path:(NSString *)path requestsWriter:(GRXWriter *)requestsWriter NS_DESIGNATED_INITIALIZER; -// Finishes the request side of this call, notifies the server that the RPC -// should be cancelled, and finishes the response side of the call with an error -// of code CANCELED. +// Finishes the request side of this call, notifies the server that the RPC should be cancelled, and +// finishes the response side of the call with an error of code CANCELED. - (void)cancel; // TODO(jcanizales): Let specify a deadline. As a category of GRXWriter? diff --git a/src/objective-c/GRPCClient/GRPCCall.m b/src/objective-c/GRPCClient/GRPCCall.m index 5f7d74bca8..ff5d1c5aaf 100644 --- a/src/objective-c/GRPCClient/GRPCCall.m +++ b/src/objective-c/GRPCClient/GRPCCall.m @@ -42,9 +42,13 @@ #import "private/NSDictionary+GRPC.h" #import "private/NSError+GRPC.h" -NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey"; +NSString * const kGRPCHeadersKey = @"io.grpc.HeadersKey"; +NSString * const kGRPCTrailersKey = @"io.grpc.TrailersKey"; @interface GRPCCall () <GRXWriteable> +// Make them read-write. +@property(atomic, strong) NSDictionary *responseHeaders; +@property(atomic, strong) NSDictionary *responseTrailers; @end // The following methods of a C gRPC call object aren't reentrant, and thus @@ -74,14 +78,22 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey"; // all. This wrapper over our actual writeable ensures thread-safety and // correct ordering. GRXConcurrentWriteable *_responseWriteable; + + // The network thread wants the requestWriter to resume (when the server is ready for more input), + // or to stop (on errors), concurrently with user threads that want to start it, pause it or stop + // it. Because a writer isn't thread-safe, we'll synchronize those operations on it. + // We don't use a dispatch queue for that purpose, because the writer can call writeValue: or + // writesFinishedWithError: on this GRPCCall as part of those operations. We want to be able to + // pause the writer immediately on writeValue:, so we need our locking to be recursive. GRXWriter *_requestWriter; // To create a retain cycle when a call is started, up until it finishes. See - // |startWithWriteable:| and |finishWithError:|. - GRPCCall *_self; + // |startWithWriteable:| and |finishWithError:|. This saves users from having to retain a + // reference to the call object if all they're interested in is the handler being executed when + // the response arrives. + GRPCCall *_retainSelf; - NSMutableDictionary *_requestMetadata; - NSMutableDictionary *_responseMetadata; + NSMutableDictionary *_requestHeaders; } @synthesize state = _state; @@ -112,35 +124,31 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey"; _requestWriter = requestWriter; - _requestMetadata = [NSMutableDictionary dictionary]; - _responseMetadata = [NSMutableDictionary dictionary]; + _requestHeaders = [NSMutableDictionary dictionary]; } return self; } #pragma mark Metadata -- (NSMutableDictionary *)requestMetadata { - return _requestMetadata; -} - -- (void)setRequestMetadata:(NSDictionary *)requestMetadata { - _requestMetadata = [NSMutableDictionary dictionaryWithDictionary:requestMetadata]; +- (NSMutableDictionary *)requestHeaders { + return _requestHeaders; } -- (NSDictionary *)responseMetadata { - return _responseMetadata; +- (void)setRequestHeaders:(NSDictionary *)requestHeaders { + _requestHeaders = [NSMutableDictionary dictionaryWithDictionary:requestHeaders]; } #pragma mark Finish - (void)finishWithError:(NSError *)errorOrNil { // If the call isn't retained anywhere else, it can be deallocated now. - _self = nil; + _retainSelf = nil; // If there were still request messages coming, stop them. - _requestWriter.state = GRXWriterStateFinished; - _requestWriter = nil; + @synchronized(_requestWriter) { + _requestWriter.state = GRXWriterStateFinished; + } if (errorOrNil) { [_responseWriteable cancelWithError:errorOrNil]; @@ -222,11 +230,10 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey"; #pragma mark Send headers -// TODO(jcanizales): Rename to commitHeaders. -- (void)sendHeaders:(NSDictionary *)metadata { +- (void)sendHeaders:(NSDictionary *)headers { // TODO(jcanizales): Add error handlers for async failures [_wrappedCall startBatchWithOperations:@[[[GRPCOpSendMetadata alloc] - initWithMetadata:metadata ?: @{} handler:nil]]]; + initWithMetadata:headers ?: @{} handler:nil]]]; } #pragma mark GRXWriteable implementation @@ -240,12 +247,14 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey"; // Resume the request writer. GRPCCall *strongSelf = weakSelf; if (strongSelf) { - strongSelf->_requestWriter.state = GRXWriterStateStarted; + @synchronized(strongSelf->_requestWriter) { + strongSelf->_requestWriter.state = GRXWriterStateStarted; + } } }; - [_wrappedCall startBatchWithOperations:@[[[GRPCOpSendMessage alloc] - initWithMessage:message - handler:resumingHandler]] errorHandler:errorHandler]; + [_wrappedCall startBatchWithOperations:@[[[GRPCOpSendMessage alloc] initWithMessage:message + handler:resumingHandler]] + errorHandler:errorHandler]; } - (void)writeValue:(id)value { @@ -253,7 +262,9 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey"; // Pause the input and only resume it when the C layer notifies us that writes // can proceed. - _requestWriter.state = GRXWriterStatePaused; + @synchronized(_requestWriter) { + _requestWriter.state = GRXWriterStatePaused; + } __weak GRPCCall *weakSelf = self; dispatch_async(_callQueue, ^{ @@ -273,7 +284,6 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey"; } - (void)writesFinishedWithError:(NSError *)errorOrNil { - _requestWriter = nil; if (errorOrNil) { [self cancel]; } else { @@ -292,42 +302,54 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey"; // Both handlers will eventually be called, from the network queue. Writes can start immediately // after this. -// The first one (metadataHandler), when the response headers are received. +// The first one (headersHandler), when the response headers are received. // The second one (completionHandler), whenever the RPC finishes for any reason. -- (void)invokeCallWithMetadataHandler:(void(^)(NSDictionary *))metadataHandler +- (void)invokeCallWithHeadersHandler:(void(^)(NSDictionary *))headersHandler completionHandler:(void(^)(NSError *, NSDictionary *))completionHandler { // TODO(jcanizales): Add error handlers for async failures [_wrappedCall startBatchWithOperations:@[[[GRPCOpRecvMetadata alloc] - initWithHandler:metadataHandler]]]; + initWithHandler:headersHandler]]]; [_wrappedCall startBatchWithOperations:@[[[GRPCOpRecvStatus alloc] initWithHandler:completionHandler]]]; } - (void)invokeCall { __weak GRPCCall *weakSelf = self; - [self invokeCallWithMetadataHandler:^(NSDictionary *headers) { + [self invokeCallWithHeadersHandler:^(NSDictionary *headers) { // Response headers received. GRPCCall *strongSelf = weakSelf; if (strongSelf) { - [strongSelf->_responseMetadata addEntriesFromDictionary:headers]; + strongSelf.responseHeaders = headers; [strongSelf startNextRead]; } } completionHandler:^(NSError *error, NSDictionary *trailers) { GRPCCall *strongSelf = weakSelf; if (strongSelf) { - [strongSelf->_responseMetadata addEntriesFromDictionary:trailers]; + strongSelf.responseTrailers = trailers; if (error) { - NSMutableDictionary *userInfo = - [NSMutableDictionary dictionaryWithDictionary:error.userInfo]; - userInfo[kGRPCStatusMetadataKey] = strongSelf->_responseMetadata; + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + if (error.userInfo) { + [userInfo addEntriesFromDictionary:error.userInfo]; + } + userInfo[kGRPCTrailersKey] = strongSelf.responseTrailers; + // TODO(jcanizales): The C gRPC library doesn't guarantee that the headers block will be + // called before this one, so an error might end up with trailers but no headers. We + // shouldn't call finishWithError until ater both blocks are called. It is also when this is + // done that we can provide a merged view of response headers and trailers in a thread-safe + // way. + if (strongSelf.responseHeaders) { + userInfo[kGRPCHeadersKey] = strongSelf.responseHeaders; + } error = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo]; } [strongSelf finishWithError:error]; } }]; // Now that the RPC has been initiated, request writes can start. - [_requestWriter startWithWriteable:self]; + @synchronized(_requestWriter) { + [_requestWriter startWithWriteable:self]; + } } #pragma mark GRXWriter implementation @@ -338,10 +360,10 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey"; // before being autoreleased). // Care is taken not to retain self strongly in any of the blocks used in this implementation, so // that the life of the instance is determined by this retain cycle. - _self = self; + _retainSelf = self; _responseWriteable = [[GRXConcurrentWriteable alloc] initWithWriteable:writeable]; - [self sendHeaders:_requestMetadata]; + [self sendHeaders:_requestHeaders]; [self invokeCall]; } diff --git a/src/objective-c/GRPCClient/private/GRPCSecureChannel.m b/src/objective-c/GRPCClient/private/GRPCSecureChannel.m index 9b4b6768f8..0a54804bb2 100644 --- a/src/objective-c/GRPCClient/private/GRPCSecureChannel.m +++ b/src/objective-c/GRPCClient/private/GRPCSecureChannel.m @@ -38,15 +38,18 @@ // Returns NULL if the file at path couldn't be read. In that case, if errorPtr isn't NULL, // *errorPtr will be an object describing what went wrong. static grpc_credentials *CertificatesAtPath(NSString *path, NSError **errorPtr) { - NSString *certsContent = [NSString stringWithContentsOfFile:path - encoding:NSASCIIStringEncoding + // Files in PEM format can have non-ASCII characters in their comments (e.g. for the name of the + // issuer). Load them as UTF8 and produce an ASCII equivalent. + NSString *contentInUTF8 = [NSString stringWithContentsOfFile:path + encoding:NSUTF8StringEncoding error:errorPtr]; - if (!certsContent) { + NSData *contentInASCII = [contentInUTF8 dataUsingEncoding:NSASCIIStringEncoding + allowLossyConversion:YES]; + if (!contentInASCII.bytes) { // Passing NULL to grpc_ssl_credentials_create produces behavior we don't want, so return. return NULL; } - const char * asCString = [certsContent cStringUsingEncoding:NSASCIIStringEncoding]; - return grpc_ssl_credentials_create(asCString, NULL); + return grpc_ssl_credentials_create(contentInASCII.bytes, NULL); } @implementation GRPCSecureChannel diff --git a/src/objective-c/RxLibrary/GRXBufferedPipe.h b/src/objective-c/RxLibrary/GRXBufferedPipe.h index b6296e1ed7..ca94ce275f 100644 --- a/src/objective-c/RxLibrary/GRXBufferedPipe.h +++ b/src/objective-c/RxLibrary/GRXBufferedPipe.h @@ -36,13 +36,11 @@ #import "GRXWriteable.h" #import "GRXWriter.h" -// A buffered pipe is a Writeable that also acts as a Writer (to whichever other writeable is passed -// to -startWithWriteable:). +// A buffered pipe is a Writer that also acts as a Writeable. // Once it is started, whatever values are written into it (via -writeValue:) will be propagated // immediately, unless flow control prevents it. // If it is throttled and keeps receiving values, as well as if it receives values before being -// started, it will buffer them and propagate them in order as soon as its state becomes -// GRXWriterStateStarted. +// started, it will buffer them and propagate them in order as soon as its state becomes Started. // If it receives an error (via -writesFinishedWithError:), it will drop any buffered values and // propagate the error immediately. // @@ -51,6 +49,9 @@ // pipe will keep buffering all data written to it, your application could run out of memory and // crash. If you want to react to flow control signals to prevent that, instead of using this class // you can implement an object that conforms to GRXWriter. +// +// Thread-safety: +// The methods of an object of this class should not be called concurrently from different threads. @interface GRXBufferedPipe : GRXWriter<GRXWriteable> // Convenience constructor. diff --git a/src/objective-c/RxLibrary/GRXForwardingWriter.h b/src/objective-c/RxLibrary/GRXForwardingWriter.h index d004333d2b..f310832284 100644 --- a/src/objective-c/RxLibrary/GRXForwardingWriter.h +++ b/src/objective-c/RxLibrary/GRXForwardingWriter.h @@ -33,11 +33,17 @@ #import "GRXWriter.h" -// A "proxy" class that simply forwards values, completion, and errors from its -// input writer to its writeable. +// A "proxy" class that simply forwards values, completion, and errors from its input writer to its +// writeable. // It is useful as a superclass for pipes that act as a transformation of their // input writer, and for classes that represent objects with input and // output sequences of values, like an RPC. +// +// Thread-safety: +// All messages sent to this object need to be serialized. When it is started, the writer it wraps +// is started in the same thread. Manual state changes are propagated to the wrapped writer in the +// same thread too. Importantly, all messages the wrapped writer sends to its writeable need to be +// serialized with any message sent to this object. @interface GRXForwardingWriter : GRXWriter - (instancetype)initWithWriter:(GRXWriter *)writer NS_DESIGNATED_INITIALIZER; @end diff --git a/src/objective-c/RxLibrary/GRXForwardingWriter.m b/src/objective-c/RxLibrary/GRXForwardingWriter.m index 2342f51ab3..a72be9ace2 100644 --- a/src/objective-c/RxLibrary/GRXForwardingWriter.m +++ b/src/objective-c/RxLibrary/GRXForwardingWriter.m @@ -48,7 +48,11 @@ // Designated initializer - (instancetype)initWithWriter:(GRXWriter *)writer { if (!writer) { - [NSException raise:NSInvalidArgumentException format:@"writer can't be nil."]; + return nil; + } + if (writer.state != GRXWriterStateNotStarted) { + [NSException raise:NSInvalidArgumentException + format:@"The writer argument must not have already started."]; } if ((self = [super init])) { _writer = writer; diff --git a/src/objective-c/RxLibrary/GRXImmediateWriter.h b/src/objective-c/RxLibrary/GRXImmediateWriter.h index b171f0c760..3fcc259434 100644 --- a/src/objective-c/RxLibrary/GRXImmediateWriter.h +++ b/src/objective-c/RxLibrary/GRXImmediateWriter.h @@ -36,10 +36,17 @@ #import "GRXWriter.h" // Utility to construct GRXWriter instances from values that are immediately available when -// required. The returned writers all support pausing and early termination. +// required. // -// Unless the writeable callback pauses them or stops them early, these writers will do all their -// interactions with the writeable before the start method returns. +// Thread-safety: +// +// An object of this class shouldn't be messaged concurrently by more than one thread. It will start +// messaging the writeable before |startWithWriteable:| returns, in the same thread. That is the +// only place where the writer can be paused or stopped prematurely. +// +// If a paused writer of this class is resumed, it will start messaging the writeable, in the same +// thread, before |setState:| returns. Because the object can't be legally accessed concurrently, +// that's the only place where it can be paused again (or stopped). @interface GRXImmediateWriter : GRXWriter // Returns a writer that pulls values from the passed NSEnumerator instance and pushes them to diff --git a/src/objective-c/RxLibrary/GRXWriteable.h b/src/objective-c/RxLibrary/GRXWriteable.h index 216de30735..45613d6dd0 100644 --- a/src/objective-c/RxLibrary/GRXWriteable.h +++ b/src/objective-c/RxLibrary/GRXWriteable.h @@ -48,15 +48,15 @@ typedef void (^GRXValueHandler)(id value); typedef void (^GRXCompletionHandler)(NSError *errorOrNil); -typedef void (^GRXSingleValueHandler)(id value, NSError *errorOrNil); -typedef void (^GRXStreamHandler)(BOOL done, id value, NSError *error); +typedef void (^GRXSingleHandler)(id value, NSError *errorOrNil); +typedef void (^GRXEventHandler)(BOOL done, id value, NSError *error); // Utility to create objects that conform to the GRXWriteable protocol, from // blocks that handle each of the two methods of the protocol. @interface GRXWriteable : NSObject<GRXWriteable> -+ (instancetype)writeableWithSingleValueHandler:(GRXSingleValueHandler)handler; -+ (instancetype)writeableWithStreamHandler:(GRXStreamHandler)handler; ++ (instancetype)writeableWithSingleHandler:(GRXSingleHandler)handler; ++ (instancetype)writeableWithEventHandler:(GRXEventHandler)handler; - (instancetype)initWithValueHandler:(GRXValueHandler)valueHandler completionHandler:(GRXCompletionHandler)completionHandler diff --git a/src/objective-c/RxLibrary/GRXWriteable.m b/src/objective-c/RxLibrary/GRXWriteable.m index 63f7c3e7f3..2729d62b72 100644 --- a/src/objective-c/RxLibrary/GRXWriteable.m +++ b/src/objective-c/RxLibrary/GRXWriteable.m @@ -38,7 +38,7 @@ GRXCompletionHandler _completionHandler; } -+ (instancetype)writeableWithSingleValueHandler:(GRXSingleValueHandler)handler { ++ (instancetype)writeableWithSingleHandler:(GRXSingleHandler)handler { if (!handler) { return [[self alloc] init]; } @@ -51,7 +51,7 @@ }]; } -+ (instancetype)writeableWithStreamHandler:(GRXStreamHandler)handler { ++ (instancetype)writeableWithEventHandler:(GRXEventHandler)handler { if (!handler) { return [[self alloc] init]; } diff --git a/src/objective-c/RxLibrary/GRXWriter.h b/src/objective-c/RxLibrary/GRXWriter.h index 5d6e1a472a..b1c994aa38 100644 --- a/src/objective-c/RxLibrary/GRXWriter.h +++ b/src/objective-c/RxLibrary/GRXWriter.h @@ -35,84 +35,73 @@ #import "GRXWriteable.h" +// States of a writer. typedef NS_ENUM(NSInteger, GRXWriterState) { - // The writer has not yet been given a writeable to which it can push its - // values. To have an writer transition to the Started state, send it a - // startWithWriteable: message. + // The writer has not yet been given a writeable to which it can push its values. To have a writer + // transition to the Started state, send it a startWithWriteable: message. // - // An writer's state cannot be manually set to this value. + // A writer's state cannot be manually set to this value. GRXWriterStateNotStarted, // The writer might push values to the writeable at any moment. GRXWriterStateStarted, - // The writer is temporarily paused, and won't send any more values to the - // writeable unless its state is set back to Started. The writer might still - // transition to the Finished state at any moment, and is allowed to send - // writesFinishedWithError: to its writeable. - // - // Not all implementations of writer have to support pausing, and thus - // trying to set an writer's state to this value might have no effect. + // The writer is temporarily paused, and won't send any more values to the writeable unless its + // state is set back to Started. The writer might still transition to the Finished state at any + // moment, and is allowed to send writesFinishedWithError: to its writeable. GRXWriterStatePaused, // The writer has released its writeable and won't interact with it anymore. // - // One seldomly wants to set an writer's state to this value, as its - // writeable isn't notified with a writesFinishedWithError: message. Instead, sending - // finishWithError: to the writer will make it notify the writeable and then - // transition to this state. + // One seldomly wants to set a writer's state to this value, as its writeable isn't notified with + // a writesFinishedWithError: message. Instead, sending finishWithError: to the writer will make + // it notify the writeable and then transition to this state. GRXWriterStateFinished }; -// An object that conforms to this protocol can produce, on demand, a sequence -// of values. The sequence may be produced asynchronously, and it may consist of -// any number of elements, including none or an infinite number. +// An GRXWriter object can produce, on demand, a sequence of values. The sequence may be produced +// asynchronously, and it may consist of any number of elements, including none or an infinite +// number. +// +// GRXWriter is the active dual of NSEnumerator. The difference between them is thus whether the +// object plays an active or passive role during usage: A user of NSEnumerator pulls values off it, +// and passes the values to a writeable. A user of GRXWriter, though, just gives it a writeable, and +// the GRXWriter instance pushes values to the writeable. This makes this protocol suitable to +// represent a sequence of future values, as well as collections with internal iteration. // -// GRXWriter is the active dual of NSEnumerator. The difference between them -// is thus whether the object plays an active or passive role during usage: A -// user of NSEnumerator pulls values off it, and passes the values to a writeable. -// A user of GRXWriter, though, just gives it a writeable, and the -// GRXWriter instance pushes values to the writeable. This makes this protocol -// suitable to represent a sequence of future values, as well as collections -// with internal iteration. +// An instance of GRXWriter can start producing values after a writeable is passed to it. It can +// also be commanded to finish the sequence immediately (with an optional error). Finally, it can be +// asked to pause, and resumed later. All GRXWriter objects support pausing and early termination. // -// An instance of GRXWriter can start producing values after a writeable is -// passed to it. It can also be commanded to finish the sequence immediately -// (with an optional error). Finally, it can be asked to pause, but the -// conforming instance is not required to oblige. +// Thread-safety: // -// Unless otherwise indicated by a conforming class, no messages should be sent -// concurrently to a GRXWriter. I.e., conforming classes aren't required to -// be thread-safe. +// State transitions take immediate effect if the object is used from a single thread. Subclasses +// might offer stronger guarantees. +// +// Unless otherwise indicated by a conforming subclass, no messages should be sent concurrently to a +// GRXWriter. I.e., conforming classes aren't required to be thread-safe. @interface GRXWriter : NSObject -// This property can be used to query the current state of the writer, which -// determines how it might currently use its writeable. Some state transitions can -// be triggered by setting this property to the corresponding value, and that's -// useful for advanced use cases like pausing an writer. For more details, -// see the documentation of the enum. +// This property can be used to query the current state of the writer, which determines how it might +// currently use its writeable. Some state transitions can be triggered by setting this property to +// the corresponding value, and that's useful for advanced use cases like pausing an writer. For +// more details, see the documentation of the enum further down. @property(nonatomic) GRXWriterState state; -// Start sending messages to the writeable. Messages may be sent before the method -// returns, or they may be sent later in the future. See GRXWriteable.h for the -// different messages a writeable can receive. +// Transition to the Started state, and start sending messages to the writeable (a reference to it +// is retained). Messages to the writeable may be sent before the method returns, or they may be +// sent later in the future. See GRXWriteable.h for the different messages a writeable can receive. // -// If this writer draws its values from an external source (e.g. from the -// filesystem or from a server), calling this method will commonly trigger side -// effects (like network connections). +// If this writer draws its values from an external source (e.g. from the filesystem or from a +// server), calling this method will commonly trigger side effects (like network connections). // // This method might only be called on writers in the NotStarted state. - (void)startWithWriteable:(id<GRXWriteable>)writeable; -// Send writesFinishedWithError:errorOrNil immediately to the writeable, and don't send -// any more messages to it. -// -// This method might only be called on writers in the Started or Paused -// state. +// Send writesFinishedWithError:errorOrNil to the writeable. Then release the reference to it and +// transition to the Finished state. // -// TODO(jcanizales): Consider adding some guarantee about the immediacy of that -// stopping. I know I've relied on it in part of the code that uses this, but -// can't remember the details in the presence of concurrency. +// This method might only be called on writers in the Started or Paused state. - (void)finishWithError:(NSError *)errorOrNil; @end diff --git a/src/objective-c/tests/GRPCClientTests.m b/src/objective-c/tests/GRPCClientTests.m index e85dd6e65c..06581e7599 100644 --- a/src/objective-c/tests/GRPCClientTests.m +++ b/src/objective-c/tests/GRPCClientTests.m @@ -114,7 +114,7 @@ static ProtoMethod *kUnaryCallMethod; [call startWithWriteable:responsesWriteable]; - [self waitForExpectationsWithTimeout:4 handler:nil]; + [self waitForExpectationsWithTimeout:8 handler:nil]; } - (void)testSimpleProtoRPC { @@ -146,7 +146,7 @@ static ProtoMethod *kUnaryCallMethod; [call startWithWriteable:responsesWriteable]; - [self waitForExpectationsWithTimeout:4 handler:nil]; + [self waitForExpectationsWithTimeout:8 handler:nil]; } - (void)testMetadata { @@ -168,11 +168,13 @@ static ProtoMethod *kUnaryCallMethod; } completionHandler:^(NSError *errorOrNil) { XCTAssertNotNil(errorOrNil, @"Finished without error!"); XCTAssertEqual(errorOrNil.code, 16, @"Finished with unexpected error: %@", errorOrNil); - XCTAssertEqualObjects(call.responseMetadata, errorOrNil.userInfo[kGRPCStatusMetadataKey], - @"Metadata in the NSError object and call object differ."); + XCTAssertEqualObjects(call.responseHeaders, errorOrNil.userInfo[kGRPCHeadersKey], + @"Headers in the NSError object and call object differ."); + XCTAssertEqualObjects(call.responseTrailers, errorOrNil.userInfo[kGRPCTrailersKey], + @"Trailers in the NSError object and call object differ."); NSString *challengeHeader = call.oauth2ChallengeHeader; XCTAssertGreaterThan(challengeHeader.length, 0, - @"No challenge in response headers %@", call.responseMetadata); + @"No challenge in response headers %@", call.responseHeaders); [expectation fulfill]; }]; diff --git a/src/objective-c/tests/InteropTests.h b/src/objective-c/tests/InteropTests.h index 4eb97e9e06..1045c3d124 100644 --- a/src/objective-c/tests/InteropTests.h +++ b/src/objective-c/tests/InteropTests.h @@ -37,8 +37,7 @@ // https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md @interface InteropTests : XCTestCase -// Returns @"localhost:5050". +// Returns @"grpc-test.sandbox.google.com". // Override in a subclass to perform the same tests against a different address. -// For interop tests, use @"grpc-test.sandbox.google.com". + (NSString *)host; @end diff --git a/src/objective-c/tests/InteropTests.m b/src/objective-c/tests/InteropTests.m index b61d567464..1b63fe2059 100644 --- a/src/objective-c/tests/InteropTests.m +++ b/src/objective-c/tests/InteropTests.m @@ -78,20 +78,17 @@ #pragma mark Tests -static NSString * const kLocalCleartextHost = @"localhost:5050"; +static NSString * const kRemoteSSLHost = @"grpc-test.sandbox.google.com"; @implementation InteropTests { RMTTestService *_service; } + (NSString *)host { - return kLocalCleartextHost; + return kRemoteSSLHost; } - (void)setUp { - // Register test server as non-SSL. - [GRPCCall useInsecureConnectionsForHost:kLocalCleartextHost]; - _service = [[RMTTestService alloc] initWithHost:self.class.host]; } @@ -131,7 +128,7 @@ static NSString * const kLocalCleartextHost = @"localhost:5050"; [expectation fulfill]; }]; - [self waitForExpectationsWithTimeout:8 handler:nil]; + [self waitForExpectationsWithTimeout:16 handler:nil]; } - (void)testClientStreamingRPC { diff --git a/src/objective-c/tests/InteropTestsLocalCleartext.m b/src/objective-c/tests/InteropTestsLocalCleartext.m new file mode 100644 index 0000000000..2d7d3c4b2c --- /dev/null +++ b/src/objective-c/tests/InteropTestsLocalCleartext.m @@ -0,0 +1,59 @@ +/* + * + * Copyright 2015, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +// Repeat of the tests in InteropTests.m, but sending the RPCs to a local cleartext server instead +// of the remote SSL one. + +#import <GRPCClient/GRPCCall+Tests.h> + +#import "InteropTests.h" + +static NSString * const kLocalCleartextHost = @"localhost:5050"; + +@interface InteropTestsLocalCleartext : InteropTests +@end + +@implementation InteropTestsLocalCleartext + ++ (NSString *)host { + return kLocalCleartextHost; +} + +- (void)setUp { + // Register test server as non-SSL. + [GRPCCall useInsecureConnectionsForHost:kLocalCleartextHost]; + + [super setUp]; +} + +@end diff --git a/src/objective-c/tests/InteropTestsLocalSSL.m b/src/objective-c/tests/InteropTestsLocalSSL.m index 227ca79659..f69f806dcf 100644 --- a/src/objective-c/tests/InteropTestsLocalSSL.m +++ b/src/objective-c/tests/InteropTestsLocalSSL.m @@ -31,8 +31,8 @@ * */ -// Repeat of the tests in InteropTests.m, but using SSL to communicate with the local server instead -// of cleartext. +// Repeat of the tests in InteropTests.m, but sending the RPCs to a local SSL server instead of the +// remote one. #import <GRPCClient/GRPCCall+Tests.h> diff --git a/src/objective-c/tests/RxLibraryUnitTests.m b/src/objective-c/tests/RxLibraryUnitTests.m index 5e3162875a..a67a4c6cd9 100644 --- a/src/objective-c/tests/RxLibraryUnitTests.m +++ b/src/objective-c/tests/RxLibraryUnitTests.m @@ -55,7 +55,7 @@ return [[self alloc] init]; } -- (GRXSingleValueHandler)block { +- (GRXSingleHandler)block { return ^(id value, NSError *errorOrNil) { ++_timesCalled; _value = value; @@ -71,13 +71,13 @@ #pragma mark Writeable -- (void)testWriteableSingleValueHandlerIsCalledForValue { +- (void)testWriteableSingleHandlerIsCalledForValue { // Given: CapturingSingleValueHandler *handler = [CapturingSingleValueHandler handler]; id anyValue = @7; // If: - id<GRXWriteable> writeable = [GRXWriteable writeableWithSingleValueHandler:handler.block]; + id<GRXWriteable> writeable = [GRXWriteable writeableWithSingleHandler:handler.block]; [writeable writeValue:anyValue]; // Then: @@ -86,13 +86,13 @@ XCTAssertEqualObjects(handler.errorOrNil, nil); } -- (void)testWriteableSingleValueHandlerIsCalledForError { +- (void)testWriteableSingleHandlerIsCalledForError { // Given: CapturingSingleValueHandler *handler = [CapturingSingleValueHandler handler]; NSError *anyError = [NSError errorWithDomain:@"domain" code:7 userInfo:nil]; // If: - id<GRXWriteable> writeable = [GRXWriteable writeableWithSingleValueHandler:handler.block]; + id<GRXWriteable> writeable = [GRXWriteable writeableWithSingleHandler:handler.block]; [writeable writesFinishedWithError:anyError]; // Then: @@ -106,7 +106,7 @@ - (void)testBufferedPipePropagatesValue { // Given: CapturingSingleValueHandler *handler = [CapturingSingleValueHandler handler]; - id<GRXWriteable> writeable = [GRXWriteable writeableWithSingleValueHandler:handler.block]; + id<GRXWriteable> writeable = [GRXWriteable writeableWithSingleHandler:handler.block]; id anyValue = @7; // If: @@ -123,7 +123,7 @@ - (void)testBufferedPipePropagatesError { // Given: CapturingSingleValueHandler *handler = [CapturingSingleValueHandler handler]; - id<GRXWriteable> writeable = [GRXWriteable writeableWithSingleValueHandler:handler.block]; + id<GRXWriteable> writeable = [GRXWriteable writeableWithSingleHandler:handler.block]; NSError *anyError = [NSError errorWithDomain:@"domain" code:7 userInfo:nil]; // If: diff --git a/src/objective-c/tests/Tests.xcodeproj/project.pbxproj b/src/objective-c/tests/Tests.xcodeproj/project.pbxproj index af98aba9c0..3a1c3d940a 100644 --- a/src/objective-c/tests/Tests.xcodeproj/project.pbxproj +++ b/src/objective-c/tests/Tests.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 63423F511B151B77006CF63C /* RxLibraryUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 63423F501B151B77006CF63C /* RxLibraryUnitTests.m */; }; 635697CD1B14FC11007A7283 /* Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 635697CC1B14FC11007A7283 /* Tests.m */; }; 635ED2EC1B1A3BC400FDE5C3 /* InteropTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 635ED2EB1B1A3BC400FDE5C3 /* InteropTests.m */; }; + 63715F561B780C020029CB0B /* InteropTestsLocalCleartext.m in Sources */ = {isa = PBXBuildFile; fileRef = 63715F551B780C020029CB0B /* InteropTestsLocalCleartext.m */; }; 63E240CE1B6C4E2B005F3B0E /* InteropTestsLocalSSL.m in Sources */ = {isa = PBXBuildFile; fileRef = 63E240CD1B6C4E2B005F3B0E /* InteropTestsLocalSSL.m */; }; 63E240D01B6C63DC005F3B0E /* TestCertificates.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 63E240CF1B6C63DC005F3B0E /* TestCertificates.bundle */; }; 7D8A186224D39101F90230F6 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 35F2B6BF3BAE8F0DC4AFD76E /* libPods.a */; }; @@ -51,6 +52,7 @@ 635697CC1B14FC11007A7283 /* Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Tests.m; sourceTree = "<group>"; }; 635697D81B14FC11007A7283 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 635ED2EB1B1A3BC400FDE5C3 /* InteropTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InteropTests.m; sourceTree = "<group>"; }; + 63715F551B780C020029CB0B /* InteropTestsLocalCleartext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InteropTestsLocalCleartext.m; sourceTree = "<group>"; }; 63E240CC1B6C4D3A005F3B0E /* InteropTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InteropTests.h; sourceTree = "<group>"; }; 63E240CD1B6C4E2B005F3B0E /* InteropTestsLocalSSL.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InteropTestsLocalSSL.m; sourceTree = "<group>"; }; 63E240CF1B6C63DC005F3B0E /* TestCertificates.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = TestCertificates.bundle; sourceTree = "<group>"; }; @@ -117,14 +119,15 @@ 635697C91B14FC11007A7283 /* Tests */ = { isa = PBXGroup; children = ( - 63E240CD1B6C4E2B005F3B0E /* InteropTestsLocalSSL.m */, 6312AE4D1B1BF49B00341DEE /* GRPCClientTests.m */, - 63175DFE1B1B9FAF00027841 /* LocalClearTextTests.m */, + 63E240CC1B6C4D3A005F3B0E /* InteropTests.h */, 635ED2EB1B1A3BC400FDE5C3 /* InteropTests.m */, + 63E240CD1B6C4E2B005F3B0E /* InteropTestsLocalSSL.m */, + 63715F551B780C020029CB0B /* InteropTestsLocalCleartext.m */, 63423F501B151B77006CF63C /* RxLibraryUnitTests.m */, + 63175DFE1B1B9FAF00027841 /* LocalClearTextTests.m */, 635697CC1B14FC11007A7283 /* Tests.m */, 635697D71B14FC11007A7283 /* Supporting Files */, - 63E240CC1B6C4D3A005F3B0E /* InteropTests.h */, ); name = Tests; sourceTree = SOURCE_ROOT; @@ -261,6 +264,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 63715F561B780C020029CB0B /* InteropTestsLocalCleartext.m in Sources */, 63175DFF1B1B9FAF00027841 /* LocalClearTextTests.m in Sources */, 63423F511B151B77006CF63C /* RxLibraryUnitTests.m in Sources */, 63E240CE1B6C4E2B005F3B0E /* InteropTestsLocalSSL.m in Sources */, diff --git a/src/python/grpcio/grpc/_adapter/_c/module.c b/src/python/grpcio/grpc/_adapter/_c/module.c index 1f3aedd9d8..9b93b051f6 100644 --- a/src/python/grpcio/grpc/_adapter/_c/module.c +++ b/src/python/grpcio/grpc/_adapter/_c/module.c @@ -53,6 +53,12 @@ PyMODINIT_FUNC init_c(void) { return; } + if (PyModule_AddStringConstant( + module, "PRIMARY_USER_AGENT_KEY", + GRPC_ARG_PRIMARY_USER_AGENT_STRING) < 0) { + return; + } + /* GRPC maintains an internal counter of how many times it has been initialized and handles multiple pairs of grpc_init()/grpc_shutdown() invocations accordingly. */ diff --git a/src/python/grpcio/grpc/_adapter/_c/types.h b/src/python/grpcio/grpc/_adapter/_c/types.h index 4e0da4a28a..f646465c63 100644 --- a/src/python/grpcio/grpc/_adapter/_c/types.h +++ b/src/python/grpcio/grpc/_adapter/_c/types.h @@ -113,6 +113,7 @@ Call *pygrpc_Call_new_empty(CompletionQueue *cq); void pygrpc_Call_dealloc(Call *self); PyObject *pygrpc_Call_start_batch(Call *self, PyObject *args, PyObject *kwargs); PyObject *pygrpc_Call_cancel(Call *self, PyObject *args, PyObject *kwargs); +PyObject *pygrpc_Call_peer(Call *self); extern PyTypeObject pygrpc_Call_type; @@ -129,6 +130,11 @@ Channel *pygrpc_Channel_new( void pygrpc_Channel_dealloc(Channel *self); Call *pygrpc_Channel_create_call( Channel *self, PyObject *args, PyObject *kwargs); +PyObject *pygrpc_Channel_check_connectivity_state(Channel *self, PyObject *args, + PyObject *kwargs); +PyObject *pygrpc_Channel_watch_connectivity_state(Channel *self, PyObject *args, + PyObject *kwargs); +PyObject *pygrpc_Channel_target(Channel *self); extern PyTypeObject pygrpc_Channel_type; @@ -181,6 +187,9 @@ pygrpc_tag *pygrpc_produce_request_tag(PyObject *user_tag, Call *empty_call); /* Construct a tag associated with a server shutdown. */ pygrpc_tag *pygrpc_produce_server_shutdown_tag(PyObject *user_tag); +/* Construct a tag associated with a channel state change. */ +pygrpc_tag *pygrpc_produce_channel_state_change_tag(PyObject *user_tag); + /* Frees all resources owned by the tag and the tag itself. */ void pygrpc_discard_tag(pygrpc_tag *tag); diff --git a/src/python/grpcio/grpc/_adapter/_c/types/call.c b/src/python/grpcio/grpc/_adapter/_c/types/call.c index 0739070044..5e46605c45 100644 --- a/src/python/grpcio/grpc/_adapter/_c/types/call.c +++ b/src/python/grpcio/grpc/_adapter/_c/types/call.c @@ -42,6 +42,7 @@ PyMethodDef pygrpc_Call_methods[] = { {"start_batch", (PyCFunction)pygrpc_Call_start_batch, METH_KEYWORDS, ""}, {"cancel", (PyCFunction)pygrpc_Call_cancel, METH_KEYWORDS, ""}, + {"peer", (PyCFunction)pygrpc_Call_peer, METH_NOARGS, ""}, {NULL} }; const char pygrpc_Call_doc[] = "See grpc._adapter._types.Call."; @@ -161,3 +162,10 @@ PyObject *pygrpc_Call_cancel(Call *self, PyObject *args, PyObject *kwargs) { } return PyInt_FromLong(errcode); } + +PyObject *pygrpc_Call_peer(Call *self) { + char *peer = grpc_call_get_peer(self->c_call); + PyObject *py_peer = PyString_FromString(peer); + gpr_free(peer); + return py_peer; +} diff --git a/src/python/grpcio/grpc/_adapter/_c/types/channel.c b/src/python/grpcio/grpc/_adapter/_c/types/channel.c index 963104742f..eb9d43d154 100644 --- a/src/python/grpcio/grpc/_adapter/_c/types/channel.c +++ b/src/python/grpcio/grpc/_adapter/_c/types/channel.c @@ -36,10 +36,14 @@ #define PY_SSIZE_T_CLEAN #include <Python.h> #include <grpc/grpc.h> +#include <grpc/support/alloc.h> PyMethodDef pygrpc_Channel_methods[] = { {"create_call", (PyCFunction)pygrpc_Channel_create_call, METH_KEYWORDS, ""}, + {"check_connectivity_state", (PyCFunction)pygrpc_Channel_check_connectivity_state, METH_KEYWORDS, ""}, + {"watch_connectivity_state", (PyCFunction)pygrpc_Channel_watch_connectivity_state, METH_KEYWORDS, ""}, + {"target", (PyCFunction)pygrpc_Channel_target, METH_NOARGS, ""}, {NULL} }; const char pygrpc_Channel_doc[] = "See grpc._adapter._types.Channel."; @@ -122,7 +126,7 @@ Call *pygrpc_Channel_create_call( const char *host; double deadline; char *keywords[] = {"cq", "method", "host", "deadline", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!ssd:create_call", keywords, + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!szd:create_call", keywords, &pygrpc_CompletionQueue_type, &cq, &method, &host, &deadline)) { return NULL; } @@ -132,3 +136,51 @@ Call *pygrpc_Channel_create_call( pygrpc_cast_double_to_gpr_timespec(deadline)); return call; } + +PyObject *pygrpc_Channel_check_connectivity_state( + Channel *self, PyObject *args, PyObject *kwargs) { + PyObject *py_try_to_connect; + int try_to_connect; + char *keywords[] = {"try_to_connect", NULL}; + grpc_connectivity_state state; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O:connectivity_state", keywords, + &py_try_to_connect)) { + return NULL; + } + if (!PyBool_Check(py_try_to_connect)) { + Py_XDECREF(py_try_to_connect); + return NULL; + } + try_to_connect = Py_True == py_try_to_connect; + Py_DECREF(py_try_to_connect); + state = grpc_channel_check_connectivity_state(self->c_chan, try_to_connect); + return PyInt_FromLong(state); +} + +PyObject *pygrpc_Channel_watch_connectivity_state( + Channel *self, PyObject *args, PyObject *kwargs) { + PyObject *tag; + double deadline; + int last_observed_state; + CompletionQueue *completion_queue; + char *keywords[] = {"last_observed_state", "deadline", + "completion_queue", "tag"}; + if (!PyArg_ParseTupleAndKeywords( + args, kwargs, "idO!O:watch_connectivity_state", keywords, + &last_observed_state, &deadline, &pygrpc_CompletionQueue_type, + &completion_queue, &tag)) { + return NULL; + } + grpc_channel_watch_connectivity_state( + self->c_chan, (grpc_connectivity_state)last_observed_state, + pygrpc_cast_double_to_gpr_timespec(deadline), completion_queue->c_cq, + pygrpc_produce_channel_state_change_tag(tag)); + Py_RETURN_NONE; +} + +PyObject *pygrpc_Channel_target(Channel *self) { + char *target = grpc_channel_get_target(self->c_chan); + PyObject *py_target = PyString_FromString(target); + gpr_free(target); + return py_target; +} diff --git a/src/python/grpcio/grpc/_adapter/_c/types/server.c b/src/python/grpcio/grpc/_adapter/_c/types/server.c index c2190ea672..2a11d09d21 100644 --- a/src/python/grpcio/grpc/_adapter/_c/types/server.c +++ b/src/python/grpcio/grpc/_adapter/_c/types/server.c @@ -96,7 +96,7 @@ Server *pygrpc_Server_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) PyObject *py_args; grpc_channel_args c_args; char *keywords[] = {"cq", "args", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!O:Channel", keywords, + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!O:Server", keywords, &pygrpc_CompletionQueue_type, &cq, &py_args)) { return NULL; } diff --git a/src/python/grpcio/grpc/_adapter/_c/utility.c b/src/python/grpcio/grpc/_adapter/_c/utility.c index 51f3c9be01..2eea0e18ef 100644 --- a/src/python/grpcio/grpc/_adapter/_c/utility.c +++ b/src/python/grpcio/grpc/_adapter/_c/utility.c @@ -88,6 +88,19 @@ pygrpc_tag *pygrpc_produce_server_shutdown_tag(PyObject *user_tag) { return tag; } +pygrpc_tag *pygrpc_produce_channel_state_change_tag(PyObject *user_tag) { + pygrpc_tag *tag = gpr_malloc(sizeof(pygrpc_tag)); + tag->user_tag = user_tag; + Py_XINCREF(tag->user_tag); + tag->call = NULL; + tag->ops = NULL; + tag->nops = 0; + grpc_call_details_init(&tag->request_call_details); + grpc_metadata_array_init(&tag->request_metadata); + tag->is_new_call = 0; + return tag; +} + void pygrpc_discard_tag(pygrpc_tag *tag) { if (!tag) { return; @@ -139,7 +152,7 @@ PyObject *pygrpc_consume_event(grpc_event event) { } int pygrpc_produce_op(PyObject *op, grpc_op *result) { - static const int OP_TUPLE_SIZE = 5; + static const int OP_TUPLE_SIZE = 6; static const int STATUS_TUPLE_SIZE = 2; static const int TYPE_INDEX = 0; static const int INITIAL_METADATA_INDEX = 1; @@ -148,6 +161,7 @@ int pygrpc_produce_op(PyObject *op, grpc_op *result) { static const int STATUS_INDEX = 4; static const int STATUS_CODE_INDEX = 0; static const int STATUS_DETAILS_INDEX = 1; + static const int WRITE_FLAGS_INDEX = 5; int type; Py_ssize_t message_size; char *message; @@ -170,7 +184,10 @@ int pygrpc_produce_op(PyObject *op, grpc_op *result) { return 0; } c_op.op = type; - c_op.flags = 0; + c_op.flags = PyInt_AsLong(PyTuple_GET_ITEM(op, WRITE_FLAGS_INDEX)); + if (PyErr_Occurred()) { + return 0; + } switch (type) { case GRPC_OP_SEND_INITIAL_METADATA: if (!pygrpc_cast_pyseq_to_send_metadata( diff --git a/src/python/grpcio/grpc/_adapter/_intermediary_low.py b/src/python/grpcio/grpc/_adapter/_intermediary_low.py index 3c7f0a2619..e7bf9dc462 100644 --- a/src/python/grpcio/grpc/_adapter/_intermediary_low.py +++ b/src/python/grpcio/grpc/_adapter/_intermediary_low.py @@ -127,7 +127,7 @@ class Call(object): def write(self, message, tag): return self._internal.start_batch([ - _types.OpArgs.send_message(message) + _types.OpArgs.send_message(message, 0) ], _TagAdapter(tag, Event.Kind.WRITE_ACCEPTED)) def complete(self, tag): diff --git a/src/python/grpcio/grpc/_adapter/_low.py b/src/python/grpcio/grpc/_adapter/_low.py index dcf67dbc11..147086e725 100644 --- a/src/python/grpcio/grpc/_adapter/_low.py +++ b/src/python/grpcio/grpc/_adapter/_low.py @@ -27,9 +27,12 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from grpc import _grpcio_metadata from grpc._adapter import _c from grpc._adapter import _types +_USER_AGENT = 'Python-gRPC-{}'.format(_grpcio_metadata.__version__) + ClientCredentials = _c.ClientCredentials ServerCredentials = _c.ServerCredentials @@ -72,10 +75,14 @@ class Call(_types.Call): else: return self.call.cancel(code, details) + def peer(self): + return self.call.peer() + class Channel(_types.Channel): def __init__(self, target, args, creds=None): + args = list(args) + [(_c.PRIMARY_USER_AGENT_KEY, _USER_AGENT)] if creds is None: self.channel = _c.Channel(target, args) else: @@ -84,6 +91,17 @@ class Channel(_types.Channel): def create_call(self, completion_queue, method, host, deadline=None): return Call(self.channel.create_call(completion_queue.completion_queue, method, host, deadline)) + def check_connectivity_state(self, try_to_connect): + return self.channel.check_connectivity_state(try_to_connect) + + def watch_connectivity_state(self, last_observed_state, deadline, + completion_queue, tag): + self.channel.watch_connectivity_state( + last_observed_state, deadline, completion_queue.completion_queue, tag) + + def target(self): + return self.channel.target() + _NO_TAG = object() diff --git a/src/python/grpcio/grpc/_adapter/_types.py b/src/python/grpcio/grpc/_adapter/_types.py index 5ddb1774ea..5470d2de4a 100644 --- a/src/python/grpcio/grpc/_adapter/_types.py +++ b/src/python/grpcio/grpc/_adapter/_types.py @@ -31,13 +31,12 @@ import abc import collections import enum -# TODO(atash): decide whether or not to move these enums to the _c module to -# force build errors with upstream changes. class GrpcChannelArgumentKeys(enum.Enum): """Mirrors keys used in grpc_channel_args for GRPC-specific arguments.""" SSL_TARGET_NAME_OVERRIDE = 'grpc.ssl_target_name_override' + @enum.unique class CallError(enum.IntEnum): """Mirrors grpc_call_error in the C core.""" @@ -53,6 +52,7 @@ class CallError(enum.IntEnum): ERROR_INVALID_FLAGS = 9 ERROR_INVALID_METADATA = 10 + @enum.unique class StatusCode(enum.IntEnum): """Mirrors grpc_status_code in the C core.""" @@ -74,6 +74,14 @@ class StatusCode(enum.IntEnum): DATA_LOSS = 15 UNAUTHENTICATED = 16 + +@enum.unique +class OpWriteFlags(enum.IntEnum): + """Mirrors defined write-flag constants in the C core.""" + WRITE_BUFFER_HINT = 1 + WRITE_NO_COMPRESS = 2 + + @enum.unique class OpType(enum.IntEnum): """Mirrors grpc_op_type in the C core.""" @@ -86,12 +94,24 @@ class OpType(enum.IntEnum): RECV_STATUS_ON_CLIENT = 6 RECV_CLOSE_ON_SERVER = 7 + @enum.unique class EventType(enum.IntEnum): """Mirrors grpc_completion_type in the C core.""" - QUEUE_SHUTDOWN = 0 - QUEUE_TIMEOUT = 1 # if seen on the Python side, something went horridly wrong - OP_COMPLETE = 2 + QUEUE_SHUTDOWN = 0 + QUEUE_TIMEOUT = 1 # if seen on the Python side, something went horridly wrong + OP_COMPLETE = 2 + + +@enum.unique +class ConnectivityState(enum.IntEnum): + """Mirrors grpc_connectivity_state in the C core.""" + IDLE = 0 + CONNECTING = 1 + READY = 2 + TRANSIENT_FAILURE = 3 + FATAL_FAILURE = 4 + class Status(collections.namedtuple( 'Status', [ @@ -105,6 +125,7 @@ class Status(collections.namedtuple( details (str): ... """ + class CallDetails(collections.namedtuple( 'CallDetails', [ 'method', @@ -119,6 +140,7 @@ class CallDetails(collections.namedtuple( deadline (float): ... """ + class OpArgs(collections.namedtuple( 'OpArgs', [ 'type', @@ -126,6 +148,7 @@ class OpArgs(collections.namedtuple( 'trailing_metadata', 'message', 'status', + 'write_flags', ])): """Arguments passed into a GRPC operation. @@ -138,39 +161,40 @@ class OpArgs(collections.namedtuple( message (bytes): Only valid if type == OpType.SEND_MESSAGE, else is None. status (Status): Only valid if type == OpType.SEND_STATUS_FROM_SERVER, else is None. + write_flags (int): a bit OR'ing of 0 or more OpWriteFlags values. """ @staticmethod def send_initial_metadata(initial_metadata): - return OpArgs(OpType.SEND_INITIAL_METADATA, initial_metadata, None, None, None) + return OpArgs(OpType.SEND_INITIAL_METADATA, initial_metadata, None, None, None, 0) @staticmethod - def send_message(message): - return OpArgs(OpType.SEND_MESSAGE, None, None, message, None) + def send_message(message, flags): + return OpArgs(OpType.SEND_MESSAGE, None, None, message, None, flags) @staticmethod def send_close_from_client(): - return OpArgs(OpType.SEND_CLOSE_FROM_CLIENT, None, None, None, None) + return OpArgs(OpType.SEND_CLOSE_FROM_CLIENT, None, None, None, None, 0) @staticmethod def send_status_from_server(trailing_metadata, status_code, status_details): - return OpArgs(OpType.SEND_STATUS_FROM_SERVER, None, trailing_metadata, None, Status(status_code, status_details)) + return OpArgs(OpType.SEND_STATUS_FROM_SERVER, None, trailing_metadata, None, Status(status_code, status_details), 0) @staticmethod def recv_initial_metadata(): - return OpArgs(OpType.RECV_INITIAL_METADATA, None, None, None, None); + return OpArgs(OpType.RECV_INITIAL_METADATA, None, None, None, None, 0); @staticmethod def recv_message(): - return OpArgs(OpType.RECV_MESSAGE, None, None, None, None) + return OpArgs(OpType.RECV_MESSAGE, None, None, None, None, 0) @staticmethod def recv_status_on_client(): - return OpArgs(OpType.RECV_STATUS_ON_CLIENT, None, None, None, None) + return OpArgs(OpType.RECV_STATUS_ON_CLIENT, None, None, None, None, 0) @staticmethod def recv_close_on_server(): - return OpArgs(OpType.RECV_CLOSE_ON_SERVER, None, None, None, None) + return OpArgs(OpType.RECV_CLOSE_ON_SERVER, None, None, None, None, 0) class OpResult(collections.namedtuple( @@ -290,6 +314,15 @@ class Call: """ return CallError.ERROR + @abc.abstractmethod + def peer(self): + """Get the peer of this call. + + Returns: + str: the peer of this call. + """ + return None + class Channel: __metaclass__ = abc.ABCMeta @@ -321,6 +354,40 @@ class Channel: """ return None + @abc.abstractmethod + def check_connectivity_state(self, try_to_connect): + """Check and optionally repair the connectivity state of the channel. + + Args: + try_to_connect (bool): whether or not to try to connect the channel if + disconnected. + + Returns: + ConnectivityState: state of the channel at the time of this invocation. + """ + return None + + @abc.abstractmethod + def watch_connectivity_state(self, last_observed_state, deadline, + completion_queue, tag): + """Watch for connectivity state changes from the last_observed_state. + + Args: + last_observed_state (ConnectivityState): ... + deadline (float): ... + completion_queue (CompletionQueue): ... + tag (object) ... + """ + + @abc.abstractmethod + def target(self): + """Get the target of this channel. + + Returns: + str: the target of this channel. + """ + return None + class Server: __metaclass__ = abc.ABCMeta diff --git a/src/python/grpcio_test/grpc_interop/_interop_test_case.py b/src/python/grpcio_test/grpc_interop/_interop_test_case.py index ed8f7ef009..b6d06b300d 100644 --- a/src/python/grpcio_test/grpc_interop/_interop_test_case.py +++ b/src/python/grpcio_test/grpc_interop/_interop_test_case.py @@ -59,3 +59,6 @@ class InteropTestCase(object): def testCancelAfterFirstResponse(self): methods.TestCase.CANCEL_AFTER_FIRST_RESPONSE.test_interoperability(self.stub, None) + + def testTimeoutOnSleepingServer(self): + methods.TestCase.TIMEOUT_ON_SLEEPING_SERVER.test_interoperability(self.stub, None) diff --git a/src/python/grpcio_test/grpc_interop/methods.py b/src/python/grpcio_test/grpc_interop/methods.py index f4c94685ee..7a831f3cbd 100644 --- a/src/python/grpcio_test/grpc_interop/methods.py +++ b/src/python/grpcio_test/grpc_interop/methods.py @@ -33,10 +33,12 @@ import enum import json import os import threading +import time from oauth2client import client as oauth2client_client from grpc.framework.alpha import utilities +from grpc.framework.alpha import exceptions from grpc_interop import empty_pb2 from grpc_interop import messages_pb2 @@ -318,6 +320,24 @@ def _cancel_after_first_response(stub): raise ValueError('expected call to be cancelled') +def _timeout_on_sleeping_server(stub): + request_payload_size = 27182 + with stub, _Pipe() as pipe: + response_iterator = stub.FullDuplexCall(pipe, 0.001) + + request = messages_pb2.StreamingOutputCallRequest( + response_type=messages_pb2.COMPRESSABLE, + payload=messages_pb2.Payload(body=b'\x00' * request_payload_size)) + pipe.add(request) + time.sleep(0.1) + try: + next(response_iterator) + except exceptions.ExpirationError: + pass + else: + raise ValueError('expected call to exceed deadline') + + def _compute_engine_creds(stub, args): response = _large_unary_common_behavior(stub, True, True) if args.default_service_account != response.username: @@ -351,6 +371,7 @@ class TestCase(enum.Enum): CANCEL_AFTER_FIRST_RESPONSE = 'cancel_after_first_response' COMPUTE_ENGINE_CREDS = 'compute_engine_creds' SERVICE_ACCOUNT_CREDS = 'service_account_creds' + TIMEOUT_ON_SLEEPING_SERVER = 'timeout_on_sleeping_server' def test_interoperability(self, stub, args): if self is TestCase.EMPTY_UNARY: @@ -367,6 +388,8 @@ class TestCase(enum.Enum): _cancel_after_begin(stub) elif self is TestCase.CANCEL_AFTER_FIRST_RESPONSE: _cancel_after_first_response(stub) + elif self is TestCase.TIMEOUT_ON_SLEEPING_SERVER: + _timeout_on_sleeping_server(stub) elif self is TestCase.COMPUTE_ENGINE_CREDS: _compute_engine_creds(stub, args) elif self is TestCase.SERVICE_ACCOUNT_CREDS: diff --git a/src/python/grpcio_test/grpc_protoc_plugin/__init__.py b/src/python/grpcio_test/grpc_protoc_plugin/__init__.py new file mode 100644 index 0000000000..7086519106 --- /dev/null +++ b/src/python/grpcio_test/grpc_protoc_plugin/__init__.py @@ -0,0 +1,30 @@ +# Copyright 2015, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + diff --git a/src/python/grpcio_test/grpc_protoc_plugin/python_plugin_test.py b/src/python/grpcio_test/grpc_protoc_plugin/python_plugin_test.py new file mode 100644 index 0000000000..b200d129a9 --- /dev/null +++ b/src/python/grpcio_test/grpc_protoc_plugin/python_plugin_test.py @@ -0,0 +1,541 @@ +# Copyright 2015, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import argparse +import contextlib +import distutils.spawn +import errno +import itertools +import os +import pkg_resources +import shutil +import subprocess +import sys +import tempfile +import threading +import time +import unittest + +from grpc.framework.alpha import exceptions +from grpc.framework.foundation import future + +# Identifiers of entities we expect to find in the generated module. +SERVICER_IDENTIFIER = 'EarlyAdopterTestServiceServicer' +SERVER_IDENTIFIER = 'EarlyAdopterTestServiceServer' +STUB_IDENTIFIER = 'EarlyAdopterTestServiceStub' +SERVER_FACTORY_IDENTIFIER = 'early_adopter_create_TestService_server' +STUB_FACTORY_IDENTIFIER = 'early_adopter_create_TestService_stub' + +# The timeout used in tests of RPCs that are supposed to expire. +SHORT_TIMEOUT = 2 +# The timeout used in tests of RPCs that are not supposed to expire. The +# absurdly large value doesn't matter since no passing execution of this test +# module will ever wait the duration. +LONG_TIMEOUT = 600 +NO_DELAY = 0 + + +class _ServicerMethods(object): + + def __init__(self, test_pb2, delay): + self._condition = threading.Condition() + self._delay = delay + self._paused = False + self._fail = False + self._test_pb2 = test_pb2 + + @contextlib.contextmanager + def pause(self): # pylint: disable=invalid-name + with self._condition: + self._paused = True + yield + with self._condition: + self._paused = False + self._condition.notify_all() + + @contextlib.contextmanager + def fail(self): # pylint: disable=invalid-name + with self._condition: + self._fail = True + yield + with self._condition: + self._fail = False + + def _control(self): # pylint: disable=invalid-name + with self._condition: + if self._fail: + raise ValueError() + while self._paused: + self._condition.wait() + time.sleep(self._delay) + + def UnaryCall(self, request, unused_rpc_context): + response = self._test_pb2.SimpleResponse() + response.payload.payload_type = self._test_pb2.COMPRESSABLE + response.payload.payload_compressable = 'a' * request.response_size + self._control() + return response + + def StreamingOutputCall(self, request, unused_rpc_context): + for parameter in request.response_parameters: + response = self._test_pb2.StreamingOutputCallResponse() + response.payload.payload_type = self._test_pb2.COMPRESSABLE + response.payload.payload_compressable = 'a' * parameter.size + self._control() + yield response + + def StreamingInputCall(self, request_iter, unused_rpc_context): + response = self._test_pb2.StreamingInputCallResponse() + aggregated_payload_size = 0 + for request in request_iter: + aggregated_payload_size += len(request.payload.payload_compressable) + response.aggregated_payload_size = aggregated_payload_size + self._control() + return response + + def FullDuplexCall(self, request_iter, unused_rpc_context): + for request in request_iter: + for parameter in request.response_parameters: + response = self._test_pb2.StreamingOutputCallResponse() + response.payload.payload_type = self._test_pb2.COMPRESSABLE + response.payload.payload_compressable = 'a' * parameter.size + self._control() + yield response + + def HalfDuplexCall(self, request_iter, unused_rpc_context): + responses = [] + for request in request_iter: + for parameter in request.response_parameters: + response = self._test_pb2.StreamingOutputCallResponse() + response.payload.payload_type = self._test_pb2.COMPRESSABLE + response.payload.payload_compressable = 'a' * parameter.size + self._control() + responses.append(response) + for response in responses: + yield response + + +@contextlib.contextmanager +def _CreateService(test_pb2, delay): + """Provides a servicer backend and a stub. + + The servicer is just the implementation + of the actual servicer passed to the face player of the python RPC + implementation; the two are detached. + + Non-zero delay puts a delay on each call to the servicer, representative of + communication latency. Timeout is the default timeout for the stub while + waiting for the service. + + Args: + test_pb2: The test_pb2 module generated by this test. + delay: Delay in seconds per response from the servicer. + + Yields: + A (servicer_methods, servicer, stub) three-tuple where servicer_methods is + the back-end of the service bound to the stub and the server and stub + are both activated and ready for use. + """ + servicer_methods = _ServicerMethods(test_pb2, delay) + + class Servicer(getattr(test_pb2, SERVICER_IDENTIFIER)): + + def UnaryCall(self, request, context): + return servicer_methods.UnaryCall(request, context) + + def StreamingOutputCall(self, request, context): + return servicer_methods.StreamingOutputCall(request, context) + + def StreamingInputCall(self, request_iter, context): + return servicer_methods.StreamingInputCall(request_iter, context) + + def FullDuplexCall(self, request_iter, context): + return servicer_methods.FullDuplexCall(request_iter, context) + + def HalfDuplexCall(self, request_iter, context): + return servicer_methods.HalfDuplexCall(request_iter, context) + + servicer = Servicer() + server = getattr( + test_pb2, SERVER_FACTORY_IDENTIFIER)(servicer, 0) + with server: + port = server.port() + stub = getattr(test_pb2, STUB_FACTORY_IDENTIFIER)('localhost', port) + with stub: + yield servicer_methods, stub, server + + +def _streaming_input_request_iterator(test_pb2): + for _ in range(3): + request = test_pb2.StreamingInputCallRequest() + request.payload.payload_type = test_pb2.COMPRESSABLE + request.payload.payload_compressable = 'a' + yield request + + +def _streaming_output_request(test_pb2): + request = test_pb2.StreamingOutputCallRequest() + sizes = [1, 2, 3] + request.response_parameters.add(size=sizes[0], interval_us=0) + request.response_parameters.add(size=sizes[1], interval_us=0) + request.response_parameters.add(size=sizes[2], interval_us=0) + return request + + +def _full_duplex_request_iterator(test_pb2): + request = test_pb2.StreamingOutputCallRequest() + request.response_parameters.add(size=1, interval_us=0) + yield request + request = test_pb2.StreamingOutputCallRequest() + request.response_parameters.add(size=2, interval_us=0) + request.response_parameters.add(size=3, interval_us=0) + yield request + + +class PythonPluginTest(unittest.TestCase): + """Test case for the gRPC Python protoc-plugin. + + While reading these tests, remember that the futures API + (`stub.method.async()`) only gives futures for the *non-streaming* responses, + else it behaves like its blocking cousin. + """ + + def setUp(self): + # Assume that the appropriate protoc and grpc_python_plugins are on the + # path. + protoc_command = 'protoc' + protoc_plugin_filename = distutils.spawn.find_executable( + 'grpc_python_plugin') + test_proto_filename = pkg_resources.resource_filename( + 'grpc_protoc_plugin', 'test.proto') + if not os.path.isfile(protoc_command): + # Assume that if we haven't built protoc that it's on the system. + protoc_command = 'protoc' + + # Ensure that the output directory exists. + self.outdir = tempfile.mkdtemp() + + # Invoke protoc with the plugin. + cmd = [ + protoc_command, + '--plugin=protoc-gen-python-grpc=%s' % protoc_plugin_filename, + '-I .', + '--python_out=%s' % self.outdir, + '--python-grpc_out=%s' % self.outdir, + os.path.basename(test_proto_filename), + ] + subprocess.check_call(' '.join(cmd), shell=True, env=os.environ, + cwd=os.path.dirname(test_proto_filename)) + sys.path.append(self.outdir) + + def tearDown(self): + try: + shutil.rmtree(self.outdir) + except OSError as exc: + if exc.errno != errno.ENOENT: + raise + + # TODO(atash): Figure out which of these tests is hanging flakily with small + # probability. + + def testImportAttributes(self): + # check that we can access the generated module and its members. + import test_pb2 # pylint: disable=g-import-not-at-top + self.assertIsNotNone(getattr(test_pb2, SERVICER_IDENTIFIER, None)) + self.assertIsNotNone(getattr(test_pb2, SERVER_IDENTIFIER, None)) + self.assertIsNotNone(getattr(test_pb2, STUB_IDENTIFIER, None)) + self.assertIsNotNone(getattr(test_pb2, SERVER_FACTORY_IDENTIFIER, None)) + self.assertIsNotNone(getattr(test_pb2, STUB_FACTORY_IDENTIFIER, None)) + + def testUpDown(self): + import test_pb2 + with _CreateService( + test_pb2, NO_DELAY) as (servicer, stub, unused_server): + request = test_pb2.SimpleRequest(response_size=13) + + def testUnaryCall(self): + import test_pb2 # pylint: disable=g-import-not-at-top + with _CreateService(test_pb2, NO_DELAY) as (methods, stub, unused_server): + timeout = 6 # TODO(issue 2039): LONG_TIMEOUT like the other methods. + request = test_pb2.SimpleRequest(response_size=13) + response = stub.UnaryCall(request, timeout) + expected_response = methods.UnaryCall(request, 'not a real RpcContext!') + self.assertEqual(expected_response, response) + + def testUnaryCallAsync(self): + import test_pb2 # pylint: disable=g-import-not-at-top + request = test_pb2.SimpleRequest(response_size=13) + with _CreateService(test_pb2, NO_DELAY) as ( + methods, stub, unused_server): + # Check that the call does not block waiting for the server to respond. + with methods.pause(): + response_future = stub.UnaryCall.async(request, LONG_TIMEOUT) + response = response_future.result() + expected_response = methods.UnaryCall(request, 'not a real RpcContext!') + self.assertEqual(expected_response, response) + + def testUnaryCallAsyncExpired(self): + import test_pb2 # pylint: disable=g-import-not-at-top + with _CreateService(test_pb2, NO_DELAY) as ( + methods, stub, unused_server): + request = test_pb2.SimpleRequest(response_size=13) + with methods.pause(): + response_future = stub.UnaryCall.async(request, SHORT_TIMEOUT) + with self.assertRaises(exceptions.ExpirationError): + response_future.result() + + @unittest.skip('TODO(atash,nathaniel): figure out why this flakily hangs ' + 'forever and fix.') + def testUnaryCallAsyncCancelled(self): + import test_pb2 # pylint: disable=g-import-not-at-top + request = test_pb2.SimpleRequest(response_size=13) + with _CreateService(test_pb2, NO_DELAY) as ( + methods, stub, unused_server): + with methods.pause(): + response_future = stub.UnaryCall.async(request, 1) + response_future.cancel() + self.assertTrue(response_future.cancelled()) + + def testUnaryCallAsyncFailed(self): + import test_pb2 # pylint: disable=g-import-not-at-top + request = test_pb2.SimpleRequest(response_size=13) + with _CreateService(test_pb2, NO_DELAY) as ( + methods, stub, unused_server): + with methods.fail(): + response_future = stub.UnaryCall.async(request, LONG_TIMEOUT) + self.assertIsNotNone(response_future.exception()) + + def testStreamingOutputCall(self): + import test_pb2 # pylint: disable=g-import-not-at-top + request = _streaming_output_request(test_pb2) + with _CreateService(test_pb2, NO_DELAY) as (methods, stub, unused_server): + responses = stub.StreamingOutputCall(request, LONG_TIMEOUT) + expected_responses = methods.StreamingOutputCall( + request, 'not a real RpcContext!') + for expected_response, response in itertools.izip_longest( + expected_responses, responses): + self.assertEqual(expected_response, response) + + @unittest.skip('TODO(atash,nathaniel): figure out why this flakily hangs ' + 'forever and fix.') + def testStreamingOutputCallExpired(self): + import test_pb2 # pylint: disable=g-import-not-at-top + request = _streaming_output_request(test_pb2) + with _CreateService(test_pb2, NO_DELAY) as ( + methods, stub, unused_server): + with methods.pause(): + responses = stub.StreamingOutputCall(request, SHORT_TIMEOUT) + with self.assertRaises(exceptions.ExpirationError): + list(responses) + + @unittest.skip('TODO(atash,nathaniel): figure out why this flakily hangs ' + 'forever and fix.') + def testStreamingOutputCallCancelled(self): + import test_pb2 # pylint: disable=g-import-not-at-top + request = _streaming_output_request(test_pb2) + with _CreateService(test_pb2, NO_DELAY) as ( + unused_methods, stub, unused_server): + responses = stub.StreamingOutputCall(request, SHORT_TIMEOUT) + next(responses) + responses.cancel() + with self.assertRaises(future.CancelledError): + next(responses) + + @unittest.skip('TODO(atash,nathaniel): figure out why this times out ' + 'instead of raising the proper error.') + def testStreamingOutputCallFailed(self): + import test_pb2 # pylint: disable=g-import-not-at-top + request = _streaming_output_request(test_pb2) + with _CreateService(test_pb2, NO_DELAY) as ( + methods, stub, unused_server): + with methods.fail(): + responses = stub.StreamingOutputCall(request, 1) + self.assertIsNotNone(responses) + with self.assertRaises(exceptions.ServicerError): + next(responses) + + @unittest.skip('TODO(atash,nathaniel): figure out why this flakily hangs ' + 'forever and fix.') + def testStreamingInputCall(self): + import test_pb2 # pylint: disable=g-import-not-at-top + with _CreateService(test_pb2, NO_DELAY) as (methods, stub, unused_server): + response = stub.StreamingInputCall( + _streaming_input_request_iterator(test_pb2), LONG_TIMEOUT) + expected_response = methods.StreamingInputCall( + _streaming_input_request_iterator(test_pb2), 'not a real RpcContext!') + self.assertEqual(expected_response, response) + + def testStreamingInputCallAsync(self): + import test_pb2 # pylint: disable=g-import-not-at-top + with _CreateService(test_pb2, NO_DELAY) as ( + methods, stub, unused_server): + with methods.pause(): + response_future = stub.StreamingInputCall.async( + _streaming_input_request_iterator(test_pb2), LONG_TIMEOUT) + response = response_future.result() + expected_response = methods.StreamingInputCall( + _streaming_input_request_iterator(test_pb2), 'not a real RpcContext!') + self.assertEqual(expected_response, response) + + def testStreamingInputCallAsyncExpired(self): + import test_pb2 # pylint: disable=g-import-not-at-top + with _CreateService(test_pb2, NO_DELAY) as ( + methods, stub, unused_server): + with methods.pause(): + response_future = stub.StreamingInputCall.async( + _streaming_input_request_iterator(test_pb2), SHORT_TIMEOUT) + with self.assertRaises(exceptions.ExpirationError): + response_future.result() + self.assertIsInstance( + response_future.exception(), exceptions.ExpirationError) + + def testStreamingInputCallAsyncCancelled(self): + import test_pb2 # pylint: disable=g-import-not-at-top + with _CreateService(test_pb2, NO_DELAY) as ( + methods, stub, unused_server): + with methods.pause(): + timeout = 6 # TODO(issue 2039): LONG_TIMEOUT like the other methods. + response_future = stub.StreamingInputCall.async( + _streaming_input_request_iterator(test_pb2), timeout) + response_future.cancel() + self.assertTrue(response_future.cancelled()) + with self.assertRaises(future.CancelledError): + response_future.result() + + def testStreamingInputCallAsyncFailed(self): + import test_pb2 # pylint: disable=g-import-not-at-top + with _CreateService(test_pb2, NO_DELAY) as ( + methods, stub, unused_server): + with methods.fail(): + response_future = stub.StreamingInputCall.async( + _streaming_input_request_iterator(test_pb2), SHORT_TIMEOUT) + self.assertIsNotNone(response_future.exception()) + + def testFullDuplexCall(self): + import test_pb2 # pylint: disable=g-import-not-at-top + with _CreateService(test_pb2, NO_DELAY) as (methods, stub, unused_server): + responses = stub.FullDuplexCall( + _full_duplex_request_iterator(test_pb2), LONG_TIMEOUT) + expected_responses = methods.FullDuplexCall( + _full_duplex_request_iterator(test_pb2), 'not a real RpcContext!') + for expected_response, response in itertools.izip_longest( + expected_responses, responses): + self.assertEqual(expected_response, response) + + @unittest.skip('TODO(atash,nathaniel): figure out why this flakily hangs ' + 'forever and fix.') + def testFullDuplexCallExpired(self): + import test_pb2 # pylint: disable=g-import-not-at-top + request_iterator = _full_duplex_request_iterator(test_pb2) + with _CreateService(test_pb2, NO_DELAY) as ( + methods, stub, unused_server): + with methods.pause(): + responses = stub.FullDuplexCall(request_iterator, SHORT_TIMEOUT) + with self.assertRaises(exceptions.ExpirationError): + list(responses) + + @unittest.skip('TODO(atash,nathaniel): figure out why this flakily hangs ' + 'forever and fix.') + def testFullDuplexCallCancelled(self): + import test_pb2 # pylint: disable=g-import-not-at-top + with _CreateService(test_pb2, NO_DELAY) as (methods, stub, unused_server): + request_iterator = _full_duplex_request_iterator(test_pb2) + responses = stub.FullDuplexCall(request_iterator, LONG_TIMEOUT) + next(responses) + responses.cancel() + with self.assertRaises(future.CancelledError): + next(responses) + + @unittest.skip('TODO(atash,nathaniel): figure out why this hangs forever ' + 'and fix.') + def testFullDuplexCallFailed(self): + import test_pb2 # pylint: disable=g-import-not-at-top + request_iterator = _full_duplex_request_iterator(test_pb2) + with _CreateService(test_pb2, NO_DELAY) as ( + methods, stub, unused_server): + with methods.fail(): + responses = stub.FullDuplexCall(request_iterator, LONG_TIMEOUT) + self.assertIsNotNone(responses) + with self.assertRaises(exceptions.ServicerError): + next(responses) + + @unittest.skip('TODO(atash,nathaniel): figure out why this flakily hangs ' + 'forever and fix.') + def testHalfDuplexCall(self): + import test_pb2 # pylint: disable=g-import-not-at-top + with _CreateService(test_pb2, NO_DELAY) as ( + methods, stub, unused_server): + def half_duplex_request_iterator(): + request = test_pb2.StreamingOutputCallRequest() + request.response_parameters.add(size=1, interval_us=0) + yield request + request = test_pb2.StreamingOutputCallRequest() + request.response_parameters.add(size=2, interval_us=0) + request.response_parameters.add(size=3, interval_us=0) + yield request + responses = stub.HalfDuplexCall( + half_duplex_request_iterator(), LONG_TIMEOUT) + expected_responses = methods.HalfDuplexCall( + half_duplex_request_iterator(), 'not a real RpcContext!') + for check in itertools.izip_longest(expected_responses, responses): + expected_response, response = check + self.assertEqual(expected_response, response) + + def testHalfDuplexCallWedged(self): + import test_pb2 # pylint: disable=g-import-not-at-top + condition = threading.Condition() + wait_cell = [False] + @contextlib.contextmanager + def wait(): # pylint: disable=invalid-name + # Where's Python 3's 'nonlocal' statement when you need it? + with condition: + wait_cell[0] = True + yield + with condition: + wait_cell[0] = False + condition.notify_all() + def half_duplex_request_iterator(): + request = test_pb2.StreamingOutputCallRequest() + request.response_parameters.add(size=1, interval_us=0) + yield request + with condition: + while wait_cell[0]: + condition.wait() + with _CreateService(test_pb2, NO_DELAY) as (methods, stub, unused_server): + with wait(): + responses = stub.HalfDuplexCall( + half_duplex_request_iterator(), SHORT_TIMEOUT) + # half-duplex waits for the client to send all info + with self.assertRaises(exceptions.ExpirationError): + next(responses) + + +if __name__ == '__main__': + os.chdir(os.path.dirname(sys.argv[0])) + unittest.main(verbosity=2) diff --git a/src/python/grpcio_test/grpc_protoc_plugin/test.proto b/src/python/grpcio_test/grpc_protoc_plugin/test.proto new file mode 100644 index 0000000000..ed7c6a7b79 --- /dev/null +++ b/src/python/grpcio_test/grpc_protoc_plugin/test.proto @@ -0,0 +1,139 @@ +// Copyright 2015, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// An integration test service that covers all the method signature permutations +// of unary/streaming requests/responses. +// This file is duplicated around the code base. See GitHub issue #526. +syntax = "proto2"; + +package grpc.testing; + +enum PayloadType { + // Compressable text format. + COMPRESSABLE= 1; + + // Uncompressable binary format. + UNCOMPRESSABLE = 2; + + // Randomly chosen from all other formats defined in this enum. + RANDOM = 3; +} + +message Payload { + required PayloadType payload_type = 1; + oneof payload_body { + string payload_compressable = 2; + bytes payload_uncompressable = 3; + } +} + +message SimpleRequest { + // Desired payload type in the response from the server. + // If response_type is RANDOM, server randomly chooses one from other formats. + optional PayloadType response_type = 1 [default=COMPRESSABLE]; + + // Desired payload size in the response from the server. + // If response_type is COMPRESSABLE, this denotes the size before compression. + optional int32 response_size = 2; + + // Optional input payload sent along with the request. + optional Payload payload = 3; +} + +message SimpleResponse { + optional Payload payload = 1; +} + +message StreamingInputCallRequest { + // Optional input payload sent along with the request. + optional Payload payload = 1; + + // Not expecting any payload from the response. +} + +message StreamingInputCallResponse { + // Aggregated size of payloads received from the client. + optional int32 aggregated_payload_size = 1; +} + +message ResponseParameters { + // Desired payload sizes in responses from the server. + // If response_type is COMPRESSABLE, this denotes the size before compression. + required int32 size = 1; + + // Desired interval between consecutive responses in the response stream in + // microseconds. + required int32 interval_us = 2; +} + +message StreamingOutputCallRequest { + // Desired payload type in the response from the server. + // If response_type is RANDOM, the payload from each response in the stream + // might be of different types. This is to simulate a mixed type of payload + // stream. + optional PayloadType response_type = 1 [default=COMPRESSABLE]; + + repeated ResponseParameters response_parameters = 2; + + // Optional input payload sent along with the request. + optional Payload payload = 3; +} + +message StreamingOutputCallResponse { + optional Payload payload = 1; +} + +service TestService { + // One request followed by one response. + // The server returns the client payload as-is. + rpc UnaryCall(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) + returns (stream StreamingOutputCallResponse); + + // A sequence of requests followed by one response (streamed upload). + // The server returns the aggregated size of client payload as the result. + rpc StreamingInputCall(stream StreamingInputCallRequest) + returns (StreamingInputCallResponse); + + // A sequence of requests with each request served by the server immediately. + // As one request could lead to multiple responses, this interface + // demonstrates the idea of full duplexing. + rpc FullDuplexCall(stream StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // A sequence of requests followed by a sequence of responses. + // The server buffers all the client requests and then serves them in order. A + // stream of responses are returned to the client when the server starts with + // first request. + rpc HalfDuplexCall(stream StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); +} diff --git a/src/python/grpcio_test/grpc_test/_adapter/_low_test.py b/src/python/grpcio_test/grpc_test/_adapter/_low_test.py index 9a8edfad0c..44fe760fbc 100644 --- a/src/python/grpcio_test/grpc_test/_adapter/_low_test.py +++ b/src/python/grpcio_test/grpc_test/_adapter/_low_test.py @@ -31,11 +31,12 @@ import threading import time import unittest +from grpc import _grpcio_metadata from grpc._adapter import _types from grpc._adapter import _low -def WaitForEvents(completion_queues, deadline): +def wait_for_events(completion_queues, deadline): """ Args: completion_queues: list of completion queues to wait for events on @@ -62,6 +63,7 @@ def WaitForEvents(completion_queues, deadline): thread.join() return results + class InsecureServerInsecureClient(unittest.TestCase): def setUp(self): @@ -115,7 +117,7 @@ class InsecureServerInsecureClient(unittest.TestCase): client_initial_metadata = [(CLIENT_METADATA_ASCII_KEY, CLIENT_METADATA_ASCII_VALUE), (CLIENT_METADATA_BIN_KEY, CLIENT_METADATA_BIN_VALUE)] client_start_batch_result = client_call.start_batch([ _types.OpArgs.send_initial_metadata(client_initial_metadata), - _types.OpArgs.send_message(REQUEST), + _types.OpArgs.send_message(REQUEST, 0), _types.OpArgs.send_close_from_client(), _types.OpArgs.recv_initial_metadata(), _types.OpArgs.recv_message(), @@ -123,20 +125,34 @@ class InsecureServerInsecureClient(unittest.TestCase): ], client_call_tag) self.assertEquals(_types.CallError.OK, client_start_batch_result) - client_no_event, request_event, = WaitForEvents([self.client_completion_queue, self.server_completion_queue], time.time() + 2) + client_no_event, request_event, = wait_for_events([self.client_completion_queue, self.server_completion_queue], time.time() + 2) self.assertEquals(client_no_event, None) self.assertEquals(_types.EventType.OP_COMPLETE, request_event.type) self.assertIsInstance(request_event.call, _low.Call) self.assertIs(server_request_tag, request_event.tag) self.assertEquals(1, len(request_event.results)) - got_initial_metadata = dict(request_event.results[0].initial_metadata) + received_initial_metadata = dict(request_event.results[0].initial_metadata) + # Check that our metadata were transmitted self.assertEquals( dict(client_initial_metadata), - dict((x, got_initial_metadata[x]) for x in zip(*client_initial_metadata)[0])) + dict((x, received_initial_metadata[x]) for x in zip(*client_initial_metadata)[0])) + # Check that Python's user agent string is a part of the full user agent + # string + self.assertIn('Python-gRPC-{}'.format(_grpcio_metadata.__version__), + received_initial_metadata['user-agent']) self.assertEquals(METHOD, request_event.call_details.method) self.assertEquals(HOST, request_event.call_details.host) self.assertLess(abs(DEADLINE - request_event.call_details.deadline), DEADLINE_TOLERANCE) + # Check that the channel is connected, and that both it and the call have + # the proper target and peer; do this after the first flurry of messages to + # avoid the possibility that connection was delayed by the core until the + # first message was sent. + self.assertEqual(_types.ConnectivityState.READY, + self.client_channel.check_connectivity_state(False)) + self.assertIsNotNone(self.client_channel.target()) + self.assertIsNotNone(client_call.peer()) + server_call_tag = object() server_call = request_event.call server_initial_metadata = [(SERVER_INITIAL_METADATA_KEY, SERVER_INITIAL_METADATA_VALUE)] @@ -144,13 +160,13 @@ class InsecureServerInsecureClient(unittest.TestCase): server_start_batch_result = server_call.start_batch([ _types.OpArgs.send_initial_metadata(server_initial_metadata), _types.OpArgs.recv_message(), - _types.OpArgs.send_message(RESPONSE), + _types.OpArgs.send_message(RESPONSE, 0), _types.OpArgs.recv_close_on_server(), _types.OpArgs.send_status_from_server(server_trailing_metadata, SERVER_STATUS_CODE, SERVER_STATUS_DETAILS) ], server_call_tag) self.assertEquals(_types.CallError.OK, server_start_batch_result) - client_event, server_event, = WaitForEvents([self.client_completion_queue, self.server_completion_queue], time.time() + 1) + client_event, server_event, = wait_for_events([self.client_completion_queue, self.server_completion_queue], time.time() + 1) self.assertEquals(6, len(client_event.results)) found_client_op_types = set() diff --git a/src/python/grpcio_test/grpc_test/framework/face/testing/blocking_invocation_inline_service_test_case.py b/src/python/grpcio_test/grpc_test/framework/face/testing/blocking_invocation_inline_service_test_case.py index 7e1158f96b..251e1eb68e 100644 --- a/src/python/grpcio_test/grpc_test/framework/face/testing/blocking_invocation_inline_service_test_case.py +++ b/src/python/grpcio_test/grpc_test/framework/face/testing/blocking_invocation_inline_service_test_case.py @@ -34,15 +34,13 @@ import abc import unittest # pylint: disable=unused-import from grpc.framework.face import exceptions +from grpc_test.framework.common import test_constants from grpc_test.framework.face.testing import control from grpc_test.framework.face.testing import coverage from grpc_test.framework.face.testing import digest from grpc_test.framework.face.testing import stock_service from grpc_test.framework.face.testing import test_case -_TIMEOUT = 3 -_LONG_TIMEOUT = 45 - class BlockingInvocationInlineServiceTestCase( test_case.FaceTestCase, coverage.BlockingCoverage): @@ -79,7 +77,7 @@ class BlockingInvocationInlineServiceTestCase( request = test_messages.request() response = self.stub.blocking_value_in_value_out( - name, request, _LONG_TIMEOUT) + name, request, test_constants.LONG_TIMEOUT) test_messages.verify(request, response, self) @@ -90,7 +88,7 @@ class BlockingInvocationInlineServiceTestCase( request = test_messages.request() response_iterator = self.stub.inline_value_in_stream_out( - name, request, _LONG_TIMEOUT) + name, request, test_constants.LONG_TIMEOUT) responses = list(response_iterator) test_messages.verify(request, responses, self) @@ -102,7 +100,7 @@ class BlockingInvocationInlineServiceTestCase( requests = test_messages.requests() response = self.stub.blocking_stream_in_value_out( - name, iter(requests), _LONG_TIMEOUT) + name, iter(requests), test_constants.LONG_TIMEOUT) test_messages.verify(requests, response, self) @@ -113,7 +111,7 @@ class BlockingInvocationInlineServiceTestCase( requests = test_messages.requests() response_iterator = self.stub.inline_stream_in_stream_out( - name, iter(requests), _LONG_TIMEOUT) + name, iter(requests), test_constants.LONG_TIMEOUT) responses = list(response_iterator) test_messages.verify(requests, responses, self) @@ -126,12 +124,12 @@ class BlockingInvocationInlineServiceTestCase( second_request = test_messages.request() first_response = self.stub.blocking_value_in_value_out( - name, first_request, _TIMEOUT) + name, first_request, test_constants.SHORT_TIMEOUT) test_messages.verify(first_request, first_response, self) second_response = self.stub.blocking_value_in_value_out( - name, second_request, _TIMEOUT) + name, second_request, test_constants.SHORT_TIMEOUT) test_messages.verify(second_request, second_response, self) @@ -144,7 +142,7 @@ class BlockingInvocationInlineServiceTestCase( with self.control.pause(), self.assertRaises( exceptions.ExpirationError): multi_callable = self.stub.unary_unary_multi_callable(name) - multi_callable(request, _TIMEOUT) + multi_callable(request, test_constants.SHORT_TIMEOUT) def testExpiredUnaryRequestStreamResponse(self): for name, test_messages_sequence in ( @@ -155,7 +153,7 @@ class BlockingInvocationInlineServiceTestCase( with self.control.pause(), self.assertRaises( exceptions.ExpirationError): response_iterator = self.stub.inline_value_in_stream_out( - name, request, _TIMEOUT) + name, request, test_constants.SHORT_TIMEOUT) list(response_iterator) def testExpiredStreamRequestUnaryResponse(self): @@ -167,7 +165,7 @@ class BlockingInvocationInlineServiceTestCase( with self.control.pause(), self.assertRaises( exceptions.ExpirationError): multi_callable = self.stub.stream_unary_multi_callable(name) - multi_callable(iter(requests), _TIMEOUT) + multi_callable(iter(requests), test_constants.SHORT_TIMEOUT) def testExpiredStreamRequestStreamResponse(self): for name, test_messages_sequence in ( @@ -178,7 +176,7 @@ class BlockingInvocationInlineServiceTestCase( with self.control.pause(), self.assertRaises( exceptions.ExpirationError): response_iterator = self.stub.inline_stream_in_stream_out( - name, iter(requests), _TIMEOUT) + name, iter(requests), test_constants.SHORT_TIMEOUT) list(response_iterator) def testFailedUnaryRequestUnaryResponse(self): @@ -188,7 +186,8 @@ class BlockingInvocationInlineServiceTestCase( request = test_messages.request() with self.control.fail(), self.assertRaises(exceptions.ServicerError): - self.stub.blocking_value_in_value_out(name, request, _TIMEOUT) + self.stub.blocking_value_in_value_out(name, request, + test_constants.SHORT_TIMEOUT) def testFailedUnaryRequestStreamResponse(self): for name, test_messages_sequence in ( @@ -198,7 +197,7 @@ class BlockingInvocationInlineServiceTestCase( with self.control.fail(), self.assertRaises(exceptions.ServicerError): response_iterator = self.stub.inline_value_in_stream_out( - name, request, _TIMEOUT) + name, request, test_constants.SHORT_TIMEOUT) list(response_iterator) def testFailedStreamRequestUnaryResponse(self): @@ -208,7 +207,8 @@ class BlockingInvocationInlineServiceTestCase( requests = test_messages.requests() with self.control.fail(), self.assertRaises(exceptions.ServicerError): - self.stub.blocking_stream_in_value_out(name, iter(requests), _TIMEOUT) + self.stub.blocking_stream_in_value_out(name, iter(requests), + test_constants.SHORT_TIMEOUT) def testFailedStreamRequestStreamResponse(self): for name, test_messages_sequence in ( @@ -218,5 +218,5 @@ class BlockingInvocationInlineServiceTestCase( with self.control.fail(), self.assertRaises(exceptions.ServicerError): response_iterator = self.stub.inline_stream_in_stream_out( - name, iter(requests), _TIMEOUT) + name, iter(requests), test_constants.SHORT_TIMEOUT) list(response_iterator) diff --git a/src/python/grpcio_test/grpc_test/framework/face/testing/event_invocation_synchronous_event_service_test_case.py b/src/python/grpcio_test/grpc_test/framework/face/testing/event_invocation_synchronous_event_service_test_case.py index 18eed53d6e..9df77678eb 100644 --- a/src/python/grpcio_test/grpc_test/framework/face/testing/event_invocation_synchronous_event_service_test_case.py +++ b/src/python/grpcio_test/grpc_test/framework/face/testing/event_invocation_synchronous_event_service_test_case.py @@ -33,6 +33,7 @@ import abc import unittest from grpc.framework.face import interfaces +from grpc_test.framework.common import test_constants from grpc_test.framework.face.testing import callback as testing_callback from grpc_test.framework.face.testing import control from grpc_test.framework.face.testing import coverage @@ -40,8 +41,6 @@ from grpc_test.framework.face.testing import digest from grpc_test.framework.face.testing import stock_service from grpc_test.framework.face.testing import test_case -_TIMEOUT = 3 - class EventInvocationSynchronousEventServiceTestCase( test_case.FaceTestCase, coverage.FullCoverage): @@ -79,7 +78,8 @@ class EventInvocationSynchronousEventServiceTestCase( callback = testing_callback.Callback() self.stub.event_value_in_value_out( - name, request, callback.complete, callback.abort, _TIMEOUT) + name, request, callback.complete, callback.abort, + test_constants.SHORT_TIMEOUT) callback.block_until_terminated() response = callback.response() @@ -93,7 +93,8 @@ class EventInvocationSynchronousEventServiceTestCase( callback = testing_callback.Callback() self.stub.event_value_in_stream_out( - name, request, callback, callback.abort, _TIMEOUT) + name, request, callback, callback.abort, + test_constants.SHORT_TIMEOUT) callback.block_until_terminated() responses = callback.responses() @@ -107,7 +108,8 @@ class EventInvocationSynchronousEventServiceTestCase( callback = testing_callback.Callback() unused_call, request_consumer = self.stub.event_stream_in_value_out( - name, callback.complete, callback.abort, _TIMEOUT) + name, callback.complete, callback.abort, + test_constants.SHORT_TIMEOUT) for request in requests: request_consumer.consume(request) request_consumer.terminate() @@ -124,7 +126,7 @@ class EventInvocationSynchronousEventServiceTestCase( callback = testing_callback.Callback() unused_call, request_consumer = self.stub.event_stream_in_stream_out( - name, callback, callback.abort, _TIMEOUT) + name, callback, callback.abort, test_constants.SHORT_TIMEOUT) for request in requests: request_consumer.consume(request) request_consumer.terminate() @@ -147,11 +149,11 @@ class EventInvocationSynchronousEventServiceTestCase( first_callback.complete(first_response) self.stub.event_value_in_value_out( name, second_request, second_callback.complete, - second_callback.abort, _TIMEOUT) + second_callback.abort, test_constants.SHORT_TIMEOUT) self.stub.event_value_in_value_out( name, first_request, make_second_invocation, first_callback.abort, - _TIMEOUT) + test_constants.SHORT_TIMEOUT) second_callback.block_until_terminated() first_response = first_callback.response() @@ -168,7 +170,8 @@ class EventInvocationSynchronousEventServiceTestCase( with self.control.pause(): self.stub.event_value_in_value_out( - name, request, callback.complete, callback.abort, _TIMEOUT) + name, request, callback.complete, callback.abort, + test_constants.SHORT_TIMEOUT) callback.block_until_terminated() self.assertEqual(interfaces.Abortion.EXPIRED, callback.abortion()) @@ -182,7 +185,8 @@ class EventInvocationSynchronousEventServiceTestCase( with self.control.pause(): self.stub.event_value_in_stream_out( - name, request, callback, callback.abort, _TIMEOUT) + name, request, callback, callback.abort, + test_constants.SHORT_TIMEOUT) callback.block_until_terminated() self.assertEqual(interfaces.Abortion.EXPIRED, callback.abortion()) @@ -194,7 +198,8 @@ class EventInvocationSynchronousEventServiceTestCase( callback = testing_callback.Callback() self.stub.event_stream_in_value_out( - name, callback.complete, callback.abort, _TIMEOUT) + name, callback.complete, callback.abort, + test_constants.SHORT_TIMEOUT) callback.block_until_terminated() self.assertEqual(interfaces.Abortion.EXPIRED, callback.abortion()) @@ -207,7 +212,7 @@ class EventInvocationSynchronousEventServiceTestCase( callback = testing_callback.Callback() unused_call, request_consumer = self.stub.event_stream_in_stream_out( - name, callback, callback.abort, _TIMEOUT) + name, callback, callback.abort, test_constants.SHORT_TIMEOUT) for request in requests: request_consumer.consume(request) callback.block_until_terminated() @@ -223,10 +228,12 @@ class EventInvocationSynchronousEventServiceTestCase( with self.control.fail(): self.stub.event_value_in_value_out( - name, request, callback.complete, callback.abort, _TIMEOUT) + name, request, callback.complete, callback.abort, + test_constants.SHORT_TIMEOUT) callback.block_until_terminated() - self.assertEqual(interfaces.Abortion.SERVICER_FAILURE, callback.abortion()) + self.assertEqual(interfaces.Abortion.SERVICER_FAILURE, + callback.abortion()) def testFailedUnaryRequestStreamResponse(self): for name, test_messages_sequence in ( @@ -237,10 +244,12 @@ class EventInvocationSynchronousEventServiceTestCase( with self.control.fail(): self.stub.event_value_in_stream_out( - name, request, callback, callback.abort, _TIMEOUT) + name, request, callback, callback.abort, + test_constants.SHORT_TIMEOUT) callback.block_until_terminated() - self.assertEqual(interfaces.Abortion.SERVICER_FAILURE, callback.abortion()) + self.assertEqual(interfaces.Abortion.SERVICER_FAILURE, + callback.abortion()) def testFailedStreamRequestUnaryResponse(self): for name, test_messages_sequence in ( @@ -251,13 +260,15 @@ class EventInvocationSynchronousEventServiceTestCase( with self.control.fail(): unused_call, request_consumer = self.stub.event_stream_in_value_out( - name, callback.complete, callback.abort, _TIMEOUT) + name, callback.complete, callback.abort, + test_constants.SHORT_TIMEOUT) for request in requests: request_consumer.consume(request) request_consumer.terminate() callback.block_until_terminated() - self.assertEqual(interfaces.Abortion.SERVICER_FAILURE, callback.abortion()) + self.assertEqual(interfaces.Abortion.SERVICER_FAILURE, + callback.abortion()) def testFailedStreamRequestStreamResponse(self): for name, test_messages_sequence in ( @@ -268,7 +279,7 @@ class EventInvocationSynchronousEventServiceTestCase( with self.control.fail(): unused_call, request_consumer = self.stub.event_stream_in_stream_out( - name, callback, callback.abort, _TIMEOUT) + name, callback, callback.abort, test_constants.SHORT_TIMEOUT) for request in requests: request_consumer.consume(request) request_consumer.terminate() @@ -287,10 +298,10 @@ class EventInvocationSynchronousEventServiceTestCase( self.stub.event_value_in_value_out( name, first_request, first_callback.complete, first_callback.abort, - _TIMEOUT) + test_constants.SHORT_TIMEOUT) self.stub.event_value_in_value_out( name, second_request, second_callback.complete, - second_callback.abort, _TIMEOUT) + second_callback.abort, test_constants.SHORT_TIMEOUT) first_callback.block_until_terminated() second_callback.block_until_terminated() @@ -312,7 +323,8 @@ class EventInvocationSynchronousEventServiceTestCase( with self.control.pause(): call = self.stub.event_value_in_value_out( - name, request, callback.complete, callback.abort, _TIMEOUT) + name, request, callback.complete, callback.abort, + test_constants.SHORT_TIMEOUT) call.cancel() callback.block_until_terminated() @@ -326,7 +338,8 @@ class EventInvocationSynchronousEventServiceTestCase( callback = testing_callback.Callback() call = self.stub.event_value_in_stream_out( - name, request, callback, callback.abort, _TIMEOUT) + name, request, callback, callback.abort, + test_constants.SHORT_TIMEOUT) call.cancel() callback.block_until_terminated() @@ -340,7 +353,8 @@ class EventInvocationSynchronousEventServiceTestCase( callback = testing_callback.Callback() call, request_consumer = self.stub.event_stream_in_value_out( - name, callback.complete, callback.abort, _TIMEOUT) + name, callback.complete, callback.abort, + test_constants.SHORT_TIMEOUT) for request in requests: request_consumer.consume(request) call.cancel() @@ -355,7 +369,7 @@ class EventInvocationSynchronousEventServiceTestCase( callback = testing_callback.Callback() call, unused_request_consumer = self.stub.event_stream_in_stream_out( - name, callback, callback.abort, _TIMEOUT) + name, callback, callback.abort, test_constants.SHORT_TIMEOUT) call.cancel() callback.block_until_terminated() diff --git a/src/python/grpcio_test/grpc_test/framework/face/testing/future_invocation_asynchronous_event_service_test_case.py b/src/python/grpcio_test/grpc_test/framework/face/testing/future_invocation_asynchronous_event_service_test_case.py index 3b42914342..70d86a0422 100644 --- a/src/python/grpcio_test/grpc_test/framework/face/testing/future_invocation_asynchronous_event_service_test_case.py +++ b/src/python/grpcio_test/grpc_test/framework/face/testing/future_invocation_asynchronous_event_service_test_case.py @@ -37,13 +37,13 @@ import unittest from grpc.framework.face import exceptions from grpc.framework.foundation import future from grpc.framework.foundation import logging_pool +from grpc_test.framework.common import test_constants from grpc_test.framework.face.testing import control from grpc_test.framework.face.testing import coverage from grpc_test.framework.face.testing import digest from grpc_test.framework.face.testing import stock_service from grpc_test.framework.face.testing import test_case -_TIMEOUT = 3 _MAXIMUM_POOL_SIZE = 10 @@ -110,7 +110,7 @@ class FutureInvocationAsynchronousEventServiceTestCase( request = test_messages.request() response_future = self.stub.future_value_in_value_out( - name, request, _TIMEOUT) + name, request, test_constants.SHORT_TIMEOUT) response = response_future.result() test_messages.verify(request, response, self) @@ -122,7 +122,7 @@ class FutureInvocationAsynchronousEventServiceTestCase( request = test_messages.request() response_iterator = self.stub.inline_value_in_stream_out( - name, request, _TIMEOUT) + name, request, test_constants.SHORT_TIMEOUT) responses = list(response_iterator) test_messages.verify(request, responses, self) @@ -138,7 +138,7 @@ class FutureInvocationAsynchronousEventServiceTestCase( # returned to calling code before the iterator yields any requests. with request_iterator.pause(): response_future = self.stub.future_stream_in_value_out( - name, request_iterator, _TIMEOUT) + name, request_iterator, test_constants.SHORT_TIMEOUT) response = response_future.result() test_messages.verify(requests, response, self) @@ -154,7 +154,7 @@ class FutureInvocationAsynchronousEventServiceTestCase( # returned to calling code before the iterator yields any requests. with request_iterator.pause(): response_iterator = self.stub.inline_stream_in_stream_out( - name, request_iterator, _TIMEOUT) + name, request_iterator, test_constants.SHORT_TIMEOUT) responses = list(response_iterator) test_messages.verify(requests, responses, self) @@ -167,13 +167,13 @@ class FutureInvocationAsynchronousEventServiceTestCase( second_request = test_messages.request() first_response_future = self.stub.future_value_in_value_out( - name, first_request, _TIMEOUT) + name, first_request, test_constants.SHORT_TIMEOUT) first_response = first_response_future.result() test_messages.verify(first_request, first_response, self) second_response_future = self.stub.future_value_in_value_out( - name, second_request, _TIMEOUT) + name, second_request, test_constants.SHORT_TIMEOUT) second_response = second_response_future.result() test_messages.verify(second_request, second_response, self) @@ -186,7 +186,8 @@ class FutureInvocationAsynchronousEventServiceTestCase( with self.control.pause(): multi_callable = self.stub.unary_unary_multi_callable(name) - response_future = multi_callable.future(request, _TIMEOUT) + response_future = multi_callable.future(request, + test_constants.SHORT_TIMEOUT) self.assertIsInstance( response_future.exception(), exceptions.ExpirationError) with self.assertRaises(exceptions.ExpirationError): @@ -200,7 +201,7 @@ class FutureInvocationAsynchronousEventServiceTestCase( with self.control.pause(): response_iterator = self.stub.inline_value_in_stream_out( - name, request, _TIMEOUT) + name, request, test_constants.SHORT_TIMEOUT) with self.assertRaises(exceptions.ExpirationError): list(response_iterator) @@ -212,7 +213,8 @@ class FutureInvocationAsynchronousEventServiceTestCase( with self.control.pause(): multi_callable = self.stub.stream_unary_multi_callable(name) - response_future = multi_callable.future(iter(requests), _TIMEOUT) + response_future = multi_callable.future(iter(requests), + test_constants.SHORT_TIMEOUT) self.assertIsInstance( response_future.exception(), exceptions.ExpirationError) with self.assertRaises(exceptions.ExpirationError): @@ -226,7 +228,7 @@ class FutureInvocationAsynchronousEventServiceTestCase( with self.control.pause(): response_iterator = self.stub.inline_stream_in_stream_out( - name, iter(requests), _TIMEOUT) + name, iter(requests), test_constants.SHORT_TIMEOUT) with self.assertRaises(exceptions.ExpirationError): list(response_iterator) @@ -238,7 +240,7 @@ class FutureInvocationAsynchronousEventServiceTestCase( with self.control.fail(): response_future = self.stub.future_value_in_value_out( - name, request, _TIMEOUT) + name, request, test_constants.SHORT_TIMEOUT) # Because the servicer fails outside of the thread from which the # servicer-side runtime called into it its failure is @@ -261,7 +263,7 @@ class FutureInvocationAsynchronousEventServiceTestCase( # expiration of the RPC. with self.control.fail(), self.assertRaises(exceptions.ExpirationError): response_iterator = self.stub.inline_value_in_stream_out( - name, request, _TIMEOUT) + name, request, test_constants.SHORT_TIMEOUT) list(response_iterator) def testFailedStreamRequestUnaryResponse(self): @@ -272,7 +274,7 @@ class FutureInvocationAsynchronousEventServiceTestCase( with self.control.fail(): response_future = self.stub.future_stream_in_value_out( - name, iter(requests), _TIMEOUT) + name, iter(requests), test_constants.SHORT_TIMEOUT) # Because the servicer fails outside of the thread from which the # servicer-side runtime called into it its failure is @@ -295,7 +297,7 @@ class FutureInvocationAsynchronousEventServiceTestCase( # expiration of the RPC. with self.control.fail(), self.assertRaises(exceptions.ExpirationError): response_iterator = self.stub.inline_stream_in_stream_out( - name, iter(requests), _TIMEOUT) + name, iter(requests), test_constants.SHORT_TIMEOUT) list(response_iterator) def testParallelInvocations(self): @@ -305,10 +307,11 @@ class FutureInvocationAsynchronousEventServiceTestCase( first_request = test_messages.request() second_request = test_messages.request() + # TODO(bug 2039): use LONG_TIMEOUT instead first_response_future = self.stub.future_value_in_value_out( - name, first_request, _TIMEOUT) + name, first_request, test_constants.SHORT_TIMEOUT) second_response_future = self.stub.future_value_in_value_out( - name, second_request, _TIMEOUT) + name, second_request, test_constants.SHORT_TIMEOUT) first_response = first_response_future.result() second_response = second_response_future.result() @@ -327,7 +330,7 @@ class FutureInvocationAsynchronousEventServiceTestCase( with self.control.pause(): response_future = self.stub.future_value_in_value_out( - name, request, _TIMEOUT) + name, request, test_constants.SHORT_TIMEOUT) cancel_method_return_value = response_future.cancel() self.assertFalse(cancel_method_return_value) @@ -341,7 +344,7 @@ class FutureInvocationAsynchronousEventServiceTestCase( with self.control.pause(): response_iterator = self.stub.inline_value_in_stream_out( - name, request, _TIMEOUT) + name, request, test_constants.SHORT_TIMEOUT) response_iterator.cancel() with self.assertRaises(future.CancelledError): @@ -355,7 +358,7 @@ class FutureInvocationAsynchronousEventServiceTestCase( with self.control.pause(): response_future = self.stub.future_stream_in_value_out( - name, iter(requests), _TIMEOUT) + name, iter(requests), test_constants.SHORT_TIMEOUT) cancel_method_return_value = response_future.cancel() self.assertFalse(cancel_method_return_value) @@ -369,7 +372,7 @@ class FutureInvocationAsynchronousEventServiceTestCase( with self.control.pause(): response_iterator = self.stub.inline_stream_in_stream_out( - name, iter(requests), _TIMEOUT) + name, iter(requests), test_constants.SHORT_TIMEOUT) response_iterator.cancel() with self.assertRaises(future.CancelledError): diff --git a/src/python/grpcio_test/setup.py b/src/python/grpcio_test/setup.py index 925c32720f..a6203cae2d 100644 --- a/src/python/grpcio_test/setup.py +++ b/src/python/grpcio_test/setup.py @@ -48,8 +48,13 @@ _PACKAGE_DIRECTORIES = { _PACKAGE_DATA = { 'grpc_interop': [ - 'credentials/ca.pem', 'credentials/server1.key', - 'credentials/server1.pem',] + 'credentials/ca.pem', + 'credentials/server1.key', + 'credentials/server1.pem', + ], + 'grpc_protoc_plugin': [ + 'test.proto', + ], } _SETUP_REQUIRES = ( @@ -75,5 +80,5 @@ setuptools.setup( package_data=_PACKAGE_DATA, install_requires=_INSTALL_REQUIRES + _SETUP_REQUIRES, setup_requires=_SETUP_REQUIRES, - cmdclass=_COMMAND_CLASS + cmdclass=_COMMAND_CLASS, ) diff --git a/src/ruby/ext/grpc/rb_call.c b/src/ruby/ext/grpc/rb_call.c index a7607a83a3..88659da535 100644 --- a/src/ruby/ext/grpc/rb_call.c +++ b/src/ruby/ext/grpc/rb_call.c @@ -179,6 +179,19 @@ static VALUE grpc_rb_call_cancel(VALUE self) { return Qnil; } +/* Called to obtain the peer that this call is connected to. */ +static VALUE grpc_rb_call_get_peer(VALUE self) { + VALUE res = Qnil; + grpc_call *call = NULL; + char *peer = NULL; + TypedData_Get_Struct(self, grpc_call, &grpc_call_data_type, call); + peer = grpc_call_get_peer(call); + res = rb_str_new2(peer); + gpr_free(peer); + + return res; +} + /* call-seq: status = call.status @@ -720,6 +733,7 @@ void Init_grpc_call() { /* Add ruby analogues of the Call methods. */ rb_define_method(grpc_rb_cCall, "run_batch", grpc_rb_call_run_batch, 4); rb_define_method(grpc_rb_cCall, "cancel", grpc_rb_call_cancel, 0); + rb_define_method(grpc_rb_cCall, "peer", grpc_rb_call_get_peer, 0); rb_define_method(grpc_rb_cCall, "status", grpc_rb_call_get_status, 0); rb_define_method(grpc_rb_cCall, "status=", grpc_rb_call_set_status, 1); rb_define_method(grpc_rb_cCall, "metadata", grpc_rb_call_get_metadata, 0); diff --git a/src/ruby/ext/grpc/rb_channel.c b/src/ruby/ext/grpc/rb_channel.c index a0663607c2..2129ba3485 100644 --- a/src/ruby/ext/grpc/rb_channel.c +++ b/src/ruby/ext/grpc/rb_channel.c @@ -37,6 +37,7 @@ #include <grpc/grpc.h> #include <grpc/grpc_security.h> +#include <grpc/support/alloc.h> #include "rb_grpc.h" #include "rb_call.h" #include "rb_channel_args.h" @@ -164,6 +165,65 @@ static VALUE grpc_rb_channel_init(int argc, VALUE *argv, VALUE self) { return self; } +/* + call-seq: + insecure_channel = Channel:new("myhost:8080", {'arg1': 'value1'}) + creds = ... + secure_channel = Channel:new("myhost:443", {'arg1': 'value1'}, creds) + + Creates channel instances. */ +static VALUE grpc_rb_channel_get_connectivity_state(int argc, VALUE *argv, + VALUE self) { + VALUE try_to_connect = Qfalse; + grpc_rb_channel *wrapper = NULL; + grpc_channel *ch = NULL; + + /* "01" == 0 mandatory args, 1 (try_to_connect) is optional */ + rb_scan_args(argc, argv, "01", try_to_connect); + + TypedData_Get_Struct(self, grpc_rb_channel, &grpc_channel_data_type, wrapper); + ch = wrapper->wrapped; + if (ch == NULL) { + rb_raise(rb_eRuntimeError, "closed!"); + return Qnil; + } + return NUM2LONG( + grpc_channel_check_connectivity_state(ch, (int)try_to_connect)); +} + +/* Watch for a change in connectivity state. + + Once the channel connectivity state is different from the last observed + state, tag will be enqueued on cq with success=1 + + If deadline expires BEFORE the state is changed, tag will be enqueued on + the completion queue with success=0 */ +static VALUE grpc_rb_channel_watch_connectivity_state(VALUE self, + VALUE last_state, + VALUE cqueue, + VALUE deadline, + VALUE tag) { + grpc_rb_channel *wrapper = NULL; + grpc_channel *ch = NULL; + grpc_completion_queue *cq = NULL; + + cq = grpc_rb_get_wrapped_completion_queue(cqueue); + TypedData_Get_Struct(self, grpc_rb_channel, &grpc_channel_data_type, wrapper); + ch = wrapper->wrapped; + if (ch == NULL) { + rb_raise(rb_eRuntimeError, "closed!"); + return Qnil; + } + grpc_channel_watch_connectivity_state( + ch, + NUM2LONG(last_state), + grpc_rb_time_timeval(deadline, /* absolute time */ 0), + cq, + ROBJECT(tag)); + + return Qnil; +} + /* Clones Channel instances. Gives Channel a consistent implementation of Ruby's object copy/dup @@ -194,15 +254,28 @@ static VALUE grpc_rb_channel_init_copy(VALUE copy, VALUE orig) { /* 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 cqueue, VALUE method, - VALUE host, VALUE deadline) { +static VALUE grpc_rb_channel_create_call(VALUE self, VALUE cqueue, + VALUE parent, VALUE mask, + VALUE method, VALUE host, + VALUE deadline) { VALUE res = Qnil; grpc_rb_channel *wrapper = NULL; grpc_call *call = NULL; + grpc_call *parent_call = NULL; grpc_channel *ch = NULL; grpc_completion_queue *cq = NULL; + int flags = GRPC_PROPAGATE_DEFAULTS; char *method_chars = StringValueCStr(method); - char *host_chars = StringValueCStr(host); + char *host_chars = NULL; + if (host != Qnil) { + host_chars = StringValueCStr(host); + } + if (mask != Qnil) { + flags = NUM2UINT(mask); + } + if (parent != Qnil) { + parent_call = grpc_rb_get_wrapped_call(parent); + } cq = grpc_rb_get_wrapped_completion_queue(cqueue); TypedData_Get_Struct(self, grpc_rb_channel, &grpc_channel_data_type, wrapper); @@ -212,10 +285,10 @@ static VALUE grpc_rb_channel_create_call(VALUE self, VALUE cqueue, VALUE method, return Qnil; } - call = grpc_channel_create_call(ch, NULL, GRPC_PROPAGATE_DEFAULTS, cq, - method_chars, host_chars, - grpc_rb_time_timeval(deadline, - /* absolute time */ 0)); + call = grpc_channel_create_call(ch, parent_call, flags, cq, 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); @@ -233,6 +306,7 @@ static VALUE grpc_rb_channel_create_call(VALUE self, VALUE cqueue, VALUE method, return res; } + /* Closes the channel, calling it's destroy method */ static VALUE grpc_rb_channel_destroy(VALUE self) { grpc_rb_channel *wrapper = NULL; @@ -249,6 +323,53 @@ static VALUE grpc_rb_channel_destroy(VALUE self) { return Qnil; } + +/* Called to obtain the target that this channel accesses. */ +static VALUE grpc_rb_channel_get_target(VALUE self) { + grpc_rb_channel *wrapper = NULL; + VALUE res = Qnil; + char* target = NULL; + + TypedData_Get_Struct(self, grpc_rb_channel, &grpc_channel_data_type, wrapper); + target = grpc_channel_get_target(wrapper->wrapped); + res = rb_str_new2(target); + gpr_free(target); + + return res; +} + +static void Init_grpc_propagate_masks() { + /* Constants representing call propagation masks in grpc.h */ + VALUE grpc_rb_mPropagateMasks = rb_define_module_under( + grpc_rb_mGrpcCore, "PropagateMasks"); + rb_define_const(grpc_rb_mPropagateMasks, "DEADLINE", + UINT2NUM(GRPC_PROPAGATE_DEADLINE)); + rb_define_const(grpc_rb_mPropagateMasks, "CENSUS_STATS_CONTEXT", + UINT2NUM(GRPC_PROPAGATE_CENSUS_STATS_CONTEXT)); + rb_define_const(grpc_rb_mPropagateMasks, "CENSUS_TRACING_CONTEXT", + UINT2NUM(GRPC_PROPAGATE_CENSUS_TRACING_CONTEXT)); + rb_define_const(grpc_rb_mPropagateMasks, "CANCELLATION", + UINT2NUM(GRPC_PROPAGATE_CANCELLATION)); + rb_define_const(grpc_rb_mPropagateMasks, "DEFAULTS", + UINT2NUM(GRPC_PROPAGATE_DEFAULTS)); +} + +static void Init_grpc_connectivity_states() { + /* Constants representing call propagation masks in grpc.h */ + VALUE grpc_rb_mConnectivityStates = rb_define_module_under( + grpc_rb_mGrpcCore, "ConnectivityStates"); + rb_define_const(grpc_rb_mConnectivityStates, "IDLE", + LONG2NUM(GRPC_CHANNEL_IDLE)); + rb_define_const(grpc_rb_mConnectivityStates, "CONNECTING", + LONG2NUM(GRPC_CHANNEL_CONNECTING)); + rb_define_const(grpc_rb_mConnectivityStates, "READY", + LONG2NUM(GRPC_CHANNEL_READY)); + rb_define_const(grpc_rb_mConnectivityStates, "TRANSIENT_FAILURE", + LONG2NUM(GRPC_CHANNEL_TRANSIENT_FAILURE)); + rb_define_const(grpc_rb_mConnectivityStates, "FATAL_FAILURE", + LONG2NUM(GRPC_CHANNEL_FATAL_FAILURE)); +} + void Init_grpc_channel() { grpc_rb_cChannelArgs = rb_define_class("TmpChannelArgs", rb_cObject); grpc_rb_cChannel = @@ -263,8 +384,14 @@ void Init_grpc_channel() { grpc_rb_channel_init_copy, 1); /* Add ruby analogues of the Channel methods. */ + rb_define_method(grpc_rb_cChannel, "connectivity_state", + grpc_rb_channel_get_connectivity_state, + -1); + rb_define_method(grpc_rb_cChannel, "watch_connectivity_state", + grpc_rb_channel_watch_connectivity_state, 4); rb_define_method(grpc_rb_cChannel, "create_call", - grpc_rb_channel_create_call, 4); + grpc_rb_channel_create_call, 6); + rb_define_method(grpc_rb_cChannel, "target", grpc_rb_channel_get_target, 0); rb_define_method(grpc_rb_cChannel, "destroy", grpc_rb_channel_destroy, 0); rb_define_alias(grpc_rb_cChannel, "close", "destroy"); @@ -279,6 +406,8 @@ void Init_grpc_channel() { ID2SYM(rb_intern(GRPC_ARG_MAX_CONCURRENT_STREAMS))); rb_define_const(grpc_rb_cChannel, "MAX_MESSAGE_LENGTH", ID2SYM(rb_intern(GRPC_ARG_MAX_MESSAGE_LENGTH))); + Init_grpc_propagate_masks(); + Init_grpc_connectivity_states(); } /* Gets the wrapped channel from the ruby wrapper */ diff --git a/src/ruby/grpc.gemspec b/src/ruby/grpc.gemspec index dd4e27df51..eb748458b9 100755 --- a/src/ruby/grpc.gemspec +++ b/src/ruby/grpc.gemspec @@ -16,12 +16,15 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 2.0.0' s.requirements << 'libgrpc ~> 0.10.0 needs to be installed' - s.files = `git ls-files`.split("\n") - s.test_files = `git ls-files -- spec/*`.split("\n") - s.executables = `git ls-files -- bin/*.rb`.split("\n").map do |f| - File.basename(f) + s.files = %w( Rakefile ) + s.files += Dir.glob('lib/**/*') + s.files += Dir.glob('ext/**/*') + s.files += Dir.glob('bin/**/*') + s.test_files = Dir.glob('spec/**/*') + %w(math noproto).each do |b| + s.executables += ["#{b}_client.rb", "#{b}_server.rb"] end - s.require_paths = ['lib'] + s.require_paths = %w( bin lib ) s.platform = Gem::Platform::RUBY s.add_dependency 'google-protobuf', '~> 3.0.0alpha.1.1' diff --git a/src/ruby/lib/grpc/generic/client_stub.rb b/src/ruby/lib/grpc/generic/client_stub.rb index 7b2c04aa22..cce718537c 100644 --- a/src/ruby/lib/grpc/generic/client_stub.rb +++ b/src/ruby/lib/grpc/generic/client_stub.rb @@ -28,16 +28,19 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. require 'grpc/generic/active_call' +require 'grpc/version' # GRPC contains the General RPC module. module GRPC + # rubocop:disable Metrics/ParameterLists + # ClientStub represents an endpoint used to send requests to GRPC servers. class ClientStub include Core::StatusCodes include Core::TimeConsts - # Default timeout is 5 seconds. - DEFAULT_TIMEOUT = 5 + # Default timeout is infinity. + DEFAULT_TIMEOUT = INFINITE_FUTURE # setup_channel is used by #initialize to constuct a channel from its # arguments. @@ -46,6 +49,7 @@ module GRPC fail(TypeError, '!Channel') unless alt_chan.is_a?(Core::Channel) return alt_chan end + kw['grpc.primary_user_agent'] = "grpc-ruby/#{VERSION}" return Core::Channel.new(host, kw) if creds.nil? fail(TypeError, '!Credentials') unless creds.is_a?(Core::Credentials) Core::Channel.new(host, kw, creds) @@ -66,6 +70,12 @@ module GRPC update_metadata end + # Allows users of the stub to modify the propagate mask. + # + # This is an advanced feature for use when making calls to another gRPC + # server whilst running in the handler of an existing one. + attr_writer :propagate_mask + # Creates a new ClientStub. # # Minimally, a stub is created with the just the host of the gRPC service @@ -89,8 +99,8 @@ module GRPC # # - :update_metadata # when present, this a func that takes a hash and returns a hash - # it can be used to update metadata, i.e, remove, change or update - # amend metadata values. + # it can be used to update metadata, i.e, remove, or amend + # metadata values. # # @param host [String] the host the stub connects to # @param q [Core::CompletionQueue] used to wait for events @@ -103,6 +113,7 @@ module GRPC channel_override: nil, timeout: nil, creds: nil, + propagate_mask: nil, update_metadata: nil, **kw) fail(TypeError, '!CompletionQueue') unless q.is_a?(Core::CompletionQueue) @@ -111,6 +122,7 @@ module GRPC @update_metadata = ClientStub.check_update_metadata(update_metadata) alt_host = kw[Core::Channel::SSL_TARGET] @host = alt_host.nil? ? host : alt_host + @propagate_mask = propagate_mask @timeout = timeout.nil? ? DEFAULT_TIMEOUT : timeout end @@ -149,11 +161,15 @@ module GRPC # @param marshal [Function] f(obj)->string that marshals requests # @param unmarshal [Function] f(string)->obj that unmarshals responses # @param timeout [Numeric] (optional) the max completion time in seconds + # @param parent [Core::Call] a prior call whose reserved metadata + # will be propagated by this one. # @param return_op [true|false] return an Operation if true # @return [Object] the response received from the server def request_response(method, req, marshal, unmarshal, timeout = nil, - return_op: false, **kw) - c = new_active_call(method, marshal, unmarshal, timeout) + return_op: false, + parent: parent, + **kw) + c = new_active_call(method, marshal, unmarshal, timeout, parent: parent) kw_with_jwt_uri = self.class.update_with_jwt_aud_uri(kw, @host, method) md = @update_metadata.nil? ? kw : @update_metadata.call(kw_with_jwt_uri) return c.request_response(req, **md) unless return_op @@ -208,10 +224,14 @@ module GRPC # @param unmarshal [Function] f(string)->obj that unmarshals responses # @param timeout [Numeric] the max completion time in seconds # @param return_op [true|false] return an Operation if true + # @param parent [Core::Call] a prior call whose reserved metadata + # will be propagated by this one. # @return [Object|Operation] the response received from the server def client_streamer(method, requests, marshal, unmarshal, timeout = nil, - return_op: false, **kw) - c = new_active_call(method, marshal, unmarshal, timeout) + return_op: false, + parent: nil, + **kw) + c = new_active_call(method, marshal, unmarshal, timeout, parent: parent) kw_with_jwt_uri = self.class.update_with_jwt_aud_uri(kw, @host, method) md = @update_metadata.nil? ? kw : @update_metadata.call(kw_with_jwt_uri) return c.client_streamer(requests, **md) unless return_op @@ -274,11 +294,16 @@ module GRPC # @param unmarshal [Function] f(string)->obj that unmarshals responses # @param timeout [Numeric] the max completion time in seconds # @param return_op [true|false]return an Operation if true + # @param parent [Core::Call] a prior call whose reserved metadata + # will be propagated by this one. # @param blk [Block] when provided, is executed for each response # @return [Enumerator|Operation|nil] as discussed above def server_streamer(method, req, marshal, unmarshal, timeout = nil, - return_op: false, **kw, &blk) - c = new_active_call(method, marshal, unmarshal, timeout) + return_op: false, + parent: nil, + **kw, + &blk) + c = new_active_call(method, marshal, unmarshal, timeout, parent: parent) kw_with_jwt_uri = self.class.update_with_jwt_aud_uri(kw, @host, method) md = @update_metadata.nil? ? kw : @update_metadata.call(kw_with_jwt_uri) return c.server_streamer(req, **md, &blk) unless return_op @@ -379,12 +404,17 @@ module GRPC # @param marshal [Function] f(obj)->string that marshals requests # @param unmarshal [Function] f(string)->obj that unmarshals responses # @param timeout [Numeric] (optional) the max completion time in seconds - # @param blk [Block] when provided, is executed for each response + # @param parent [Core::Call] a prior call whose reserved metadata + # will be propagated by this one. # @param return_op [true|false] return an Operation if true + # @param blk [Block] when provided, is executed for each response # @return [Enumerator|nil|Operation] as discussed above def bidi_streamer(method, requests, marshal, unmarshal, timeout = nil, - return_op: false, **kw, &blk) - c = new_active_call(method, marshal, unmarshal, timeout) + return_op: false, + parent: nil, + **kw, + &blk) + c = new_active_call(method, marshal, unmarshal, timeout, parent: parent) kw_with_jwt_uri = self.class.update_with_jwt_aud_uri(kw, @host, method) md = @update_metadata.nil? ? kw : @update_metadata.call(kw_with_jwt_uri) return c.bidi_streamer(requests, **md, &blk) unless return_op @@ -405,10 +435,17 @@ module GRPC # @param method [string] the method being called. # @param marshal [Function] f(obj)->string that marshals requests # @param unmarshal [Function] f(string)->obj that unmarshals responses + # @param parent [Grpc::Call] a parent call, available when calls are + # made from server # @param timeout [TimeConst] - def new_active_call(method, marshal, unmarshal, timeout = nil) + def new_active_call(method, marshal, unmarshal, timeout = nil, parent: nil) deadline = from_relative_time(timeout.nil? ? @timeout : timeout) - call = @ch.create_call(@queue, method, @host, deadline) + call = @ch.create_call(@queue, + parent, # parent call + @propagate_mask, # propagation options + method, + nil, # host use nil, + deadline) ActiveCall.new(call, @queue, marshal, unmarshal, deadline, started: false) end end diff --git a/src/ruby/spec/call_spec.rb b/src/ruby/spec/call_spec.rb index 4977c10a7e..3c5d33ffcd 100644 --- a/src/ruby/spec/call_spec.rb +++ b/src/ruby/spec/call_spec.rb @@ -137,7 +137,7 @@ describe GRPC::Core::Call do end def make_test_call - @ch.create_call(client_queue, 'dummy_method', 'dummy_host', deadline) + @ch.create_call(client_queue, nil, nil, 'dummy_method', nil, deadline) end def deadline diff --git a/src/ruby/spec/channel_spec.rb b/src/ruby/spec/channel_spec.rb index d471ff5db6..25cefcdfb7 100644 --- a/src/ruby/spec/channel_spec.rb +++ b/src/ruby/spec/channel_spec.rb @@ -117,7 +117,7 @@ describe GRPC::Core::Channel do deadline = Time.now + 5 blk = proc do - ch.create_call(cq, 'dummy_method', 'dummy_host', deadline) + ch.create_call(cq, nil, nil, 'dummy_method', nil, deadline) end expect(&blk).to_not raise_error end @@ -128,7 +128,7 @@ describe GRPC::Core::Channel do deadline = Time.now + 5 blk = proc do - ch.create_call(cq, 'dummy_method', 'dummy_host', deadline) + ch.create_call(cq, nil, nil, 'dummy_method', nil, deadline) end expect(&blk).to raise_error(RuntimeError) end diff --git a/src/ruby/spec/client_server_spec.rb b/src/ruby/spec/client_server_spec.rb index 0e85441209..2e673ff413 100644 --- a/src/ruby/spec/client_server_spec.rb +++ b/src/ruby/spec/client_server_spec.rb @@ -61,7 +61,7 @@ shared_context 'setup: tags' do end def new_client_call - @ch.create_call(@client_queue, '/method', 'foo.test.google.fr', deadline) + @ch.create_call(@client_queue, nil, nil, '/method', nil, deadline) end end @@ -69,6 +69,23 @@ shared_examples 'basic GRPC message delivery is OK' do include GRPC::Core include_context 'setup: tags' + context 'the test channel' do + it 'should have a target' do + expect(@ch.target).to be_a(String) + end + end + + context 'a client call' do + it 'should have a peer' do + expect(new_client_call.peer).to be_a(String) + end + end + + it 'calls have peer info' do + call = new_client_call + expect(call.peer).to be_a(String) + end + it 'servers receive requests from clients and can respond' do call = new_client_call server_call = nil diff --git a/src/ruby/spec/generic/active_call_spec.rb b/src/ruby/spec/generic/active_call_spec.rb index bc3bee3d44..0bf65ba2e9 100644 --- a/src/ruby/spec/generic/active_call_spec.rb +++ b/src/ruby/spec/generic/active_call_spec.rb @@ -338,7 +338,7 @@ describe GRPC::ActiveCall do end def make_test_call - @ch.create_call(@client_queue, '/method', 'a.dummy.host', deadline) + @ch.create_call(@client_queue, nil, nil, '/method', nil, deadline) end def deadline |