Skip to content

Commit 7bc77a7

Browse files
authored
Merge pull request #509 from microsoft/release/update/250502071537
Minor fix for CancellationToken handling between retries
2 parents 4ed7e5b + a73e643 commit 7bc77a7

File tree

5 files changed

+103
-8
lines changed

5 files changed

+103
-8
lines changed

src/GeneralTools/DataverseClient/Client/ConnectionService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2383,7 +2383,7 @@ internal async Task<HttpResponseMessage> Command_WebExecuteAsync(string queryStr
23832383
retry = ShouldRetryWebAPI(ex, retryCount, maxRetryCount, retryPauseTime, out isThrottled);
23842384
if (retry)
23852385
{
2386-
retryCount = await Utilities.RetryRequest(null, requestTrackingId, TimeSpan.Zero, logDt, logEntry, SessionTrackingId, disableConnectionLocking, _retryPauseTimeRunning, ex, errorStringCheck, retryCount, isThrottled, webUriReq: $"{method} {queryString}").ConfigureAwait(false);
2386+
retryCount = await Utilities.RetryRequest(null, requestTrackingId, TimeSpan.Zero, logDt, logEntry, SessionTrackingId, disableConnectionLocking, _retryPauseTimeRunning, ex, errorStringCheck, retryCount, isThrottled, webUriReq: $"{method} {queryString}", cancellationToken).ConfigureAwait(false);
23872387
}
23882388
else
23892389
{

src/GeneralTools/DataverseClient/Client/ServiceClient.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1876,17 +1876,23 @@ internal async Task<OrganizationResponse> Command_ExecuteAsyncImpl(OrganizationR
18761876
catch (Exception ex)
18771877
{
18781878
bool isThrottled = false;
1879-
retry = ShouldRetry(req, ex, retryCount, out isThrottled);
1879+
retry = ShouldRetry(req, ex, retryCount, out isThrottled) || !cancellationToken.IsCancellationRequested;
18801880
if (retry)
18811881
{
1882-
retryCount = await Utilities.RetryRequest(req, requestTrackingId, LockWait, logDt, _logEntry, SessionTrackingId, _disableConnectionLocking, _retryPauseTimeRunning, ex, errorStringCheck, retryCount, isThrottled).ConfigureAwait(false);
1882+
retryCount = await Utilities.RetryRequest(req, requestTrackingId, LockWait, logDt, _logEntry, SessionTrackingId, _disableConnectionLocking, _retryPauseTimeRunning, ex, errorStringCheck, retryCount, isThrottled, cancellationToken: cancellationToken).ConfigureAwait(false);
18831883
}
18841884
else
18851885
{
18861886
_logEntry.LogRetry(retryCount, req, _retryPauseTimeRunning, true, isThrottled: isThrottled);
18871887
_logEntry.LogException(req, ex, errorStringCheck);
18881888
//keep it in end so that LastError could be a better message.
18891889
_logEntry.LogFailure(req, requestTrackingId, SessionTrackingId, _disableConnectionLocking, LockWait, logDt, ex, errorStringCheck, true);
1890+
1891+
// Callers which cancel should expect to handle a OperationCanceledException
1892+
if (ex is OperationCanceledException)
1893+
throw;
1894+
else
1895+
cancellationToken.ThrowIfCancellationRequested();
18901896
}
18911897
resp = null;
18921898
}
@@ -2022,6 +2028,8 @@ private bool ShouldRetry(OrganizationRequest req, Exception ex, int retryCount,
20222028
isThrottlingRetry = false;
20232029
if (retryCount >= _configuration.Value.MaxRetryCount)
20242030
return false;
2031+
else if (ex is OperationCanceledException)
2032+
return false;
20252033
else if (((string.Equals(req.RequestName.ToLower(), "retrieve"))
20262034
&& ((Utilities.ShouldAutoRetryRetrieveByEntityName(((Microsoft.Xrm.Sdk.EntityReference)req.Parameters["Target"]).LogicalName))))
20272035
|| (string.Equals(req.RequestName.ToLower(), "retrievemultiple")
@@ -2047,10 +2055,10 @@ private bool ShouldRetry(OrganizationRequest req, Exception ex, int retryCount,
20472055
OrgEx.Detail.ErrorCode == ErrorCodes.ThrottlingTimeExceededError ||
20482056
OrgEx.Detail.ErrorCode == ErrorCodes.ThrottlingConcurrencyLimitExceededError)
20492057
{
2050-
// Use Retry-After delay when specified
2058+
// Use Retry-After delay when specified
20512059
if (OrgEx.Detail.ErrorDetails.TryGetValue("Retry-After", out var retryAfter) && retryAfter is TimeSpan retryAsTimeSpan)
2052-
{
2053-
_retryPauseTimeRunning = retryAsTimeSpan;
2060+
{
2061+
_retryPauseTimeRunning = retryAsTimeSpan;
20542062
}
20552063
else
20562064
{

src/GeneralTools/DataverseClient/Client/Utils/Utils.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using System.Net.Http;
2020
using System.Reflection;
2121
using System.Text;
22+
using System.Threading;
2223
using System.Threading.Tasks;
2324
#endregion
2425

@@ -445,14 +446,20 @@ internal static string ConstructWebApiRequestUrl(OrganizationRequest request, Ht
445446
/// <param name="retryCount">retryCount</param>
446447
/// <param name="isThrottled">when set indicated this was caused by a Throttle</param>
447448
/// <param name="webUriReq"></param>
449+
/// <param name="cancellationToken"></param>
448450
internal static async Task<int> RetryRequest(OrganizationRequest req, Guid requestTrackingId, TimeSpan LockWait, Stopwatch logDt,
449451
DataverseTraceLogger logEntry, Guid? sessionTrackingId, bool disableConnectionLocking, TimeSpan retryPauseTimeRunning,
450-
Exception ex, string errorStringCheck, int retryCount, bool isThrottled, string webUriReq = "")
452+
Exception ex, string errorStringCheck, int retryCount, bool isThrottled, string webUriReq = "", CancellationToken cancellationToken = default)
451453
{
452454
retryCount++;
453455
logEntry.LogFailure(req, requestTrackingId, sessionTrackingId, disableConnectionLocking, LockWait, logDt, ex, errorStringCheck, webUriMessageReq: webUriReq);
454456
logEntry.LogRetry(retryCount, req, retryPauseTimeRunning, isThrottled: isThrottled);
455-
await Task.Delay(retryPauseTimeRunning).ConfigureAwait(false);
457+
458+
// Task.Delay returns early if the CancellationToken is cancelled, but throws a TaskCancelledException if
459+
// we pass it an already-cancelled CancellationToken. If case that occurs, skip the call for consistent behavior.
460+
if (!cancellationToken.IsCancellationRequested)
461+
await Task.Delay(retryPauseTimeRunning, cancellationToken).ConfigureAwait(false);
462+
456463
return retryCount;
457464
}
458465

src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/ServiceClientTests.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
using System.Net.Http;
2727
using System.Runtime.CompilerServices;
2828
using System.Security;
29+
using System.Threading;
2930
using System.Threading.Tasks;
3031
using Xunit;
3132
using Xunit.Abstractions;
@@ -104,6 +105,23 @@ public void TestThrowDisposedOperationCheck()
104105
});
105106
}
106107

108+
[Fact]
109+
public async Task ThrowsOperationCanceledExceptionWhenCancelled()
110+
{
111+
testSupport.SetupMockAndSupport(out var orgSvc, out var fakHttpMethodHander, out var cli);
112+
var cts = new CancellationTokenSource();
113+
114+
var cancelledToken = cts.Token;
115+
cts.Cancel();
116+
117+
Func<Task> cancelledRequest = async() =>
118+
{
119+
await cli.ExecuteAsync(new WhoAmIRequest(), cts.Token).ConfigureAwait(false);
120+
};
121+
122+
await cancelledRequest.Should().ThrowAsync<OperationCanceledException>().ConfigureAwait(false);
123+
}
124+
107125
[Fact]
108126
public void ExecuteMessageTests()
109127
{
@@ -807,6 +825,65 @@ public async void RetryOperationAyncTest()
807825
Assert.True(retrycount == 2);
808826
}
809827

828+
[Fact]
829+
public async Task RetryOperationCancelledDuringDelayTest()
830+
{
831+
testSupport.SetupMockAndSupport(out var orgSvc, out var fakHttpMethodHander, out var cli);
832+
833+
Guid testGuid = Guid.NewGuid();
834+
835+
CreateRequest exampleRequest = new CreateRequest();
836+
exampleRequest.Target = new Entity("account");
837+
exampleRequest.Target.Attributes.Add("id", testGuid);
838+
839+
Stopwatch testwatch = Stopwatch.StartNew();
840+
841+
var cts = new CancellationTokenSource();
842+
var cancellationToken = cts.Token;
843+
844+
var delay = TimeSpan.FromSeconds(5);
845+
846+
var retryTask = Task.Run(async () =>
847+
{
848+
await Utilities.RetryRequest(exampleRequest, testGuid, new TimeSpan(0), testwatch, cli._logEntry, null, false, delay, new Exception("Fake_TEST_MSG"), "test retry logic", 0, false, null, cancellationToken: cancellationToken).ConfigureAwait(false);
849+
});
850+
851+
await Task.Delay(TimeSpan.FromMilliseconds(100)).ConfigureAwait(false);
852+
cts.Cancel();
853+
await Task.Delay(TimeSpan.FromMilliseconds(50)).ConfigureAwait(false);
854+
855+
retryTask.IsCompleted.Should().BeTrue("Task.Delay within Utilities.RetryRequest should just return early, allowing the task to complete");
856+
testwatch.Elapsed.Should().BeLessThan(delay, "Task should return before its delay timer can complete due to cancellation");
857+
}
858+
859+
[Fact]
860+
public async Task RetryOperationShouldNotThrowWhenAlreadyCanceledTest()
861+
{
862+
testSupport.SetupMockAndSupport(out var orgSvc, out var fakHttpMethodHander, out var cli);
863+
864+
Guid testGuid = Guid.NewGuid();
865+
866+
CreateRequest exampleRequest = new CreateRequest();
867+
exampleRequest.Target = new Entity("account");
868+
exampleRequest.Target.Attributes.Add("id", testGuid);
869+
870+
Stopwatch testwatch = Stopwatch.StartNew();
871+
872+
var cts = new CancellationTokenSource();
873+
var cancellationToken = cts.Token;
874+
cts.Cancel(); // Cancel before the token is even provided to the method
875+
876+
var delay = TimeSpan.FromSeconds(5);
877+
878+
Func<Task> cancelledRetry = async () =>
879+
{
880+
await Utilities.RetryRequest(exampleRequest, testGuid, new TimeSpan(0), testwatch, cli._logEntry, null, false, delay, new Exception("Fake_TEST_MSG"), "test retry logic", 0, false, null, cancellationToken: cancellationToken).ConfigureAwait(false);
881+
};
882+
883+
await cancelledRetry.Should().NotThrowAsync<TaskCanceledException>().ConfigureAwait(false);
884+
testwatch.Elapsed.Should().BeLessThan(delay, "Task should return before its delay timer can complete due to cancellation");
885+
}
886+
810887
#region LiveConnectedTests
811888

812889
[SkippableConnectionTest]

src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ Notice:
77
Note: Only AD on FullFramework, OAuth, Certificate, ClientSecret Authentication types are supported at this time.
88

99
++CURRENTRELEASEID++
10+
Fix for CancellationToken not canceling retries during delays Git: https://github.com/microsoft/PowerPlatform-DataverseServiceClient/issues/508
11+
12+
1.2.5
1013
Fix for Null ILogger passed to the AzAuth Client Creators causing a fault. Git: https://github.com/microsoft/PowerPlatform-DataverseServiceClient/issues/481
1114
Fix for Memory Leak in Retrieve Multiple. Git: https://github.com/microsoft/PowerPlatform-DataverseServiceClient/issues/463
1215
Fix for Memory bloat issue caused by not releasing lastErrorMessage property.

0 commit comments

Comments
 (0)