From 383e600066151dd94a785b87680da0a389977046 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Mon, 21 Sep 2026 19:32:23 +0100 Subject: [PATCH] Handle registration client failures gracefully --- .../HealthMonitoringRegistrationClient.cs | 27 ++++++++++++------- ...HealthMonitoringRegistrationClientTests.cs | 24 +++++++++++++++++ ...thMonitoringRegistrationExtensionsTests.cs | 6 ++--- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/HealthMonitoring/HealthMonitoring.Client/HealthMonitoringRegistrationClient.cs b/HealthMonitoring/HealthMonitoring.Client/HealthMonitoringRegistrationClient.cs index 3e5c74b..fec5e8e 100644 --- a/HealthMonitoring/HealthMonitoring.Client/HealthMonitoringRegistrationClient.cs +++ b/HealthMonitoring/HealthMonitoring.Client/HealthMonitoringRegistrationClient.cs @@ -28,9 +28,9 @@ public sealed record ServiceRegistrationResult(string ServiceId, string Action, public interface IHealthMonitoringRegistrationClient { - Task RegisterAsync(ServiceRegistrationOptions options, CancellationToken cancellationToken = default); + Task RegisterAsync(ServiceRegistrationOptions options, CancellationToken cancellationToken = default); Task SetDependencyMappingsAsync(string serviceId, IEnumerable mappings, CancellationToken cancellationToken = default); - Task RegisterAndConfigureAsync(ServiceRegistrationOptions options, IEnumerable mappings, CancellationToken cancellationToken = default); + Task RegisterAndConfigureAsync(ServiceRegistrationOptions options, IEnumerable mappings, CancellationToken cancellationToken = default); } public sealed class HealthMonitoringRegistrationClient( @@ -39,7 +39,7 @@ public sealed class HealthMonitoringRegistrationClient( { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); - public async Task RegisterAsync(ServiceRegistrationOptions options, CancellationToken cancellationToken = default) + public async Task RegisterAsync(ServiceRegistrationOptions options, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(options); var monitorType = ParseMonitorType(options.MonitorType); @@ -71,12 +71,20 @@ public async Task RegisterAsync(ServiceRegistrationOp "POST /api/services/register body: {RequestBody}", JsonSerializer.Serialize(loggedPayload, JsonOptions)); - using var response = await httpClient.PostAsJsonAsync("api/services/register", payload, JsonOptions, cancellationToken); + try + { + using var response = await httpClient.PostAsJsonAsync("api/services/register", payload, JsonOptions, cancellationToken); - await EnsureSuccessAsync(response, "register service", cancellationToken); - var result = await response.Content.ReadFromJsonAsync(JsonOptions, cancellationToken) - ?? throw new InvalidOperationException("The monitoring server returned an empty registration response."); - return new ServiceRegistrationResult(result.ServiceId, result.Action, result.Service.Id); + await EnsureSuccessAsync(response, "register service", cancellationToken); + var result = await response.Content.ReadFromJsonAsync(JsonOptions, cancellationToken) + ?? throw new InvalidOperationException("The monitoring server returned an empty registration response."); + return new ServiceRegistrationResult(result.ServiceId, result.Action, result.Service.Id); + } + catch (Exception exception) when (!cancellationToken.IsCancellationRequested) + { + logger?.LogWarning(exception, "Unable to register service '{ServiceId}' with health monitoring.", options.ServiceId); + return null; + } } public async Task SetDependencyMappingsAsync(string serviceId, IEnumerable mappings, CancellationToken cancellationToken = default) @@ -100,9 +108,10 @@ public async Task SetDependencyMappingsAsync(string serviceId, IEnumerable RegisterAndConfigureAsync(ServiceRegistrationOptions options, IEnumerable mappings, CancellationToken cancellationToken = default) + public async Task RegisterAndConfigureAsync(ServiceRegistrationOptions options, IEnumerable mappings, CancellationToken cancellationToken = default) { var result = await RegisterAsync(options, cancellationToken); + if (result is null) return null; await SetDependencyMappingsAsync(options.ServiceId, mappings, cancellationToken); return result; } diff --git a/HealthMonitoring/HealthMonitoring.Tests/Client/HealthMonitoringRegistrationClientTests.cs b/HealthMonitoring/HealthMonitoring.Tests/Client/HealthMonitoringRegistrationClientTests.cs index 55b40d4..5380fec 100644 --- a/HealthMonitoring/HealthMonitoring.Tests/Client/HealthMonitoringRegistrationClientTests.cs +++ b/HealthMonitoring/HealthMonitoring.Tests/Client/HealthMonitoringRegistrationClientTests.cs @@ -38,6 +38,7 @@ public async Task RegisterAsync_posts_service_configuration_and_returns_acknowle Version = "1.2.3" }); + Assert.NotNull(result); Assert.Equal("created", result.Action); Assert.Equal(Guid.Parse("11111111-1111-1111-1111-111111111111"), result.MonitoredServiceId); Assert.Equal(HttpMethod.Post, handler.Request!.Method); @@ -81,6 +82,29 @@ await client.RegisterAsync(new ServiceRegistrationOptions Assert.DoesNotContain("super-secret", logger.Messages.Single()); } + [Fact] + public async Task RegisterAsync_returns_null_and_logs_when_registration_call_fails() + { + var handler = new RecordingHandler(_ => new HttpResponseMessage(HttpStatusCode.ServiceUnavailable) + { + Content = new StringContent("monitoring unavailable") + }); + var logger = new RecordingLogger(); + var client = new HealthMonitoringRegistrationClient( + new HttpClient(handler) { BaseAddress = new Uri(MonitoringBaseUrl) }, + logger); + + var result = await client.RegisterAsync(new ServiceRegistrationOptions + { + ServiceId = PaymentsApiServiceId, + Name = "Payments API", + HealthUrl = new Uri("https://payments-api/health") + }); + + Assert.Null(result); + Assert.Contains(logger.Messages, message => message.Contains("register service", StringComparison.OrdinalIgnoreCase)); + } + [Fact] public async Task SetDependencyMappingsAsync_resolves_target_service_and_upserts_mapping() { diff --git a/HealthMonitoring/HealthMonitoring.Tests/Client/HealthMonitoringRegistrationExtensionsTests.cs b/HealthMonitoring/HealthMonitoring.Tests/Client/HealthMonitoringRegistrationExtensionsTests.cs index fdae71a..12ee924 100644 --- a/HealthMonitoring/HealthMonitoring.Tests/Client/HealthMonitoringRegistrationExtensionsTests.cs +++ b/HealthMonitoring/HealthMonitoring.Tests/Client/HealthMonitoringRegistrationExtensionsTests.cs @@ -82,7 +82,7 @@ private sealed class RecordingRegistrationClient : IHealthMonitoringRegistration public ServiceRegistrationOptions? Service { get; private set; } public IEnumerable? Dependencies { get; private set; } - public Task RegisterAsync(ServiceRegistrationOptions options, CancellationToken cancellationToken = default) + public Task RegisterAsync(ServiceRegistrationOptions options, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); throw new NotSupportedException($"RegisterAsync is not used for {options.ServiceId}."); @@ -94,12 +94,12 @@ public Task SetDependencyMappingsAsync(string serviceId, IEnumerable RegisterAndConfigureAsync(ServiceRegistrationOptions options, IEnumerable mappings, CancellationToken cancellationToken = default) + public Task RegisterAndConfigureAsync(ServiceRegistrationOptions options, IEnumerable mappings, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); Service = options; Dependencies = mappings; - return Task.FromResult(new ServiceRegistrationResult(options.ServiceId, "created", Guid.NewGuid())); + return Task.FromResult(new ServiceRegistrationResult(options.ServiceId, "created", Guid.NewGuid())); } } }