Skip to content

Commit 3fb695d

Browse files
Odonnoaaronpowell
andauthored
fix(surrealdb): improve surrealdb healthchecks (#795)
* fix(surrealdb): improve surrealdb healthchecks * fix(surrealdb): escape password value * Adding some logging statements in the health check for diagnostics * Bit more diag messaging * fix: new client for each healthcheck call * fix: wait until ns created --------- Co-authored-by: Aaron Powell <[email protected]>
1 parent 2e2efac commit 3fb695d

File tree

5 files changed

+119
-86
lines changed

5 files changed

+119
-86
lines changed

src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbBuilderExtensions.cs

Lines changed: 92 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,16 @@ public static IResourceBuilder<SurrealDbServerResource> AddSurrealServer(
7272
{
7373
args.Add("--strict");
7474
}
75-
75+
7676
// The password must be at least 8 characters long and contain characters from three of the following four sets: Uppercase letters, Lowercase letters, Base 10 digits, and Symbols
7777
var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-password", minLower: 1, minUpper: 1, minNumeric: 1);
7878

7979
string imageTag = builder.ExecutionContext.IsRunMode
8080
? $"{SurrealDbContainerImageTags.Tag}-dev"
8181
: SurrealDbContainerImageTags.Tag;
82-
82+
8383
var surrealServer = new SurrealDbServerResource(name, userName?.Resource, passwordParameter);
84-
84+
8585
return builder.AddResource(surrealServer)
8686
.WithEndpoint(port: port, targetPort: SurrealDbPort, name: SurrealDbServerResource.PrimaryEndpointName)
8787
.WithImage(SurrealDbContainerImageTags.Image, imageTag)
@@ -99,42 +99,65 @@ public static IResourceBuilder<SurrealDbServerResource> AddSurrealServer(
9999
{
100100
return;
101101
}
102-
102+
103103
var connectionString = await surrealServer.GetConnectionStringAsync(ct).ConfigureAwait(false);
104104
if (connectionString is null)
105105
{
106106
throw new DistributedApplicationException($"ResourceReadyEvent was published for the '{surrealServer.Name}' resource but the connection string was null.");
107107
}
108-
109-
var options = new SurrealDbOptionsBuilder().FromConnectionString(connectionString).Build();
110-
await using var surrealClient = new SurrealDbClient(options);
111-
112-
foreach (var nsResourceName in surrealServer.Namespaces.Keys)
113-
{
114-
if (builder.Resources.FirstOrDefault(n =>
115-
string.Equals(n.Name, nsResourceName, StringComparison.OrdinalIgnoreCase)) is
116-
SurrealDbNamespaceResource surrealDbNamespace)
117-
{
118-
await CreateNamespaceAsync(surrealClient, surrealDbNamespace, @event.Services, ct)
119-
.ConfigureAwait(false);
120-
121-
await surrealClient.Use(surrealDbNamespace.NamespaceName, null!, ct).ConfigureAwait(false);
122-
123-
foreach (var dbResourceName in surrealDbNamespace.Databases.Keys)
124-
{
125-
if (builder.Resources.FirstOrDefault(n =>
126-
string.Equals(n.Name, dbResourceName, StringComparison.OrdinalIgnoreCase)) is
127-
SurrealDbDatabaseResource surrealDbDatabase)
128-
{
129-
await CreateDatabaseAsync(surrealClient, surrealDbDatabase, @event.Services, ct)
130-
.ConfigureAwait(false);
131-
}
132-
}
133-
}
134-
}
108+
109+
await EnsuresNsDbCreated(builder, connectionString, surrealServer, @event.Services, ct);
135110
});
136111
}
137112

