Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f4e3bc3

Browse files
mitchdennyAlirexaa
andauthored
Alirexaa/qdrant waitfor (#6044)
* WaitFor for Qdrant * Address PR feedback * Address PR feedback * Don't use logger factory. --------- Co-authored-by: Alireza Baloochi <[email protected]>
1 parent 865775b commit f4e3bc3

File tree

6 files changed

+144
-2
lines changed

6 files changed

+144
-2
lines changed

playground/Qdrant/Qdrant.AppHost/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
builder.AddProject<Projects.Qdrant_ApiService>("apiservice")
1010
.WithExternalHttpEndpoints()
11-
.WithReference(qdrant);
11+
.WithReference(qdrant)
12+
.WaitFor(qdrant);
1213

1314
builder.Build().Run();

src/Aspire.Hosting.Qdrant/Aspire.Hosting.Qdrant.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<ItemGroup>
1616
<Compile Include="$(SharedDir)StringComparers.cs" Link="Utils\StringComparers.cs" />
1717
<Compile Include="$(SharedDir)VolumeNameGenerator.cs" Link="Utils\VolumeNameGenerator.cs" />
18+
<Compile Include="$(ComponentsDir)Aspire.Qdrant.Client\QdrantHealthCheck.cs" Link="QdrantHealthCheck.cs"></Compile>
1819
</ItemGroup>
1920

2021
<ItemGroup>

src/Aspire.Hosting.Qdrant/QdrantBuilderExtensions.cs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
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 System.Data.Common;
45
using Aspire.Hosting.ApplicationModel;
56
using Aspire.Hosting.Qdrant;
67
using Aspire.Hosting.Utils;
8+
using Aspire.Qdrant.Client;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Diagnostics.HealthChecks;
711

812
namespace Aspire.Hosting;
913

@@ -42,6 +46,25 @@ public static IResourceBuilder<QdrantServerResource> AddQdrant(this IDistributed
4246
var apiKeyParameter = apiKey?.Resource ??
4347
ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-Key", special: false);
4448
var qdrant = new QdrantServerResource(name, apiKeyParameter);
49+
50+
HttpClient? httpClient = null;
51+
52+
builder.Eventing.Subscribe<ConnectionStringAvailableEvent>(qdrant, async (@event, ct) =>
53+
{
54+
var connectionString = await qdrant.HttpConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false)
55+
?? throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{qdrant.Name}' resource but the connection string was null.");
56+
httpClient = CreateQdrantHttpClient(connectionString);
57+
});
58+
59+
var healthCheckKey = $"{name}_check";
60+
builder.Services.AddHealthChecks()
61+
.Add(new HealthCheckRegistration(
62+
healthCheckKey,
63+
sp => new QdrantHealthCheck(httpClient!),
64+
failureStatus: default,
65+
tags: default,
66+
timeout: default));
67+
4568
return builder.AddResource(qdrant)
4669
.WithImage(QdrantContainerImageTags.Image, QdrantContainerImageTags.Tag)
4770
.WithImageRegistry(QdrantContainerImageTags.Registry)
@@ -61,7 +84,8 @@ public static IResourceBuilder<QdrantServerResource> AddQdrant(this IDistributed
6184
{
6285
context.EnvironmentVariables[EnableStaticContentEnvVarName] = "0";
6386
}
64-
});
87+
})
88+
.WithHealthCheck(healthCheckKey);
6589
}
6690

