Adds abstraction around boot time checks for database availability (#19848)
* Adds abstraction around boot time checks for database availability. * Addressed issues raised in code review.
This commit is contained in:
@@ -90,6 +90,9 @@ public static partial class UmbracoBuilderExtensions
|
||||
builder.AddNotificationAsyncHandler<RuntimeUnattendedUpgradeNotification, UnattendedUpgrader>();
|
||||
builder.AddNotificationAsyncHandler<RuntimePremigrationsUpgradeNotification, PremigrationUpgrader>();
|
||||
|
||||
// Database availability check.
|
||||
builder.Services.AddUnique<IDatabaseAvailabilityCheck, DefaultDatabaseAvailabilityCheck>();
|
||||
|
||||
// Add runtime mode validation
|
||||
builder.Services.AddSingleton<IRuntimeModeValidationService, RuntimeModeValidationService>();
|
||||
builder.RuntimeModeValidators()
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a configured database is available on boot using the default method of 5 attempts with a 1 second delay between each one.
|
||||
/// </summary>
|
||||
internal class DefaultDatabaseAvailabilityCheck : IDatabaseAvailabilityCheck
|
||||
{
|
||||
private const int NumberOfAttempts = 5;
|
||||
private const int DefaultAttemptDelayMilliseconds = 1000;
|
||||
|
||||
private readonly ILogger<DefaultDatabaseAvailabilityCheck> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultDatabaseAvailabilityCheck"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
public DefaultDatabaseAvailabilityCheck(ILogger<DefaultDatabaseAvailabilityCheck> logger) => _logger = logger;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of milliseconds to delay between attempts.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Exposed for testing purposes, hence settable only internally.
|
||||
/// </remarks>
|
||||
public int AttemptDelayMilliseconds { get; internal set; } = DefaultAttemptDelayMilliseconds;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsDatabaseAvailable(IUmbracoDatabaseFactory databaseFactory)
|
||||
{
|
||||
bool canConnect;
|
||||
for (var i = 0; ;)
|
||||
{
|
||||
canConnect = databaseFactory.CanConnect;
|
||||
if (canConnect || ++i == NumberOfAttempts)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Could not immediately connect to database, trying again.");
|
||||
}
|
||||
|
||||
// Wait for the configured time before trying again.
|
||||
Thread.Sleep(AttemptDelayMilliseconds);
|
||||
}
|
||||
|
||||
return canConnect;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a configured database is available on boot.
|
||||
/// </summary>
|
||||
public interface IDatabaseAvailabilityCheck
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if the database is available for Umbraco to boot.
|
||||
/// </summary>
|
||||
/// <param name="databaseFactory">The <see cref="IUmbracoDatabaseFactory"/>.</param>
|
||||
/// <returns>
|
||||
/// A value indicating whether the database is available.
|
||||
/// </returns>
|
||||
bool IsDatabaseAvailable(IUmbracoDatabaseFactory databaseFactory);
|
||||
}
|
||||
@@ -31,6 +31,7 @@ public class RuntimeState : IRuntimeState
|
||||
private readonly IConflictingRouteService _conflictingRouteService = null!;
|
||||
private readonly IEnumerable<IDatabaseProviderMetadata> _databaseProviderMetadata = null!;
|
||||
private readonly IRuntimeModeValidationService _runtimeModeValidationService = null!;
|
||||
private readonly IDatabaseAvailabilityCheck _databaseAvailabilityCheck = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The initial <see cref="RuntimeState"/>
|
||||
@@ -46,6 +47,7 @@ public class RuntimeState : IRuntimeState
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RuntimeState" /> class.
|
||||
/// </summary>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public RuntimeState(
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
IOptions<UnattendedSettings> unattendedSettings,
|
||||
@@ -56,6 +58,34 @@ public class RuntimeState : IRuntimeState
|
||||
IConflictingRouteService conflictingRouteService,
|
||||
IEnumerable<IDatabaseProviderMetadata> databaseProviderMetadata,
|
||||
IRuntimeModeValidationService runtimeModeValidationService)
|
||||
: this(
|
||||
globalSettings,
|
||||
unattendedSettings,
|
||||
umbracoVersion,
|
||||
databaseFactory,
|
||||
logger,
|
||||
packageMigrationState,
|
||||
conflictingRouteService,
|
||||
databaseProviderMetadata,
|
||||
runtimeModeValidationService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDatabaseAvailabilityCheck>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RuntimeState" /> class.
|
||||
/// </summary>
|
||||
public RuntimeState(
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
IOptions<UnattendedSettings> unattendedSettings,
|
||||
IUmbracoVersion umbracoVersion,
|
||||
IUmbracoDatabaseFactory databaseFactory,
|
||||
ILogger<RuntimeState> logger,
|
||||
PendingPackageMigrations packageMigrationState,
|
||||
IConflictingRouteService conflictingRouteService,
|
||||
IEnumerable<IDatabaseProviderMetadata> databaseProviderMetadata,
|
||||
IRuntimeModeValidationService runtimeModeValidationService,
|
||||
IDatabaseAvailabilityCheck databaseAvailabilityCheck)
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
_unattendedSettings = unattendedSettings;
|
||||
@@ -66,6 +96,7 @@ public class RuntimeState : IRuntimeState
|
||||
_conflictingRouteService = conflictingRouteService;
|
||||
_databaseProviderMetadata = databaseProviderMetadata;
|
||||
_runtimeModeValidationService = runtimeModeValidationService;
|
||||
_databaseAvailabilityCheck = databaseAvailabilityCheck;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -242,7 +273,7 @@ public class RuntimeState : IRuntimeState
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!TryDbConnect(databaseFactory))
|
||||
if (_databaseAvailabilityCheck.IsDatabaseAvailable(databaseFactory) is false)
|
||||
{
|
||||
return UmbracoDatabaseState.CannotConnect;
|
||||
}
|
||||
@@ -305,27 +336,4 @@ public class RuntimeState : IRuntimeState
|
||||
}
|
||||
return CurrentMigrationState != FinalMigrationState;
|
||||
}
|
||||
|
||||
private bool TryDbConnect(IUmbracoDatabaseFactory databaseFactory)
|
||||
{
|
||||
// anything other than install wants a database - see if we can connect
|
||||
// (since this is an already existing database, assume localdb is ready)
|
||||
bool canConnect;
|
||||
var tries = 5;
|
||||
for (var i = 0; ;)
|
||||
{
|
||||
canConnect = databaseFactory.CanConnect;
|
||||
if (canConnect || ++i == tries)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Could not immediately connect to database, trying again.");
|
||||
}
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
|
||||
return canConnect;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Install.Models;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Persistence.SqlServer.Services;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence;
|
||||
|
||||
[TestFixture]
|
||||
public class DefaultDatabaseAvailabilityCheckTests
|
||||
{
|
||||
[Test]
|
||||
public void IsDatabaseAvailable_WithDatabaseUnavailable_ReturnsFalse()
|
||||
{
|
||||
var mockDatabaseFactory = new Mock<IUmbracoDatabaseFactory>();
|
||||
mockDatabaseFactory
|
||||
.Setup(x => x.CanConnect)
|
||||
.Returns(false);
|
||||
|
||||
var sut = CreateDefaultDatabaseAvailabilityCheck();
|
||||
var result = sut.IsDatabaseAvailable(mockDatabaseFactory.Object);
|
||||
Assert.IsFalse(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsDatabaseAvailable_WithDatabaseImmediatelyAvailable_ReturnsTrue()
|
||||
{
|
||||
var mockDatabaseFactory = new Mock<IUmbracoDatabaseFactory>();
|
||||
mockDatabaseFactory
|
||||
.Setup(x => x.CanConnect)
|
||||
.Returns(true);
|
||||
|
||||
var sut = CreateDefaultDatabaseAvailabilityCheck();
|
||||
var result = sut.IsDatabaseAvailable(mockDatabaseFactory.Object);
|
||||
Assert.IsTrue(result);
|
||||
}
|
||||
|
||||
[TestCase(5, true)]
|
||||
[TestCase(6, false)]
|
||||
public void IsDatabaseAvailable_WithDatabaseImmediatelyAvailableAfterMultipleAttempts_ReturnsExpectedResult(int attemptsUntilConnection, bool expectedResult)
|
||||
{
|
||||
if (attemptsUntilConnection < 1)
|
||||
{
|
||||
throw new ArgumentException($"{nameof(attemptsUntilConnection)} must be greater than or equal to 1.", nameof(attemptsUntilConnection));
|
||||
}
|
||||
|
||||
var attemptResults = new Queue<bool>();
|
||||
for (var i = 0; i < attemptsUntilConnection - 1; i++)
|
||||
{
|
||||
attemptResults.Enqueue(false);
|
||||
}
|
||||
|
||||
attemptResults.Enqueue(true);
|
||||
|
||||
var mockDatabaseFactory = new Mock<IUmbracoDatabaseFactory>();
|
||||
mockDatabaseFactory
|
||||
.Setup(x => x.CanConnect)
|
||||
.Returns(attemptResults.Dequeue);
|
||||
|
||||
var sut = CreateDefaultDatabaseAvailabilityCheck();
|
||||
var result = sut.IsDatabaseAvailable(mockDatabaseFactory.Object);
|
||||
Assert.AreEqual(expectedResult, result);
|
||||
}
|
||||
|
||||
private static DefaultDatabaseAvailabilityCheck CreateDefaultDatabaseAvailabilityCheck()
|
||||
=> new(new NullLogger<DefaultDatabaseAvailabilityCheck>())
|
||||
{
|
||||
AttemptDelayMilliseconds = 1 // Set to 1 ms for faster tests.
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user