113+
private static async Task EnsuresNsDbCreated(
114+
IDistributedApplicationBuilder builder,
115+
string connectionString,
116+
SurrealDbServerResource surrealServer,
117+
IServiceProvider services,
118+
CancellationToken ct
119+
)
120+
{
121+
var options = new SurrealDbOptionsBuilder().FromConnectionString(connectionString).Build();
122+
await using var surrealClient = new SurrealDbClient(options);
123+
124+
foreach (var nsResourceName in surrealServer.Namespaces.Keys)
125+
{
126+
if (builder.Resources.FirstOrDefault(n =>
127+
string.Equals(n.Name, nsResourceName, StringComparison.OrdinalIgnoreCase)) is
128+
SurrealDbNamespaceResource surrealDbNamespace)
129+
{
130+
await CreateNamespaceAsync(surrealClient, surrealDbNamespace, services, ct)
131+
.ConfigureAwait(false);
132+
133+
// 💡 Wait until the Namespace is really created?!
134+
while (!ct.IsCancellationRequested)
135+
{
136+
try
137+
{
138+
await surrealClient.Use(surrealDbNamespace.NamespaceName, null!, ct).ConfigureAwait(false);
139+
break;
140+
}
141+
catch
142+
{
143+
await Task.Delay(200, ct).ConfigureAwait(false);
144+
}
145+
}
146+
147+
foreach (var dbResourceName in surrealDbNamespace.Databases.Keys)
148+
{
149+
if (builder.Resources.FirstOrDefault(n =>
150+
string.Equals(n.Name, dbResourceName, StringComparison.OrdinalIgnoreCase)) is
151+
SurrealDbDatabaseResource surrealDbDatabase)
152+
{
153+
await CreateDatabaseAsync(surrealClient, surrealDbDatabase, services, ct)
154+
.ConfigureAwait(false);
155+
}
156+
}
157+
}
158+
}
159+
}
160+
138161
/// <summary>
139162
/// Adds a SurrealDB namespace to the application model. This is a child resource of a <see cref="SurrealDbServerResource"/>.
140163
/// </summary>
@@ -174,7 +197,7 @@ public static IResourceBuilder<SurrealDbNamespaceResource> AddNamespace(
174197
var surrealServerNamespace = new SurrealDbNamespaceResource(name, namespaceName, builder.Resource);
175198
return builder.ApplicationBuilder.AddResource(surrealServerNamespace);
176199
}
177-
200+
178201
/// <summary>
179202
/// Defines the SQL script used to create the namespace.
180203
/// </summary>
@@ -232,38 +255,34 @@ public static IResourceBuilder<SurrealDbDatabaseResource> AddDatabase(
232255
builder.Resource.AddDatabase(name, databaseName);
233256
var surrealServerDatabase = new SurrealDbDatabaseResource(name, databaseName, builder.Resource);
234257

235-
SurrealDbClient? surrealDbClient = null;
236-
237-
builder.ApplicationBuilder.Eventing.Subscribe<ConnectionStringAvailableEvent>(surrealServerDatabase, async (@event, ct) =>
238-
{
239-
var connectionString = await surrealServerDatabase.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
240-
if (connectionString is null)
241-
{
242-
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{surrealServerDatabase}' resource but the connection string was null.");
243-
}
244-
245-
var options = new SurrealDbOptionsBuilder().FromConnectionString(connectionString).Build();
246-
surrealDbClient = new SurrealDbClient(options);
247-
});
258+
SurrealDbOptions? surrealDbOptions = null;
248259

249260
string namespaceName = builder.Resource.Name;
250261
string serverName = builder.Resource.Parent.Name;
251262

252263
string healthCheckKey = $"{serverName}_{namespaceName}_{name}_check";
253-
// TODO : Bug to be fixed
254-
//builder.ApplicationBuilder.Services.AddHealthChecks().AddSurreal(_ => surrealDbClient!, healthCheckKey);
255264
builder.ApplicationBuilder.Services.AddHealthChecks().Add(new HealthCheckRegistration(
256265
name: healthCheckKey,
257-
_ => new SurrealDbHealthCheck(surrealDbClient!),
266+
sp => new SurrealDbHealthCheck(surrealDbOptions!, sp.GetRequiredService<ILogger<SurrealDbHealthCheck>>()),
258267
failureStatus: null,
259268
tags: null
260269
)
261270
);
262-
271+
263272
return builder.ApplicationBuilder.AddResource(surrealServerDatabase)
264-
.WithHealthCheck(healthCheckKey);
273+
.WithHealthCheck(healthCheckKey)
274+
.OnConnectionStringAvailable(async (_, _, ct) =>
275+
{
276+
var connectionString = await surrealServerDatabase.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
277+
if (connectionString is null)
278+
{
279+
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{surrealServerDatabase}' resource but the connection string was null.");
280+
}
281+
282+
surrealDbOptions = new SurrealDbOptionsBuilder().FromConnectionString(connectionString).Build();
283+
});
265284
}
266-
285+
267286
/// <summary>
268287
/// Defines the SQL script used to create the database.
269288
/// </summary>
@@ -347,7 +366,7 @@ public static IResourceBuilder<SurrealDbServerResource> WithDataBindMount(this I
347366

348367
return builder.WithBindMount(source, "/data");
349368
}
350-
369+
351370
/// <summary>
352371
/// Copies init files into a SurrealDB container resource.
353372
/// </summary>
@@ -367,10 +386,10 @@ public static IResourceBuilder<SurrealDbServerResource> WithInitFiles(this IReso
367386
{
368387
throw new DistributedApplicationException($"Unable to determine the file name for '{source}'.");
369388
}
370-
389+
371390
string fileName = Path.GetFileName(importFullPath);
372391
string initFilePath = $"{initPath}/{fileName}";
373-
392+
374393
return builder
375394
.WithContainerFiles(initPath, importFullPath)
376395
.WithEnvironment(context =>
@@ -413,16 +432,16 @@ public static IResourceBuilder<T> WithSurrealist<T>(
413432
.WithHttpEndpoint(targetPort: 8080, name: "http")
414433
.WithRelationship(builder.Resource, "Surrealist")
415434
.ExcludeFromManifest();
416-
435+
417436
surrealistContainerBuilder.WithContainerFiles(
418437
destinationPath: "/usr/share/nginx/html",
419438
callback: async (_, cancellationToken) =>
420439
{
421-
var surrealDbServerInstances =
440+
var surrealDbServerInstances =
422441
builder.ApplicationBuilder.Resources.OfType<SurrealDbServerResource>().ToList();
423-
var surrealDbNamespaceResources =
442+
var surrealDbNamespaceResources =
424443
builder.ApplicationBuilder.Resources.OfType<SurrealDbNamespaceResource>().ToList();
425-
var surrealDbDatabaseResources =
444+
var surrealDbDatabaseResources =
426445
builder.ApplicationBuilder.Resources.OfType<SurrealDbDatabaseResource>().ToList();
427446

428447
return [
@@ -431,8 +450,8 @@ public static IResourceBuilder<T> WithSurrealist<T>(
431450
Name = "instance.json",
432451
Contents = await WriteSurrealistInstanceJson(
433452
surrealDbServerInstances,
434-
surrealDbNamespaceResources,
435-
surrealDbDatabaseResources,
453+
surrealDbNamespaceResources,
454+
surrealDbDatabaseResources,
436455
cancellationToken
437456
).ConfigureAwait(false),
438457
},
@@ -443,7 +462,7 @@ public static IResourceBuilder<T> WithSurrealist<T>(
443462

444463
return builder;
445464
}
446-
465+
447466
private static async Task<string> WriteSurrealistInstanceJson(
448467
IList<SurrealDbServerResource> surrealDbServerInstances,
449468
IList<SurrealDbNamespaceResource> surrealDbNamespaceResources,
@@ -522,16 +541,16 @@ CancellationToken cancellationToken
522541
writer.WriteEndArray();
523542

524543
writer.WriteEndObject();
525-
544+
526545
await writer.FlushAsync(cancellationToken);
527-
546+
528547
return Encoding.UTF8.GetString(stream.ToArray());
529548
}
530-
549+
531550
private static async Task CreateNamespaceAsync(
532-
SurrealDbClient surrealClient,
533-
SurrealDbNamespaceResource namespaceResource,
534-
IServiceProvider serviceProvider,
551+
SurrealDbClient surrealClient,
552+
SurrealDbNamespaceResource namespaceResource,
553+
IServiceProvider serviceProvider,
535554
CancellationToken cancellationToken
536555
)
537556
{
@@ -543,7 +562,7 @@ CancellationToken cancellationToken
543562
try
544563
{
545564
var response = await surrealClient.RawQuery(
546-
scriptAnnotation?.Script ?? $"DEFINE NAMESPACE IF NOT EXISTS `{namespaceResource.NamespaceName}`;",
565+
scriptAnnotation?.Script ?? $"DEFINE NAMESPACE IF NOT EXISTS `{namespaceResource.NamespaceName}`;",
547566
cancellationToken: cancellationToken
548567
).ConfigureAwait(false);
549568

@@ -556,11 +575,11 @@ CancellationToken cancellationToken
556575
logger.LogError(e, "Failed to create namespace '{NamespaceName}'", namespaceResource.NamespaceName);
557576
}
558577
}
559-
578+
560579
private static async Task CreateDatabaseAsync(
561-
SurrealDbClient surrealClient,
562-
SurrealDbDatabaseResource databaseResource,
563-
IServiceProvider serviceProvider,
580+
SurrealDbClient surrealClient,
581+
SurrealDbDatabaseResource databaseResource,
582+
IServiceProvider serviceProvider,
564583
CancellationToken cancellationToken
565584
)
566585
{
@@ -572,7 +591,7 @@ CancellationToken cancellationToken
572591
try
573592
{
574593
var response = await surrealClient.RawQuery(
575-
scriptAnnotation?.Script ?? $"DEFINE DATABASE IF NOT EXISTS `{databaseResource.DatabaseName}`;",
594+
scriptAnnotation?.Script ?? $"DEFINE DATABASE IF NOT EXISTS `{databaseResource.DatabaseName}`;",
576595
cancellationToken: cancellationToken
577596
).ConfigureAwait(false);
578597

src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbServerResource.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ UserNameParameter is not null ?
5454

5555
private ReferenceExpression ConnectionString =>
5656
ReferenceExpression.Create(
57-
$"Server={SchemeUri}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}/rpc;User={UserNameReference};Password={PasswordParameter}");
57+
$"Server={SchemeUri}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}/rpc;User={UserNameReference};Password='{PasswordParameter}'");
5858

5959
/// <summary>
6060
/// Gets the connection string expression for the SurrealDB instance.

src/CommunityToolkit.Aspire.SurrealDb/AspireSurrealDbExtensions.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Microsoft.Extensions.Configuration;
77
using Microsoft.Extensions.DependencyInjection;
88
using Microsoft.Extensions.Diagnostics.HealthChecks;
9+
using Microsoft.Extensions.Logging;
910
using SurrealDb.Net;
1011

1112
namespace Microsoft.Extensions.Hosting;
@@ -16,7 +17,7 @@ namespace Microsoft.Extensions.Hosting;
1617
public static class AspireSurrealDbExtensions
1718
{
1819
private const string DefaultConfigSectionName = "Aspire:Surreal:Client";
19-
20+
2021
/// <summary>
2122
/// Registers <see cref="SurrealDbClient" /> in the services provided by the <paramref name="builder"/>.
2223
/// </summary>
@@ -97,9 +98,9 @@ private static void AddSurrealClient(
9798

9899
builder.TryAddHealthCheck(new HealthCheckRegistration(
99100
healthCheckName,
100-
sp => new SurrealDbHealthCheck(serviceKey is null ?
101-
sp.GetRequiredService<SurrealDbClient>() :
102-
sp.GetRequiredKeyedService<SurrealDbClient>(serviceKey)),
101+
sp => new SurrealDbHealthCheck(
102+
settings.Options,
103+
sp.GetRequiredService<ILogger<SurrealDbHealthCheck>>()),
103104
failureStatus: null,
104105
tags: null,
105106
timeout: settings.HealthCheckTimeout > 0 ? TimeSpan.FromMilliseconds(settings.HealthCheckTimeout.Value) : null

src/CommunityToolkit.Aspire.SurrealDb/SurrealDbHealthCheck.cs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,47 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.Extensions.DependencyInjection;
45
using SurrealDb.Net;
56
using Microsoft.Extensions.Diagnostics.HealthChecks;
7+
using Microsoft.Extensions.Logging;
68

79
namespace CommunityToolkit.Aspire.SurrealDb;
810

911
internal sealed class SurrealDbHealthCheck : IHealthCheck
1012
{
11-
private readonly ISurrealDbClient _surrealdbClient;
13+
private readonly SurrealDbOptions _options;
14+
private readonly ILogger<SurrealDbHealthCheck> _logger;
1215

13-
public SurrealDbHealthCheck(ISurrealDbClient surrealdbClient)
16+
public SurrealDbHealthCheck(SurrealDbOptions options, ILogger<SurrealDbHealthCheck> logger)
1417
{
15-
ArgumentNullException.ThrowIfNull(surrealdbClient, nameof(surrealdbClient));
16-
_surrealdbClient = surrealdbClient;
18+
_options = options;
19+
_logger = logger;
1720
}
1821

1922
/// <inheritdoc />
2023
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
2124
{
25+
bool isHealthy = false;
26+
2227
try
2328
{
24-
bool isHealthy = await _surrealdbClient.Health(cancellationToken).ConfigureAwait(false);
29+
await using var surrealdbClient = new SurrealDbClient(_options);
30+
31+
isHealthy = await surrealdbClient.Health(cancellationToken).ConfigureAwait(false);
32+
var response = await surrealdbClient.RawQuery("RETURN 1", cancellationToken: cancellationToken).ConfigureAwait(false);
33+
response.EnsureAllOks();
34+
35+
_logger.LogInformation("SurrealDB health check passed. Response: {Response}", response);
36+
_logger.LogInformation("SurrealDB health check outcome: {Outcome}", isHealthy ? "Healthy" : "Unhealthy");
2537

2638
return isHealthy
2739
? HealthCheckResult.Healthy()
2840
: new HealthCheckResult(context.Registration.FailureStatus);
2941
}
3042
catch (Exception ex)
3143
{
44+
_logger.LogError(ex, "SurrealDB health check raised an exception. Health check had previously reported: {Outcome}. CancellationToken status: {CancellationTokenStatus}", isHealthy ? "Healthy" : "Unhealthy", cancellationToken.IsCancellationRequested ? "Canceled" : "Active");
3245
return new HealthCheckResult(context.Registration.FailureStatus, exception: ex);
3346
}
3447
}

0 commit comments

Comments
 (0)