diff --git a/src/Microsoft.Health.Fhir.Api.UnitTests/Microsoft.Health.Fhir.R4.Api.UnitTests.csproj b/src/Microsoft.Health.Fhir.Api.UnitTests/Microsoft.Health.Fhir.R4.Api.UnitTests.csproj index a1020bb235..ea7defa336 100644 --- a/src/Microsoft.Health.Fhir.Api.UnitTests/Microsoft.Health.Fhir.R4.Api.UnitTests.csproj +++ b/src/Microsoft.Health.Fhir.Api.UnitTests/Microsoft.Health.Fhir.R4.Api.UnitTests.csproj @@ -15,6 +15,9 @@ + + + diff --git a/src/Microsoft.Health.Fhir.Api/Features/Health/StorageInitializedHealthCheck.cs b/src/Microsoft.Health.Fhir.Api/Features/Health/StorageInitializedHealthCheck.cs new file mode 100644 index 0000000000..2c7506a590 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Api/Features/Health/StorageInitializedHealthCheck.cs @@ -0,0 +1,45 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Health.Core; +using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Messages.Storage; + +namespace Microsoft.Health.Fhir.Api.Features.Health +{ + public class StorageInitializedHealthCheck : IHealthCheck, INotificationHandler + { + private const string SuccessfullyInitializedMessage = "Successfully initialized."; + private bool _storageReady; + private readonly DateTimeOffset _started = Clock.UtcNow; + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + if (_storageReady) + { + return Task.FromResult(HealthCheckResult.Healthy(SuccessfullyInitializedMessage)); + } + + TimeSpan waited = Clock.UtcNow - _started; + if (waited < TimeSpan.FromMinutes(5)) + { + return Task.FromResult(new HealthCheckResult(HealthStatus.Degraded, $"Storage is initializing. Waited: {(int)waited.TotalSeconds}s.")); + } + + return Task.FromResult(new HealthCheckResult(HealthStatus.Unhealthy, $"Storage has not been initialized. Waited: {(int)waited.TotalSeconds}s.")); + } + + public Task Handle(StorageInitializedNotification notification, CancellationToken cancellationToken) + { + _storageReady = true; + return Task.CompletedTask; + } + } +} diff --git a/src/Microsoft.Health.Fhir.R4B.Api.UnitTests/Microsoft.Health.Fhir.R4B.Api.UnitTests.csproj b/src/Microsoft.Health.Fhir.R4B.Api.UnitTests/Microsoft.Health.Fhir.R4B.Api.UnitTests.csproj index bf44b92bcb..b0e4e1feff 100644 --- a/src/Microsoft.Health.Fhir.R4B.Api.UnitTests/Microsoft.Health.Fhir.R4B.Api.UnitTests.csproj +++ b/src/Microsoft.Health.Fhir.R4B.Api.UnitTests/Microsoft.Health.Fhir.R4B.Api.UnitTests.csproj @@ -15,6 +15,9 @@ + + + diff --git a/src/Microsoft.Health.Fhir.R5.Api.UnitTests/Microsoft.Health.Fhir.R5.Api.UnitTests.csproj b/src/Microsoft.Health.Fhir.R5.Api.UnitTests/Microsoft.Health.Fhir.R5.Api.UnitTests.csproj index 239a28af30..7adb326aad 100644 --- a/src/Microsoft.Health.Fhir.R5.Api.UnitTests/Microsoft.Health.Fhir.R5.Api.UnitTests.csproj +++ b/src/Microsoft.Health.Fhir.R5.Api.UnitTests/Microsoft.Health.Fhir.R5.Api.UnitTests.csproj @@ -15,6 +15,9 @@ + + + diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Health/StorageInitializedHealthCheckTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Health/StorageInitializedHealthCheckTests.cs new file mode 100644 index 0000000000..9679d19e5d --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Health/StorageInitializedHealthCheckTests.cs @@ -0,0 +1,52 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Messages.Storage; +using Microsoft.Health.Fhir.Tests.Common; +using Microsoft.Health.Test.Utilities; +using Xunit; + +namespace Microsoft.Health.Fhir.Api.Features.Health; + +[Trait(Traits.OwningTeam, OwningTeam.Fhir)] +[Trait(Traits.Category, Categories.DataSourceValidation)] +public class StorageInitializedHealthCheckTests +{ + private readonly StorageInitializedHealthCheck _sut = new(); + + [Fact] + public async Task GivenStorageInitialized_WhenCheckHealthAsync_ThenReturnsHealthy() + { + await _sut.Handle(new StorageInitializedNotification(), CancellationToken.None); + + HealthCheckResult result = await _sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None); + + Assert.Equal(HealthStatus.Healthy, result.Status); + } + + [Fact] + public async Task GivenStorageInitializedHealthCheck_WhenCheckHealthAsync_ThenStartsAsDegraded() + { + HealthCheckResult result = await _sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None); + Assert.Equal(HealthStatus.Degraded, result.Status); + } + +#if NET8_0_OR_GREATER + [Fact] + public async Task GivenStorageInitializedHealthCheck_WhenCheckHealthAsync_ThenChangedToUnhealthyAfter5Minute() + { + using (Mock.Property(() => ClockResolver.TimeProvider, new Microsoft.Extensions.Time.Testing.FakeTimeProvider(DateTimeOffset.Now.AddMinutes(5).AddSeconds(1)))) + { + HealthCheckResult result = await _sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None); + Assert.Equal(HealthStatus.Unhealthy, result.Status); + } + } +#endif +} diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems index c237305398..9dd83921e8 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems @@ -58,6 +58,7 @@ + @@ -80,7 +81,4 @@ Never - - - \ No newline at end of file diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs b/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs index bbae24e968..c79a4e4849 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs @@ -35,6 +35,7 @@ using Microsoft.Health.Fhir.Core.Features.Persistence; using Microsoft.Health.Fhir.Core.Features.Security; using Microsoft.Health.Fhir.Core.Messages.CapabilityStatement; +using Microsoft.Health.Fhir.Core.Messages.Storage; using Microsoft.Health.Fhir.Core.Models; namespace Microsoft.Health.Fhir.Api.Modules @@ -176,6 +177,16 @@ ResourceElement SetMetadata(Resource resource, string versionId, DateTimeOffset services.AddHealthChecks().AddCheck(name: "BehaviorHealthCheck"); + // Registers a health check to ensure storage gets initialized + services.RemoveServiceTypeExact>() + .Add() + .Singleton() + .AsSelf() + .AsService() + .AsService>(); + + services.AddHealthChecks().AddCheck(name: "StorageInitializedHealthCheck"); + services.AddLazy(); services.AddScoped(); diff --git a/src/Microsoft.Health.Fhir.Stu3.Api.UnitTests/Microsoft.Health.Fhir.Stu3.Api.UnitTests.csproj b/src/Microsoft.Health.Fhir.Stu3.Api.UnitTests/Microsoft.Health.Fhir.Stu3.Api.UnitTests.csproj index 753994bc5c..0a72f20685 100644 --- a/src/Microsoft.Health.Fhir.Stu3.Api.UnitTests/Microsoft.Health.Fhir.Stu3.Api.UnitTests.csproj +++ b/src/Microsoft.Health.Fhir.Stu3.Api.UnitTests/Microsoft.Health.Fhir.Stu3.Api.UnitTests.csproj @@ -17,6 +17,9 @@ + + +