/// <summary> /// Returns a new <see cref="CallSettings"/> with the given the expiration, merged with the (optional) original settings specified by <paramref name="settings"/>. /// </summary> /// <remarks> /// If <paramref name="settings"/> is non-null and has retry-based timing, the returned settings will have the same timing, but with the specified /// expiration. Otherwise, the returned settings will have a simple expiration. /// </remarks> /// <param name="settings">Existing settings. May be null, meaning there are currently no settings.</param> /// <param name="expiration">Expiration to use in the returned settings, possibly as part of a retry. Must not be null.</param> /// <returns></returns> public static CallSettings WithExpiration(this CallSettings settings, Expiration expiration) { GaxPreconditions.CheckNotNull(expiration, nameof(expiration)); var retry = settings?.Timing?.Retry?.WithTotalExpiration(expiration); var timing = retry == null?CallTiming.FromExpiration(expiration) : CallTiming.FromRetry(retry); return(settings.WithCallTiming(timing)); }
/// <summary> /// Returns a new <see cref="CallSettings"/> with the specified call timing, /// merged with the (optional) original settings specified by <paramref name="settings"/>. /// </summary> /// <param name="settings">Original settings. May be null, in which case the returned settings /// will only contain call timing.</param> /// <param name="timing">Call timing for the new call settings. /// This may be null, in which case any call timing in <paramref name="settings"/> are /// not present in the new call settings. If both this and <paramref name="settings"/> are null, /// the return value is null.</param> /// <returns>A new set of call settings, or null if both parameters are null.</returns> public static CallSettings WithCallTiming( this CallSettings settings, CallTiming timing) => settings == null ? CallSettings.FromCallTiming(timing) : new CallSettings(settings.CancellationToken, settings.Credentials, timing, settings.HeaderMutation, settings.WriteOptions, settings.PropagationToken);
/// <summary> /// Constructs an instance with the specified settings. /// </summary> /// <param name="cancellationToken">Cancellation token that can be used for cancelling the call.</param> /// <param name="credentials">Credentials to use for the call.</param> /// <param name="timing"><see cref="CallTiming"/> to use, or null for default retry/expiration behavior.</param> /// <param name="headerMutation">Action to modify the headers to send at the beginning of the call.</param> /// <param name="writeOptions"><see cref="global::Grpc.Core.WriteOptions"/> that will be used for the call.</param> /// <param name="propagationToken"><see cref="ContextPropagationToken"/> for propagating settings from a parent call.</param> public CallSettings( CancellationToken?cancellationToken, CallCredentials credentials, CallTiming timing, Action <Metadata> headerMutation, WriteOptions writeOptions, ContextPropagationToken propagationToken) : this(cancellationToken, credentials, timing, headerMutation, writeOptions, propagationToken, null, null) { }
/// <summary> /// Returns a new <see cref="CallSettings"/> with the specified call timing, /// merged with the (optional) original settings specified by <paramref name="settings"/>. /// </summary> /// <param name="settings">Original settings. May be null, in which case the returned settings /// will only contain call timing.</param> /// <param name="timing">Call timing for the new call settings. /// This may be null, in which case any call timing in <paramref name="settings"/> are /// not present in the new call settings. If both this and <paramref name="settings"/> are null, /// the return value is null.</param> /// <returns>A new set of call settings, or null if both parameters are null.</returns> public static CallSettings WithCallTiming( this CallSettings settings, CallTiming timing) => settings == null ? CallSettings.FromCallTiming(timing) : new CallSettings(settings.CancellationToken, settings.Credentials, timing, settings.HeaderMutation, settings.WriteOptions, settings.PropagationToken, settings.ResponseMetadataHandler, settings.TrailingMetadataHandler);
/// <summary> /// Calculate a deadline from a <see cref="CallTiming"/> and a <see cref="IClock"/>. /// </summary> /// <param name="callTiming"><see cref="CallTiming"/>, may be null.</param> /// <param name="clock"><see cref="IClock"/> to use for deadline calculation.</param> /// <returns>The calculated absolute deadline, or null is no deadline should be used.</returns> /// <exception cref="InvalidOperationException"> /// The <paramref name="callTiming"/> uses retry. Only a simple expiration is valid /// for calculating a deadline. /// </exception> internal static DateTime?CalculateDeadline(this CallTiming callTiming, IClock clock) { if (callTiming == null) { return(null); } if (callTiming.Type != CallTimingType.Expiration) { throw new InvalidOperationException("Cannot calculate deadline from retry."); } return(callTiming.Expiration.CalculateDeadline(clock)); }
/// <summary> /// Constructs an instance with the specified settings. /// </summary> /// <param name="cancellationToken">Cancellation token that can be used for cancelling the call.</param> /// <param name="credentials">Credentials to use for the call.</param> /// <param name="timing"><see cref="CallTiming"/> to use, or null for default retry/expiration behavior.</param> /// <param name="headerMutation">Action to modify the headers to send at the beginning of the call.</param> /// <param name="writeOptions"><see cref="global::Grpc.Core.WriteOptions"/> that will be used for the call.</param> /// <param name="propagationToken"><see cref="ContextPropagationToken"/> for propagating settings from a parent call.</param> public CallSettings( CancellationToken?cancellationToken, CallCredentials credentials, CallTiming timing, Action <Metadata> headerMutation, WriteOptions writeOptions, ContextPropagationToken propagationToken) { CancellationToken = cancellationToken; Credentials = credentials; Timing = timing; HeaderMutation = headerMutation; WriteOptions = writeOptions; PropagationToken = propagationToken; }
/// <summary> /// Constructs an instance with the specified settings. /// </summary> /// <param name="cancellationToken">Cancellation token that can be used for cancelling the call.</param> /// <param name="credentials">Credentials to use for the call.</param> /// <param name="timing"><see cref="CallTiming"/> to use, or null for default retry/expiration behavior.</param> /// <param name="headerMutation">Action to modify the headers to send at the beginning of the call.</param> /// <param name="writeOptions"><see cref="global::Grpc.Core.WriteOptions"/> that will be used for the call.</param> /// <param name="propagationToken"><see cref="ContextPropagationToken"/> for propagating settings from a parent call.</param> public CallSettings( CancellationToken? cancellationToken, CallCredentials credentials, CallTiming timing, Action<Metadata> headerMutation, WriteOptions writeOptions, ContextPropagationToken propagationToken) { CancellationToken = cancellationToken; Credentials = credentials; Timing = timing; HeaderMutation = headerMutation; WriteOptions = writeOptions; PropagationToken = propagationToken; }
// By design, the code is mostly duplicated between the async and sync calls. // Async retry internal static Func <TRequest, CallSettings, Task <TResponse> > WithRetry <TRequest, TResponse>( this Func <TRequest, CallSettings, Task <TResponse> > fn, IClock clock, IScheduler scheduler, Func <TResponse, Task> postResponse) => async(request, callSettings) => { RetrySettings retrySettings = callSettings.Timing?.Retry; if (retrySettings == null) { return(await fn(request, callSettings).ConfigureAwait(false)); } DateTime?overallDeadline = retrySettings.TotalExpiration.CalculateDeadline(clock); TimeSpan retryDelay = retrySettings.RetryBackoff.Delay; TimeSpan callTimeout = retrySettings.TimeoutBackoff.Delay; while (true) { DateTime attemptDeadline = clock.GetCurrentDateTimeUtc() + callTimeout; // Note: this handles a null total deadline due to "<" returning false if overallDeadline is null. DateTime combinedDeadline = overallDeadline < attemptDeadline ? overallDeadline.Value : attemptDeadline; CallSettings attemptCallSettings = callSettings.WithCallTiming(CallTiming.FromDeadline(combinedDeadline)); try { var response = await fn(request, attemptCallSettings).ConfigureAwait(false); if (postResponse != null) { await postResponse(response).ConfigureAwait(false); } return(response); } catch (RpcException e) when(retrySettings.RetryFilter(e)) { TimeSpan actualDelay = retrySettings.DelayJitter.GetDelay(retryDelay); DateTime expectedRetryTime = clock.GetCurrentDateTimeUtc() + actualDelay; if (expectedRetryTime > overallDeadline) { throw; } await scheduler.Delay(actualDelay, callSettings.CancellationToken.GetValueOrDefault()).ConfigureAwait(false); retryDelay = retrySettings.RetryBackoff.NextDelay(retryDelay); callTimeout = retrySettings.TimeoutBackoff.NextDelay(callTimeout); } } };
/// <summary> /// Constructs an instance with the specified settings. /// </summary> /// <param name="cancellationToken">Cancellation token that can be used for cancelling the call.</param> /// <param name="credentials">Credentials to use for the call.</param> /// <param name="timing"><see cref="CallTiming"/> to use, or null for default retry/expiration behavior.</param> /// <param name="headerMutation">Action to modify the headers to send at the beginning of the call.</param> /// <param name="writeOptions"><see cref="global::Grpc.Core.WriteOptions"/> that will be used for the call.</param> /// <param name="propagationToken"><see cref="ContextPropagationToken"/> for propagating settings from a parent call.</param> /// <param name="responseMetadataHandler">Action to invoke when response metadata is received.</param> /// <param name="trailingMetadataHandler">Action to invoke when trailing metadata is received.</param> public CallSettings( CancellationToken?cancellationToken, CallCredentials credentials, CallTiming timing, Action <Metadata> headerMutation, WriteOptions writeOptions, ContextPropagationToken propagationToken, Action <Metadata> responseMetadataHandler, Action <Metadata> trailingMetadataHandler) { CancellationToken = cancellationToken; Credentials = credentials; Timing = timing; HeaderMutation = headerMutation; WriteOptions = writeOptions; PropagationToken = propagationToken; ResponseMetadataHandler = responseMetadataHandler; TrailingMetadataHandler = trailingMetadataHandler; }
/// <summary> /// Returns a <see cref="CallSettings"/> which will have an effective deadline of at least <paramref name="deadline"/>. /// If <paramref name="settings"/> already observes an earlier deadline (with respect to <paramref name="clock"/>), /// or if <paramref name="deadline"/> is null, the original settings will be returned. /// </summary> /// <param name="settings">Existing settings. May be null, meaning there are currently no settings.</param> /// <param name="deadline">Deadline to enforce. May be null, meaning there is no deadline to enforce.</param> /// <param name="clock">The clock to use when computing deadlines. Must not be null.</param> /// <returns>The call settings to use to observe the given deadline.</returns> public static CallSettings WithEarlierDeadline(this CallSettings settings, DateTime?deadline, IClock clock) { GaxPreconditions.CheckNotNull(clock, nameof(clock)); // No deadline? Return what we already have. if (deadline == null) { return(settings); } // No settings, or no timing in the settings? Create a simple expiration. if (settings == null || settings.Timing == null) { // WithCallTiming (above) is an extension method - it's fine for settings to be null. return(settings.WithCallTiming(CallTiming.FromDeadline(deadline.Value))); } // Okay, we have settings with a genuine timing component and a new deadline. // We may still return the existing settings, if the new deadline is later than the existing // one in the settings. But if the new deadline is earlier, we need to build new settings to accommodate // it. var timing = settings.Timing; switch (timing.Type) { // For simple expirations, we can just replace one deadline for another simple one, // if necessary. case CallTimingType.Expiration: return(timing.Expiration.CalculateDeadline(clock) < deadline.Value ? settings : settings.WithCallTiming(CallTiming.FromDeadline(deadline.Value))); // For retries, we may need to create a new RetrySettings with all the same other aspects, // but a new deadline. case CallTimingType.Retry: if (timing.Retry.TotalExpiration.CalculateDeadline(clock) < deadline.Value) { return(settings); } var newTiming = CallTiming.FromRetry(timing.Retry.WithTotalExpiration(Expiration.FromDeadline(deadline.Value))); return(settings.WithCallTiming(newTiming)); default: throw new InvalidOperationException("Invalid call timing type"); } }
/// <summary> /// Creates a <see cref="CallSettings"/> for the specified call timing, or returns null /// if <paramref name="timing"/> is null. /// </summary> /// <param name="timing">The call timing for the new settings.</param> /// <returns>A new instance or null if <paramref name="timing"/> is null..</returns> public static CallSettings FromCallTiming(CallTiming timing) => timing == null ? null : new CallSettings(null, null, timing, null, null, null);