Skip to content

Commit b3582d2

Browse files
Update retry in Kusto emulator actions to handle any non-permanent error (#11752)
* Extract resilence pipeline and add tests * Generalize reslience pipeline to handle any non-permanent exception * Clean up naming * Fix up names * Add comment * Fix namespace * Apply suggestions from code review Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent f62ee00 commit b3582d2

File tree

3 files changed

+102
-11
lines changed

3 files changed

+102
-11
lines changed

src/Aspire.Hosting.Azure.Kusto/AzureKustoBuilderExtensions.cs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
using Kusto.Data.Net.Client;
1515
using Microsoft.Extensions.DependencyInjection;
1616
using Microsoft.Extensions.Logging;
17-
using Polly;
1817

1918
namespace Aspire.Hosting;
2019

@@ -23,15 +22,6 @@ namespace Aspire.Hosting;
2322
/// </summary>
2423
public static class AzureKustoBuilderExtensions
2524
{
26-
private static readonly ResiliencePipeline s_pipeline = new ResiliencePipelineBuilder()
27-
.AddRetry(new()
28-
{
29-
MaxRetryAttempts = 3,
30-
Delay = TimeSpan.FromSeconds(2),
31-
ShouldHandle = new PredicateBuilder().Handle<KustoRequestThrottledException>(),
32-
})
33-
.Build();
34-
3525
/// <summary>
3626
/// Adds an Azure Data Explorer (Kusto) cluster resource to the application model.
3727
/// </summary>
@@ -293,7 +283,7 @@ private static async Task CreateDatabaseAsync(ICslAdminProvider adminProvider, A
293283

294284
try
295285
{
296-
await s_pipeline.ExecuteAsync(async cancellationToken => await adminProvider.ExecuteControlCommandAsync(databaseResource.DatabaseName, script, crp).ConfigureAwait(false), cancellationToken).ConfigureAwait(false);
286+
await AzureKustoEmulatorResiliencePipelines.Default.ExecuteAsync(async ct => await adminProvider.ExecuteControlCommandAsync(databaseResource.DatabaseName, script, crp).ConfigureAwait(false), cancellationToken).ConfigureAwait(false);
297287
logger.LogDebug("Database '{DatabaseName}' created successfully", databaseResource.DatabaseName);
298288
}
299289
catch (KustoBadRequestException e) when (e.Message.Contains("EntityNameAlreadyExistsException"))
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Kusto.Cloud.Platform.Utils;
5+
using Polly;
6+
7+
namespace Aspire.Hosting.Azure;
8+
9+
/// <summary>
10+
/// Provides pre-configured resilience pipelines for Azure Kusto emulator operations.
11+
/// </summary>
12+
internal static class AzureKustoEmulatorResiliencePipelines
13+
{
14+
/// <summary>
15+
/// Gets a resilience pipeline configured to handle non-permanent exceptions.
16+
/// </summary>
17+
public static ResiliencePipeline Default { get; } = new ResiliencePipelineBuilder()
18+
.AddRetry(new()
19+
{
20+
MaxRetryAttempts = 10,
21+
Delay = TimeSpan.FromMilliseconds(100),
22+
BackoffType = DelayBackoffType.Exponential,
23+
ShouldHandle = new PredicateBuilder().Handle<Exception>(IsTransient),
24+
})
25+
.Build();
26+
27+
/// <summary>
28+
/// Determines whether the specified exception represents a transient error that may succeed if retried.
29+
/// </summary>
30+
/// <remarks>
31+
/// There's no common base exception type between exceptions in the Kusto.Data and Kusto.Ingest libraries, however
32+
/// they do share a common interface, <see cref="ICloudPlatformException"/>, which has the <c>IsPermanent</c> property.
33+
/// </remarks>
34+
private static bool IsTransient(Exception ex) => ex is ICloudPlatformException cpe && !cpe.IsPermanent;
35+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Kusto.Data.Exceptions;
5+
6+
namespace Aspire.Hosting.Azure.Kusto.Tests;
7+
8+
public class KustoResiliencePipelinesTests
9+
{
10+
[Fact]
11+
public async Task ShouldRetryOnTemporaryExceptions()
12+
{
13+
// Arrange
14+
var attemptCount = 0;
15+
ValueTask work(CancellationToken ct)
16+
{
17+
attemptCount++;
18+
throw new KustoRequestThrottledException();
19+
}
20+
21+
// Act + Assert
22+
await Assert.ThrowsAsync<KustoRequestThrottledException>(async () =>
23+
{
24+
await AzureKustoEmulatorResiliencePipelines.Default.ExecuteAsync(work, TestContext.Current.CancellationToken);
25+
});
26+
Assert.True(attemptCount > 1, "Operation should have been retried");
27+
}
28+
29+
[Fact]
30+
public async Task ShouldNotRetryOnOtherExceptions()
31+
{
32+
// Arrange
33+
var attemptCount = 0;
34+
ValueTask work(CancellationToken ct)
35+
{
36+
attemptCount++;
37+
throw new InvalidOperationException();
38+
}
39+
40+
// Act + Assert
41+
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
42+
{
43+
await AzureKustoEmulatorResiliencePipelines.Default.ExecuteAsync(work, TestContext.Current.CancellationToken);
44+
});
45+
Assert.Equal(1, attemptCount);
46+
}
47+
48+
[Fact]
49+
public async Task ShouldNotRetryOnPermanentExceptions()
50+
{
51+
// Arrange
52+
var attemptCount = 0;
53+
ValueTask work(CancellationToken ct)
54+
{
55+
attemptCount++;
56+
throw new KustoBadRequestException();
57+
}
58+
59+
// Act + Assert
60+
await Assert.ThrowsAsync<KustoBadRequestException>(async () =>
61+
{
62+
await AzureKustoEmulatorResiliencePipelines.Default.ExecuteAsync(work, TestContext.Current.CancellationToken);
63+
});
64+
Assert.Equal(1, attemptCount);
65+
}
66+
}

0 commit comments

Comments
 (0)