Skip to content

Commit 0c22d51

Browse files
authored
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.
1 parent 240e155 commit 0c22d51

File tree

5 files changed

+174
-24
lines changed

5 files changed

+174
-24
lines changed

src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builde
9090
builder.AddNotificationAsyncHandler<RuntimeUnattendedUpgradeNotification, UnattendedUpgrader>();
9191
builder.AddNotificationAsyncHandler<RuntimePremigrationsUpgradeNotification, PremigrationUpgrader>();
9292

93+
// Database availability check.
94+
builder.Services.AddUnique<IDatabaseAvailabilityCheck, DefaultDatabaseAvailabilityCheck>();
95+
9396
// Add runtime mode validation
9497
builder.Services.AddSingleton<IRuntimeModeValidationService, RuntimeModeValidationService>();
9598
builder.RuntimeModeValidators()
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using Microsoft.Extensions.Logging;
2+
3+
namespace Umbraco.Cms.Infrastructure.Persistence;
4+
5+
/// <summary>
6+
/// Checks if a configured database is available on boot using the default method of 5 attempts with a 1 second delay between each one.
7+
/// </summary>
8+
internal class DefaultDatabaseAvailabilityCheck : IDatabaseAvailabilityCheck
9+
{
10+
private const int NumberOfAttempts = 5;
11+
private const int DefaultAttemptDelayMilliseconds = 1000;
12+
13+
private readonly ILogger<DefaultDatabaseAvailabilityCheck> _logger;
14+
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="DefaultDatabaseAvailabilityCheck"/> class.
17+
/// </summary>
18+
/// <param name="logger"></param>
19+
public DefaultDatabaseAvailabilityCheck(ILogger<DefaultDatabaseAvailabilityCheck> logger) => _logger = logger;
20+
21+
/// <summary>
22+
/// Gets or sets the number of milliseconds to delay between attempts.
23+
/// </summary>
24+
/// <remarks>
25+
/// Exposed for testing purposes, hence settable only internally.
26+
/// </remarks>
27+
public int AttemptDelayMilliseconds { get; internal set; } = DefaultAttemptDelayMilliseconds;
28+
29+
/// <inheritdoc/>
30+
public bool IsDatabaseAvailable(IUmbracoDatabaseFactory databaseFactory)
31+
{
32+
bool canConnect;
33+
for (var i = 0; ;)
34+
{
35+
canConnect = databaseFactory.CanConnect;
36+
if (canConnect || ++i == NumberOfAttempts)
37+
{
38+
break;
39+
}
40+
41+
if (_logger.IsEnabled(LogLevel.Debug))
42+
{
43+
_logger.LogDebug("Could not immediately connect to database, trying again.");
44+
}
45+
46+
// Wait for the configured time before trying again.
47+
Thread.Sleep(AttemptDelayMilliseconds);
48+
}
49+
50+
return canConnect;
51+
}
52+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace Umbraco.Cms.Infrastructure.Persistence;
2+
3+
/// <summary>
4+
/// Checks if a configured database is available on boot.
5+
/// </summary>
6+
public interface IDatabaseAvailabilityCheck
7+
{
8+
/// <summary>
9+
/// Checks if the database is available for Umbraco to boot.
10+
/// </summary>
11+
/// <param name="databaseFactory">The <see cref="IUmbracoDatabaseFactory"/>.</param>
12+
/// <returns>
13+
/// A value indicating whether the database is available.
14+
/// </returns>
15+
bool IsDatabaseAvailable(IUmbracoDatabaseFactory databaseFactory);
16+
}

src/Umbraco.Infrastructure/Runtime/RuntimeState.cs

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public class RuntimeState : IRuntimeState
3131
private readonly IConflictingRouteService _conflictingRouteService = null!;
3232
private readonly IEnumerable<IDatabaseProviderMetadata> _databaseProviderMetadata = null!;
3333
private readonly IRuntimeModeValidationService _runtimeModeValidationService = null!;
34+
private readonly IDatabaseAvailabilityCheck _databaseAvailabilityCheck = null!;
3435

3536
/// <summary>
3637
/// The initial <see cref="RuntimeState"/>
@@ -46,6 +47,7 @@ private RuntimeState()
4647
/// <summary>
4748
/// Initializes a new instance of the <see cref="RuntimeState" /> class.
4849
/// </summary>
50+
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
4951
public RuntimeState(
5052
IOptions<GlobalSettings> globalSettings,
5153
IOptions<UnattendedSettings> unattendedSettings,
@@ -56,6 +58,34 @@ public RuntimeState(
5658
IConflictingRouteService conflictingRouteService,
5759
IEnumerable<IDatabaseProviderMetadata> databaseProviderMetadata,
5860
IRuntimeModeValidationService runtimeModeValidationService)
61+
: this(
62+
globalSettings,
63+
unattendedSettings,
64+
umbracoVersion,
65+
databaseFactory,
66+
logger,
67+
packageMigrationState,
68+
conflictingRouteService,
69+
databaseProviderMetadata,
70+
runtimeModeValidationService,
71+
StaticServiceProvider.Instance.GetRequiredService<IDatabaseAvailabilityCheck>())
72+
{
73+
}
74+
75+
/// <summary>
76+
/// Initializes a new instance of the <see cref="RuntimeState" /> class.
77+
/// </summary>
78+
public RuntimeState(
79+
IOptions<GlobalSettings> globalSettings,
80+
IOptions<UnattendedSettings> unattendedSettings,
81+
IUmbracoVersion umbracoVersion,
82+
IUmbracoDatabaseFactory databaseFactory,
83+
ILogger<RuntimeState> logger,
84+
PendingPackageMigrations packageMigrationState,
85+
IConflictingRouteService conflictingRouteService,
86+
IEnumerable<IDatabaseProviderMetadata> databaseProviderMetadata,
87+
IRuntimeModeValidationService runtimeModeValidationService,
88+
IDatabaseAvailabilityCheck databaseAvailabilityCheck)
5989
{
6090
_globalSettings = globalSettings;
6191
_unattendedSettings = unattendedSettings;
@@ -66,6 +96,7 @@ public RuntimeState(
6696
_conflictingRouteService = conflictingRouteService;
6797
_databaseProviderMetadata = databaseProviderMetadata;
6898
_runtimeModeValidationService = runtimeModeValidationService;
99+
_databaseAvailabilityCheck = databaseAvailabilityCheck;
69100
}
70101

71102
/// <inheritdoc />
@@ -242,7 +273,7 @@ private UmbracoDatabaseState GetUmbracoDatabaseState(IUmbracoDatabaseFactory dat
242273
{
243274
try
244275
{
245-
if (!TryDbConnect(databaseFactory))
276+
if (_databaseAvailabilityCheck.IsDatabaseAvailable(databaseFactory) is false)
246277
{
247278
return UmbracoDatabaseState.CannotConnect;
248279
}
@@ -305,27 +336,4 @@ private bool DoesUmbracoRequireUpgrade(IReadOnlyDictionary<string, string?>? key
305336
}
306337
return CurrentMigrationState != FinalMigrationState;
307338
}
308-
309-
private bool TryDbConnect(IUmbracoDatabaseFactory databaseFactory)
310-
{
311-
// anything other than install wants a database - see if we can connect
312-
// (since this is an already existing database, assume localdb is ready)
313-
bool canConnect;
314-
var tries = 5;
315-
for (var i = 0; ;)
316-
{
317-
canConnect = databaseFactory.CanConnect;
318-
if (canConnect || ++i == tries)
319-
{
320-
break;
321-
}
322-
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
323-
{
324-
_logger.LogDebug("Could not immediately connect to database, trying again.");
325-
}
326-
Thread.Sleep(1000);
327-
}
328-
329-
return canConnect;
330-
}
331339
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using Microsoft.Extensions.Logging.Abstractions;
2+
using Moq;
3+
using NUnit.Framework;
4+
using Umbraco.Cms.Core.Install.Models;
5+
using Umbraco.Cms.Infrastructure.Persistence;
6+
using Umbraco.Cms.Persistence.SqlServer.Services;
7+
8+
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence;
9+
10+
[TestFixture]
11+
public class DefaultDatabaseAvailabilityCheckTests
12+
{
13+
[Test]
14+
public void IsDatabaseAvailable_WithDatabaseUnavailable_ReturnsFalse()
15+
{
16+
var mockDatabaseFactory = new Mock<IUmbracoDatabaseFactory>();
17+
mockDatabaseFactory
18+
.Setup(x => x.CanConnect)
19+
.Returns(false);
20+
21+
var sut = CreateDefaultDatabaseAvailabilityCheck();
22+
var result = sut.IsDatabaseAvailable(mockDatabaseFactory.Object);
23+
Assert.IsFalse(result);
24+
}
25+
26+
[Test]
27+
public void IsDatabaseAvailable_WithDatabaseImmediatelyAvailable_ReturnsTrue()
28+
{
29+
var mockDatabaseFactory = new Mock<IUmbracoDatabaseFactory>();
30+
mockDatabaseFactory
31+
.Setup(x => x.CanConnect)
32+
.Returns(true);
33+
34+
var sut = CreateDefaultDatabaseAvailabilityCheck();
35+
var result = sut.IsDatabaseAvailable(mockDatabaseFactory.Object);
36+
Assert.IsTrue(result);
37+
}
38+
39+
[TestCase(5, true)]
40+
[TestCase(6, false)]
41+
public void IsDatabaseAvailable_WithDatabaseImmediatelyAvailableAfterMultipleAttempts_ReturnsExpectedResult(int attemptsUntilConnection, bool expectedResult)
42+
{
43+
if (attemptsUntilConnection < 1)
44+
{
45+
throw new ArgumentException($"{nameof(attemptsUntilConnection)} must be greater than or equal to 1.", nameof(attemptsUntilConnection));
46+
}
47+
48+
var attemptResults = new Queue<bool>();
49+
for (var i = 0; i < attemptsUntilConnection - 1; i++)
50+
{
51+
attemptResults.Enqueue(false);
52+
}
53+
54+
attemptResults.Enqueue(true);
55+
56+
var mockDatabaseFactory = new Mock<IUmbracoDatabaseFactory>();
57+
mockDatabaseFactory
58+
.Setup(x => x.CanConnect)
59+
.Returns(attemptResults.Dequeue);
60+
61+
var sut = CreateDefaultDatabaseAvailabilityCheck();
62+
var result = sut.IsDatabaseAvailable(mockDatabaseFactory.Object);
63+
Assert.AreEqual(expectedResult, result);
64+
}
65+
66+
private static DefaultDatabaseAvailabilityCheck CreateDefaultDatabaseAvailabilityCheck()
67+
=> new(new NullLogger<DefaultDatabaseAvailabilityCheck>())
68+
{
69+
AttemptDelayMilliseconds = 1 // Set to 1 ms for faster tests.
70+
};
71+
}

0 commit comments

Comments
 (0)