From 88905344843699ddfd514ec98c76af06040aba4d Mon Sep 17 00:00:00 2001 From: Wesley Cabus Date: Mon, 29 Sep 2025 14:08:29 +0200 Subject: [PATCH 1/3] Catch potential OperationCanceledException in DiagnosticHostedService --- .../V2/Diagnostics/DiagnosticHostedService.cs | 28 +++++++--- .../v2/DiagnosticHostedServiceTests.cs | 54 +++++++++++++++++++ 2 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticHostedServiceTests.cs diff --git a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticHostedService.cs b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticHostedService.cs index 9ffd85f56..0cf4c5cdd 100644 --- a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticHostedService.cs +++ b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticHostedService.cs @@ -13,17 +13,31 @@ 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 async Task ExecuteForTestOnly(CancellationToken stoppingToken) + { + await ExecuteAsync(stoppingToken); } public override async Task StopAsync(CancellationToken cancellationToken) diff --git a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticHostedServiceTests.cs b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticHostedServiceTests.cs new file mode 100644 index 000000000..c78c1395d --- /dev/null +++ b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticHostedServiceTests.cs @@ -0,0 +1,54 @@ +// 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(); + var firstDiagnosticEntry = new TestDiagnosticEntry(); + var secondDiagnosticEntry = new TestDiagnosticEntry(); + var thirdDiagnosticEntry = new TestDiagnosticEntry(); + var entries = new List + { + 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(); + + 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 bool WasCalled { get; private set; } + public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer) + { + WasCalled = true; + return Task.CompletedTask; + } + } +} From c95f51851110fd92c8c2bee04efac0f3d06b45d7 Mon Sep 17 00:00:00 2001 From: Wesley Cabus Date: Mon, 29 Sep 2025 14:15:10 +0200 Subject: [PATCH 2/3] Converted internal test method to expression body --- .../Licensing/V2/Diagnostics/DiagnosticHostedService.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticHostedService.cs b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticHostedService.cs index 0cf4c5cdd..2d7b355ab 100644 --- a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticHostedService.cs +++ b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticHostedService.cs @@ -35,10 +35,7 @@ internal class DiagnosticHostedService(DiagnosticSummary diagnosticSummary, IOpt } // Added for testing purposes to be able to call ExecuteAsync directly. - internal async Task ExecuteForTestOnly(CancellationToken stoppingToken) - { - await ExecuteAsync(stoppingToken); - } + internal Task ExecuteForTestOnly(CancellationToken stoppingToken) => ExecuteAsync(stoppingToken); public override async Task StopAsync(CancellationToken cancellationToken) { From d44bd75d81c6d446679d5648c241a2973c4915c3 Mon Sep 17 00:00:00 2001 From: Wesley Cabus Date: Mon, 29 Sep 2025 14:22:14 +0200 Subject: [PATCH 3/3] Cleanup and remove UTF-8 BOM --- .../Licensing/v2/DiagnosticHostedServiceTests.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticHostedServiceTests.cs b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticHostedServiceTests.cs index c78c1395d..9fba275d3 100644 --- a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticHostedServiceTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticHostedServiceTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Duende Software. All rights reserved. +// Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. using System.Text.Json; @@ -44,11 +44,6 @@ public class DiagnosticHostedServiceTests private class TestDiagnosticEntry : IDiagnosticEntry { - public bool WasCalled { get; private set; } - public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer) - { - WasCalled = true; - return Task.CompletedTask; - } + public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer) => Task.CompletedTask; } }