Merge pull request #2229 from DuendeSoftware/wca/diagnostics-operation-canceled

Catch potential OperationCanceledException in DiagnosticHostedService
This commit is contained in:
Brett Hazen 2025-10-13 08:17:22 -05:00 committed by GitHub
commit def100a79c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 67 additions and 7 deletions

View file

@ -13,19 +13,30 @@ internal class DiagnosticHostedService(DiagnosticSummary diagnosticSummary, IOpt
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(options.Value.Diagnostics.LogFrequency);
while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
try
{
try
while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
{
await diagnosticSummary.PrintSummary();
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while logging the diagnostic summary: {Message}", ex.Message);
try
{
await diagnosticSummary.PrintSummary();
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while logging the diagnostic summary: {Message}",
ex.Message);
}
}
}
catch (OperationCanceledException)
{
// When stopping this hosted service, "await timer.WaitForNextTickAsync(stoppingToken)" can throw an OperationCanceledException.
}
}
// Added for testing purposes to be able to call ExecuteAsync directly.
internal Task ExecuteForTestOnly(CancellationToken stoppingToken) => ExecuteAsync(stoppingToken);
public override async Task StopAsync(CancellationToken cancellationToken)
{
await diagnosticSummary.PrintSummary();

View file

@ -0,0 +1,49 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using System.Text.Json;
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Licensing.V2.Diagnostics;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
namespace IdentityServer.UnitTests.Licensing.V2;
public class DiagnosticHostedServiceTests
{
[Fact]
public async Task ExecuteAsync_ShouldNotThrowOperationCancelledException()
{
var diagnosticSummaryLogger = new NullLogger<DiagnosticSummary>();
var firstDiagnosticEntry = new TestDiagnosticEntry();
var secondDiagnosticEntry = new TestDiagnosticEntry();
var thirdDiagnosticEntry = new TestDiagnosticEntry();
var entries = new List<IDiagnosticEntry>
{
firstDiagnosticEntry,
secondDiagnosticEntry,
thirdDiagnosticEntry
};
var diagnosticSummary = new DiagnosticSummary(DateTime.UtcNow, entries, new IdentityServerOptions(), new StubLoggerFactory(diagnosticSummaryLogger));
var options = Options.Create(new IdentityServerOptions());
var logger = new NullLogger<DiagnosticHostedService>();
var service = new DiagnosticHostedService(diagnosticSummary, options, logger);
using var cts = new CancellationTokenSource();
cts.CancelAfter(100);
var exception = await Record.ExceptionAsync(async () =>
{
await service.ExecuteForTestOnly(cts.Token);
});
exception.ShouldBeNull();
}
private class TestDiagnosticEntry : IDiagnosticEntry
{
public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer) => Task.CompletedTask;
}
}