using Grpc.Core;
namespace Udb.Client.Generated;
public sealed record UdbCallOptions
{
public TimeSpan? Timeout { get; init; } = TimeSpan.FromSeconds(30);
public int MaxAttempts { get; init; } = 4;
public TimeSpan InitialBackoff { get; init; } = TimeSpan.FromMilliseconds(100);
public TimeSpan MaxBackoff { get; init; } = TimeSpan.FromSeconds(5);
public double BackoffMultiplier { get; init; } = 2.0;
public IReadOnlySet<StatusCode> RetryableCodes { get; init; } = new HashSet<StatusCode>
{
StatusCode.Unavailable,
StatusCode.DeadlineExceeded,
StatusCode.ResourceExhausted,
};
}
public sealed class UdbRpcException : Exception
{
public StatusCode Code { get; }
public string RpcPath { get; }
public byte[]? ErrorDetail { get; }
public UdbRpcException(string rpcPath, RpcException inner)
: base($"udb: RPC {rpcPath} failed with {inner.StatusCode}: {inner.Status.Detail}", inner)
{
RpcPath = rpcPath;
Code = inner.StatusCode;
ErrorDetail = ExtractDetail(inner);
}
private static byte[]? ExtractDetail(RpcException ex)
{
foreach (var entry in ex.Trailers)
{
if (entry.IsBinary &&
string.Equals(entry.Key, "udb-error-detail-bin", StringComparison.OrdinalIgnoreCase))
{
return entry.ValueBytes;
}
}
return null;
}
}
public abstract class GeneratedServiceBase
{
private static readonly Random Jitter = new();
protected Func<Metadata> HeadersFactory { get; }
protected UdbCallOptions Options { get; }
protected GeneratedServiceBase(Func<Metadata> headersFactory, UdbCallOptions? options)
{
HeadersFactory = headersFactory ?? throw new ArgumentNullException(nameof(headersFactory));
Options = options ?? new UdbCallOptions();
}
private CallOptions MakeCallOptions(TimeSpan? deadline, CancellationToken ct)
{
var effective = deadline ?? Options.Timeout;
DateTime? when = effective.HasValue ? DateTime.UtcNow.Add(effective.Value) : null;
return new CallOptions(HeadersFactory(), when, ct);
}
protected async Task<dynamic> InvokeUnaryAsync(
string rpcPath,
Func<CallOptions, object> call,
TimeSpan? deadline,
CancellationToken ct)
{
var backoff = Options.InitialBackoff;
for (var attempt = 1; ; attempt++)
{
try
{
var co = MakeCallOptions(deadline, ct);
dynamic rpc = call(co);
using (rpc)
{
return await rpc.ResponseAsync.ConfigureAwait(false);
}
}
catch (RpcException ex) when (
attempt < Options.MaxAttempts &&
Options.RetryableCodes.Contains(ex.StatusCode) &&
!ct.IsCancellationRequested)
{
await Task.Delay(NextBackoff(ref backoff), ct).ConfigureAwait(false);
}
catch (RpcException ex)
{
throw new UdbRpcException(rpcPath, ex);
}
}
}
protected dynamic InvokeStreaming(
string rpcPath,
Func<CallOptions, object> call,
TimeSpan? deadline,
CancellationToken ct)
{
try
{
return call(MakeCallOptions(deadline, ct));
}
catch (RpcException ex)
{
throw new UdbRpcException(rpcPath, ex);
}
}
public static UdbRpcException MapStreamError(string rpcPath, RpcException ex)
=> new(rpcPath, ex);
private TimeSpan NextBackoff(ref TimeSpan backoff)
{
var current = backoff;
var nextMs = Math.Min(
backoff.TotalMilliseconds * Options.BackoffMultiplier,
Options.MaxBackoff.TotalMilliseconds);
backoff = TimeSpan.FromMilliseconds(nextMs);
var jittered = current.TotalMilliseconds * (0.5 + Jitter.NextDouble() * 0.5);
return TimeSpan.FromMilliseconds(jittered);
}
}