6791
/// <summary>
@@ -117,4 +141,45 @@ public static IResourceBuilder<TDestination> WithReference<TDestination>(this IR
117141

118142
return builder;
119143
}
144+
145+
private static HttpClient CreateQdrantHttpClient(string? connectionString)
146+
{
147+
if (connectionString is null)
148+
{
149+
throw new InvalidOperationException("Connection string is unavailable");
150+
}
151+
152+
Uri? endpoint = null;
153+
string? key = null;
154+
155+
if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
156+
{
157+
endpoint = uri;
158+
}
159+
else
160+
{
161+
var connectionBuilder = new DbConnectionStringBuilder
162+
{
163+
ConnectionString = connectionString
164+
};
165+
166+
if (connectionBuilder.TryGetValue("Endpoint", out var endpointValue) && Uri.TryCreate(endpointValue.ToString(), UriKind.Absolute, out var serviceUri))
167+
{
168+
endpoint = serviceUri;
169+
}
170+
171+
if (connectionBuilder.TryGetValue("Key", out var keyValue))
172+
{
173+
key = keyValue.ToString();
174+
}
175+
}
176+
177+
var client = new HttpClient();
178+
client.BaseAddress = endpoint;
179+
if (key is not null)
180+
{
181+
client.DefaultRequestHeaders.Add("Api-Key", key);
182+
}
183+
return client;
184+
}
120185
}

src/Components/Aspire.Qdrant.Client/Aspire.Qdrant.Client.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<PackageReference Include="Qdrant.Client" />
2121
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
2222
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
23+
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
2324
</ItemGroup>
2425

2526
</Project>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Extensions.Diagnostics.HealthChecks;
5+
6+
namespace Aspire.Qdrant.Client;
7+
internal sealed class QdrantHealthCheck : IHealthCheck
8+
{
9+
private readonly HttpClient _client;
10+
11+
public QdrantHealthCheck(HttpClient client)
12+
{
13+
ArgumentNullException.ThrowIfNull(client, nameof(client));
14+
_client = client;
15+
}
16+
17+
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
18+
{
19+
try
20+
{
21+
var response = await _client.GetAsync("/readyz", cancellationToken).ConfigureAwait(false);
22+
23+
return response.IsSuccessStatusCode
24+
? HealthCheckResult.Healthy()
25+
: new HealthCheckResult(HealthStatus.Unhealthy);
26+
}
27+
catch (Exception ex)
28+
{
29+
return new HealthCheckResult(context.Registration.FailureStatus, exception: ex);
30+
}
31+
}
32+
}

tests/Aspire.Hosting.Qdrant.Tests/QdrantFunctionalTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Aspire.Components.Common.Tests;
5+
using Aspire.Hosting.ApplicationModel;
56
using Aspire.Hosting.Utils;
67
using Grpc.Core;
78
using Microsoft.Extensions.Configuration;
89
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Diagnostics.HealthChecks;
911
using Microsoft.Extensions.Hosting;
1012
using Polly;
1113
using Qdrant.Client;
@@ -216,4 +218,44 @@ await pipeline.ExecuteAsync(async token =>
216218
}
217219
}
218220
}
221+
222+
[Fact]
223+
[RequiresDocker]
224+
public async Task VerifyWaitForOnQdrantBlocksDependentResources()
225+
{
226+
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
227+
using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);
228+
229+
var healthCheckTcs = new TaskCompletionSource<HealthCheckResult>();
230+
builder.Services.AddHealthChecks().AddAsyncCheck("blocking_check", () =>
231+
{
232+
return healthCheckTcs.Task;
233+
});
234+
235+
var resource = builder.AddQdrant("resource")
236+
.WithHealthCheck("blocking_check");
237+
238+
var dependentResource = builder.AddQdrant("dependentresource")
239+
.WaitFor(resource);
240+
241+
using var app = builder.Build();
242+
243+
var pendingStart = app.StartAsync(cts.Token);
244+
245+
var rns = app.Services.GetRequiredService<ResourceNotificationService>();
246+
247+
await rns.WaitForResourceAsync(resource.Resource.Name, KnownResourceStates.Running, cts.Token);
248+
249+
await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Waiting, cts.Token);
250+
251+
healthCheckTcs.SetResult(HealthCheckResult.Healthy());
252+
253+
await rns.WaitForResourceAsync(resource.Resource.Name, (re => re.Snapshot.HealthStatus == HealthStatus.Healthy), cts.Token);
254+
255+
await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Running, cts.Token);
256+
257+
await pendingStart;
258+
259+
await app.StopAsync();
260+
}
219261
}

0 commit comments

Comments
 (0)