Skip to content
Merged
25 changes: 18 additions & 7 deletions Microsoft.Azure.Cosmos/src/HttpClient/CosmosHttpClientCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ private async Task<HttpResponseMessage> SendHttpHelperAsync(
return responseMessage;
}

bool isOutOfRetries = CosmosHttpClientCore.IsOutOfRetries(timeoutPolicy, startDateTimeUtc, timeoutEnumerator);
bool isOutOfRetries = CosmosHttpClientCore.IsOutOfRetries(timeoutEnumerator);
if (isOutOfRetries)
{
return responseMessage;
Expand All @@ -402,7 +402,7 @@ private async Task<HttpResponseMessage> SendHttpHelperAsync(
datum.RecordHttpException(requestMessage, e, resourceType, requestStartTime);
trace = datum.Trace;
}
bool isOutOfRetries = CosmosHttpClientCore.IsOutOfRetries(timeoutPolicy, startDateTimeUtc, timeoutEnumerator);
bool isOutOfRetries = CosmosHttpClientCore.IsOutOfRetries(timeoutEnumerator);

switch (e)
{
Expand All @@ -415,7 +415,7 @@ private async Task<HttpResponseMessage> SendHttpHelperAsync(

// Convert OperationCanceledException to 408 when the HTTP client throws it. This makes it clear that the
// the request timed out and was not user canceled operation.
if (isOutOfRetries || !timeoutPolicy.IsSafeToRetry(requestMessage.Method))
if (isOutOfRetries || !CosmosHttpClientCore.IsSafeToRetry(documentServiceRequest))
{
// throw current exception (caught in transport handler)
string message =
Expand All @@ -440,14 +440,14 @@ private async Task<HttpResponseMessage> SendHttpHelperAsync(

break;
case WebException webException:
if (isOutOfRetries || (!timeoutPolicy.IsSafeToRetry(requestMessage.Method) && !WebExceptionUtility.IsWebExceptionRetriable(webException)))
if (isOutOfRetries || (!CosmosHttpClientCore.IsSafeToRetry(documentServiceRequest) && !WebExceptionUtility.IsWebExceptionRetriable(webException)))
{
throw;
}

break;
case HttpRequestException httpRequestException:
if (isOutOfRetries || !timeoutPolicy.IsSafeToRetry(requestMessage.Method))
if (isOutOfRetries || !CosmosHttpClientCore.IsSafeToRetry(documentServiceRequest))
{
throw;
}
Expand Down Expand Up @@ -493,13 +493,24 @@ private async Task<HttpResponseMessage> SendHttpHelperAsync(
}

private static bool IsOutOfRetries(
HttpTimeoutPolicy timeoutPolicy,
DateTime startDateTimeUtc,
IEnumerator<(TimeSpan requestTimeout, TimeSpan delayForNextRequest)> timeoutEnumerator)
{
return !timeoutEnumerator.MoveNext(); // No more retries are configured
}

private static bool IsSafeToRetry(DocumentServiceRequest documentServiceRequest)
{
// Three scenarios are safely retriable:
// 1) If request is null since they are originated from GetAsync calls
// 2) If request is read-only
// 3) If request is an address request.
if (documentServiceRequest == null)
{
return true;
}
return documentServiceRequest.IsReadOnlyRequest || documentServiceRequest.ResourceType == ResourceType.Address;
}

private async Task<HttpResponseMessage> ExecuteHttpHelperAsync(
HttpRequestMessage requestMessage,
ResourceType resourceType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ internal abstract class HttpTimeoutPolicy
public abstract string TimeoutPolicyName { get; }
public abstract int TotalRetryCount { get; }
public abstract IEnumerator<(TimeSpan requestTimeout, TimeSpan delayForNextRequest)> GetTimeoutEnumerator();
public abstract bool IsSafeToRetry(HttpMethod httpMethod);

public abstract bool ShouldRetryBasedOnResponse(HttpMethod requestHttpMethod, HttpResponseMessage responseMessage);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,6 @@ private HttpTimeoutPolicyControlPlaneRead()
return this.TimeoutsAndDelays.GetEnumerator();
}

// This is for control plane reads which should always be safe to retry on.
public override bool IsSafeToRetry(HttpMethod httpMethod)
{
return true;
}

public override bool ShouldRetryBasedOnResponse(HttpMethod requestHttpMethod, HttpResponseMessage responseMessage)
{
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,6 @@ private HttpTimeoutPolicyControlPlaneRetriableHotPath(bool shouldThrow503OnTimeo
return this.TimeoutsAndDelays.GetEnumerator();
}

// The hot path should always be safe to retires since it should be retrieving meta data
// information that is not idempotent.
public override bool IsSafeToRetry(HttpMethod httpMethod)
{
return true;
}

public override bool ShouldRetryBasedOnResponse(HttpMethod requestHttpMethod, HttpResponseMessage responseMessage)
{
if (responseMessage == null)
Expand All @@ -55,11 +48,6 @@ public override bool ShouldRetryBasedOnResponse(HttpMethod requestHttpMethod, Ht
return false;
}

if (!this.IsSafeToRetry(requestHttpMethod))
{
return false;
}

return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,6 @@ private HttpTimeoutPolicyDefault(bool shouldThrow503OnTimeout)
return this.TimeoutsAndDelays.GetEnumerator();
}

// Assume that it is not safe to retry unless it is a get method.
// Create and other operations could have succeeded even though a timeout occurred.
public override bool IsSafeToRetry(HttpMethod httpMethod)
{
return httpMethod == HttpMethod.Get;
}

public override bool ShouldRetryBasedOnResponse(HttpMethod requestHttpMethod, HttpResponseMessage responseMessage)
{
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,6 @@ private HttpTimeoutPolicyForPartitionFailover(bool shouldThrow503OnTimeout)
return this.TimeoutsAndDelays.GetEnumerator();
}

// Assume that it is not safe to retry unless it is a get method.
// Create and other operations could have succeeded even though a timeout occurred.
public override bool IsSafeToRetry(HttpMethod httpMethod)
{
return httpMethod == HttpMethod.Get;
}

public override bool ShouldRetryBasedOnResponse(HttpMethod requestHttpMethod, HttpResponseMessage responseMessage)
{
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@ private HttpTimeoutPolicyForThinClient(
return this.TimeoutsAndDelays.GetEnumerator();
}

public override bool IsSafeToRetry(HttpMethod httpMethod)
{
return this.shouldRetry;
}

public override bool ShouldRetryBasedOnResponse(HttpMethod requestHttpMethod, HttpResponseMessage responseMessage)
{
if (responseMessage == null)
Expand All @@ -56,7 +51,7 @@ public override bool ShouldRetryBasedOnResponse(HttpMethod requestHttpMethod, Ht
return false;
}

if (!this.IsSafeToRetry(requestHttpMethod))
if (!this.shouldRetry)
{
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@ private HttpTimeoutPolicyNoRetry()
return this.TimeoutsAndDelays.GetEnumerator();
}

// Always Unsafe to retry
public override bool IsSafeToRetry(HttpMethod httpMethod)
{
return false;
}

public override bool ShouldRetryBasedOnResponse(HttpMethod requestHttpMethod, HttpResponseMessage responseMessage)
{
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ async Task<HttpResponseMessage> sendFunc(HttpRequestMessage request, Cancellatio
resourceType: ResourceType.Collection,
timeoutPolicy: currentTimeoutPolicy.Key,
clientSideRequestStatistics: new ClientSideRequestStatisticsTraceDatum(DateTime.UtcNow, trace),
cancellationToken: default);
cancellationToken: default,
documentServiceRequest: CreateDocumentServiceRequestByOperation(ResourceType.Collection, OperationType.Read));

Assert.AreEqual(HttpStatusCode.OK, responseMessage.StatusCode);
}
Expand Down Expand Up @@ -266,7 +267,8 @@ Task<HttpResponseMessage> sendFunc(HttpRequestMessage request, CancellationToken
resourceType: ResourceType.Collection,
timeoutPolicy: HttpTimeoutPolicyDefault.Instance,
clientSideRequestStatistics: new ClientSideRequestStatisticsTraceDatum(DateTime.UtcNow, trace),
cancellationToken: default);
cancellationToken: default,
documentServiceRequest: CreateDocumentServiceRequestByOperation(ResourceType.Collection, OperationType.Read));
}
}
catch (Exception)
Expand All @@ -281,7 +283,7 @@ Task<HttpResponseMessage> sendFunc(HttpRequestMessage request, CancellationToken
public async Task HttpTimeoutThrow503TestAsync()
{

async Task TestScenarioAsync(HttpMethod method, ResourceType resourceType, HttpTimeoutPolicy timeoutPolicy, Type expectedException, int expectedNumberOfRetrys)
async Task TestScenarioAsync(HttpMethod method, ResourceType resourceType, OperationType operationType, HttpTimeoutPolicy timeoutPolicy, Type expectedException, int expectedNumberOfRetrys)
{
int count = 0;
Task<HttpResponseMessage> sendFunc(HttpRequestMessage request, CancellationToken cancellationToken)
Expand All @@ -306,7 +308,8 @@ Task<HttpResponseMessage> sendFunc(HttpRequestMessage request, CancellationToken
resourceType: resourceType,
timeoutPolicy: timeoutPolicy,
clientSideRequestStatistics: new ClientSideRequestStatisticsTraceDatum(DateTime.UtcNow, trace),
cancellationToken: default);
cancellationToken: default,
documentServiceRequest: CreateDocumentServiceRequestByOperation(resourceType, operationType));
}
}
catch (Exception e)
Expand All @@ -328,19 +331,19 @@ Task<HttpResponseMessage> sendFunc(HttpRequestMessage request, CancellationToken
}

//Data plane read
await TestScenarioAsync(HttpMethod.Get, ResourceType.Document, HttpTimeoutPolicyDefault.InstanceShouldThrow503OnTimeout, typeof(CosmosException), 3);
await TestScenarioAsync(HttpMethod.Get, ResourceType.Document, OperationType.Read, HttpTimeoutPolicyDefault.InstanceShouldThrow503OnTimeout, typeof(CosmosException), 3);

//Data plane write (Should throw a 408 OperationCanceledException rather than a 503)
await TestScenarioAsync(HttpMethod.Post, ResourceType.Document, HttpTimeoutPolicyDefault.Instance, typeof(TaskCanceledException), 1);
await TestScenarioAsync(HttpMethod.Post, ResourceType.Document, OperationType.Upsert, HttpTimeoutPolicyDefault.Instance, typeof(TaskCanceledException), 1);

//Meta data read
await TestScenarioAsync(HttpMethod.Get, ResourceType.Database, HttpTimeoutPolicyDefault.InstanceShouldThrow503OnTimeout, typeof(CosmosException), 3);
await TestScenarioAsync(HttpMethod.Get, ResourceType.Database, OperationType.Read, HttpTimeoutPolicyDefault.InstanceShouldThrow503OnTimeout, typeof(CosmosException), 3);

//Query plan read (note all query plan operations are reads).
await TestScenarioAsync(HttpMethod.Get, ResourceType.Document, HttpTimeoutPolicyDefault.InstanceShouldThrow503OnTimeout, typeof(CosmosException), 3);
await TestScenarioAsync(HttpMethod.Get, ResourceType.Document, OperationType.Read, HttpTimeoutPolicyDefault.InstanceShouldThrow503OnTimeout, typeof(CosmosException), 3);

//Metadata Write (Should throw a 408 OperationCanceledException rather than a 503)
await TestScenarioAsync(HttpMethod.Post, ResourceType.Document, HttpTimeoutPolicyDefault.Instance, typeof(TaskCanceledException), 1);
await TestScenarioAsync(HttpMethod.Post, ResourceType.Document, OperationType.Upsert, HttpTimeoutPolicyDefault.Instance, typeof(TaskCanceledException), 1);
}

[TestMethod]
Expand Down Expand Up @@ -433,7 +436,8 @@ async Task<HttpResponseMessage> sendFunc(HttpRequestMessage request, Cancellatio
resourceType: ResourceType.Document,
timeoutPolicy: HttpTimeoutPolicyControlPlaneRetriableHotPath.Instance,
clientSideRequestStatistics: new ClientSideRequestStatisticsTraceDatum(DateTime.UtcNow, trace),
cancellationToken: default);
cancellationToken: default,
documentServiceRequest: documentServiceRequest);

Assert.AreEqual(HttpStatusCode.OK, responseMessage.StatusCode);
}
Expand Down Expand Up @@ -492,7 +496,7 @@ public void CreateHttpClientHandlerCreatesCorrectValueType()
public async Task HttpTimeoutPolicyForThinClientOn503TestAsync()
{

async Task TestScenarioAsync(HttpMethod method, ResourceType resourceType, HttpTimeoutPolicy timeoutPolicy, Type expectedException, int expectedNumberOfRetrys)
async Task TestScenarioAsync(HttpMethod method, ResourceType resourceType, OperationType operationType, HttpTimeoutPolicy timeoutPolicy, Type expectedException, int expectedNumberOfRetrys)
{
int count = 0;
Task<HttpResponseMessage> sendFunc(HttpRequestMessage request, CancellationToken cancellationToken)
Expand All @@ -517,7 +521,8 @@ Task<HttpResponseMessage> sendFunc(HttpRequestMessage request, CancellationToken
resourceType: resourceType,
timeoutPolicy: timeoutPolicy,
clientSideRequestStatistics: new ClientSideRequestStatisticsTraceDatum(DateTime.UtcNow, trace),
cancellationToken: default);
cancellationToken: default,
documentServiceRequest: CreateDocumentServiceRequestByOperation(resourceType, operationType));
}
}
catch (Exception e)
Expand All @@ -542,6 +547,7 @@ Task<HttpResponseMessage> sendFunc(HttpRequestMessage request, CancellationToken
await TestScenarioAsync(
method: HttpMethod.Get,
resourceType: ResourceType.Document,
operationType: OperationType.Read,
timeoutPolicy: HttpTimeoutPolicy.GetTimeoutPolicy(
documentServiceRequest: CosmosHttpClientCoreTests.CreateDocumentServiceRequestByOperation(ResourceType.Document, OperationType.Read),
isPartitionLevelFailoverEnabled: false,
Expand All @@ -553,6 +559,7 @@ await TestScenarioAsync(
await TestScenarioAsync(
method: HttpMethod.Get,
resourceType: ResourceType.Document,
operationType: OperationType.Read,
timeoutPolicy: HttpTimeoutPolicy.GetTimeoutPolicy(
documentServiceRequest: CosmosHttpClientCoreTests.CreateDocumentServiceRequestByOperation(ResourceType.Document, OperationType.Query),
isPartitionLevelFailoverEnabled: false,
Expand All @@ -564,6 +571,7 @@ await TestScenarioAsync(
await TestScenarioAsync(
method: HttpMethod.Post,
resourceType: ResourceType.Document,
operationType: OperationType.Upsert,
timeoutPolicy: HttpTimeoutPolicy.GetTimeoutPolicy(
documentServiceRequest: CosmosHttpClientCoreTests.CreateDocumentServiceRequestByOperation(ResourceType.Document, OperationType.Create),
isPartitionLevelFailoverEnabled: false,
Expand All @@ -575,6 +583,7 @@ await TestScenarioAsync(
await TestScenarioAsync(
method: HttpMethod.Get,
resourceType: ResourceType.Database,
operationType: OperationType.Read,
timeoutPolicy: HttpTimeoutPolicy.GetTimeoutPolicy(
documentServiceRequest: CosmosHttpClientCoreTests.CreateDocumentServiceRequestByOperation(ResourceType.Database, OperationType.Read),
isPartitionLevelFailoverEnabled: false,
Expand All @@ -583,6 +592,7 @@ await TestScenarioAsync(
expectedNumberOfRetrys: 3);
}


private static DocumentServiceRequest CreateDocumentServiceRequestByOperation(
ResourceType resourceType,
OperationType operationType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -889,15 +889,22 @@ public async Task GatewayStatsDurationTest()
DocumentClientEventSource.Instance);

using (ITrace trace = Tracing.Trace.GetRootTrace(nameof(GatewayStatsDurationTest)))
{

{
Tracing.TraceData.ClientSideRequestStatisticsTraceDatum clientSideRequestStatistics = new Tracing.TraceData.ClientSideRequestStatisticsTraceDatum(DateTime.UtcNow, trace);

await cosmosHttpClient.SendHttpAsync(() => new ValueTask<HttpRequestMessage>(new HttpRequestMessage(HttpMethod.Get, "http://someuri.com")),
ResourceType.Document,
HttpTimeoutPolicyDefault.InstanceShouldThrow503OnTimeout,
clientSideRequestStatistics,
CancellationToken.None);
CancellationToken.None,
documentServiceRequest: new DocumentServiceRequest(
OperationType.Read,
ResourceType.Document,
$"dbs/dummy_db_id/colls/dummy_ct_id",
body: null,
AuthorizationTokenType.PrimaryMasterKey,
headers: null));

Assert.AreEqual(clientSideRequestStatistics.HttpResponseStatisticsList.Count, 2);
// The duration is calculated using date times which can cause the duration to be slightly off. This allows for up to 15 Ms of variance.
Expand Down
Loading