Skip to content

Commit b27846b

Browse files
committed
Use complete native readiness and stable mount identities
Share Aspire health and bootstrap completion readiness across the workload client, native controls and resource observation. Compare full mount source/target/type identities independently of Docker list order while retaining actual-change guards. Exercise MongoDB replica readiness and million-document OpenSearch controls; retain bounded rejection types. Keep the full workflow group and gates, and run the same native controls in an independent contracts group. Verified: canonical full Debug solution build with -warnaserror (21 projects, zero warnings/errors), full format verification, governance and actionlint. Local million-document OpenSearch controls passed at one/three nodes, and the original MongoDB three-node readiness failure passed after correction. Linux sustained OpenSearch n3 HTTP 429 remains unresolved pending exact rejection evidence; no retry or server-setting change.
1 parent 15545fb commit b27846b

12 files changed

Lines changed: 86 additions & 49 deletions

File tree

‎.github/workflows/benchmarks.yml‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ on:
55
workflow_dispatch:
66
inputs:
77
mode:
8-
description: Run harness contracts or the full database suite
8+
description: Run harness/native controls or the full database suite
99
type: choice
1010
options: [contracts, full]
1111
default: full
1212
permissions: {contents: read, actions: read}
1313
concurrency:
14-
group: keyload-benchmarks-${{ github.ref }}
14+
group: keyload-benchmarks-${{ github.ref }}${{ github.event_name == 'workflow_dispatch' && inputs.mode == 'contracts' && '-contracts' || '' }}
1515
cancel-in-progress: false
1616
env:
1717
NUGET_PACKAGES: ${{ github.workspace }}/artifacts/benchmark-runtime/packages
@@ -71,7 +71,6 @@ jobs:
7171
if-no-files-found: error
7272
retention-days: 90
7373
- name: Check Docker
74-
if: (github.event_name == 'push' || inputs.mode == 'full')
7574
run: docker version
7675
- name: Resolve the latest released KeyLoad server image to one digest
7776
id: keyload-image
@@ -111,8 +110,7 @@ jobs:
111110
compression-level: 0
112111
retention-days: 90
113112
- name: Test external database bulk operations and joined cleanup
114-
if: (github.event_name == 'push' || inputs.mode == 'full')
115-
run: dotnet benchmarks/KeyLoad.ComparisonHost/bin/Release/net10.0/KeyLoad.ComparisonHost.dll --output Detailed --maximum-parallel-tests 1 --minimum-expected-tests 5 --treenode-filter '/*/*/NativeDatabaseTests/*' --results-directory "$GITHUB_WORKSPACE/benchmarks/KeyLoad.ComparisonHost/bin/Release/net10.0/TestResults/NativeAdapters" --report-trx --timeout 20m
113+
run: dotnet benchmarks/KeyLoad.ComparisonHost/bin/Release/net10.0/KeyLoad.ComparisonHost.dll --output Detailed --maximum-parallel-tests 1 --minimum-expected-tests 6 --treenode-filter '/*/*/NativeDatabaseTests/*' --results-directory "$GITHUB_WORKSPACE/benchmarks/KeyLoad.ComparisonHost/bin/Release/net10.0/TestResults/NativeAdapters" --report-trx --timeout 20m
116114
- name: Test bounded terminal execution diagnostics
117115
run: dotnet benchmarks/KeyLoad.ComparisonHost/bin/Release/net10.0/KeyLoad.ComparisonHost.dll --output Detailed --minimum-expected-tests 1 --treenode-filter '/*/*/ComparisonHostCaptureTests/*' --results-directory "$GITHUB_WORKSPACE/benchmarks/KeyLoad.ComparisonHost/bin/Release/net10.0/TestResults/TerminalDiagnostics" --report-trx
118116
- name: Save original preparation diagnostics

‎benchmarks/KeyLoad.ComparisonHost/DatabaseAdapters/Cases/NativeDatabaseTests.cs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ namespace KeyLoad.ComparisonHost.BenchmarkComparisons;
77
internal sealed class NativeDatabaseTests
88
{
99
internal const string Redis = "Redis", OpenSearch = "OpenSearch", HelixDb = "HelixDB", MongoDb = "MongoDB";
10-
private const int Empty = 0, Clients = 16, First = 0, Updated = 1, Deleted = 2, Single = 1;
11-
private const int RedisRecords = 800_000, OpenSearchRecords = 80_000;
10+
private const int Empty = 0, Clients = 16, First = 0, Updated = 1, Deleted = 2, Single = 1, ReplicaNodes = 3;
11+
private const int RedisRecords = 800_000, OpenSearchRecords = 1_000_000;
1212
private const string BodyMismatch = "NativeDatabaseBulkReadbackMismatch";
1313

1414
[Test, Arguments(OpenSearch, 1), Arguments(OpenSearch, 3), Arguments(Redis, 3), Arguments(HelixDb, 1), Arguments(MongoDb, 1)]
1515
public Task NativeBulkPreservesBodiesAndIndependentClients(string target, int nodes)
1616
=> AdditionalDatabaseResources.RunAsync(target, VerifyAsync, TestContext.Current!.Execution.CancellationToken, nodes);
1717

18+
[Test]
19+
public Task MongoReplicaSetReadinessPrecedesIndependentBulkClients()
20+
=> AdditionalDatabaseResources.RunAsync(MongoDb, VerifyAsync, TestContext.Current!.Execution.CancellationToken, ReplicaNodes);
21+
1822
private static async Task VerifyAsync(IComparisonTarget target, CancellationToken token)
1923
{
2024
var documents = (IDocumentComparisonTarget)target;

‎benchmarks/KeyLoad.ComparisonHost/DatabaseAdapters/Resources/AdditionalDatabaseResources.cs‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using KeyLoad.ComparisonHost.BenchmarkOrchestration;
77
using KeyLoad.Comparisons;
88
using KeyLoad.Comparisons.Targets;
9-
using KeyLoad.LoadTests.AppHost.BenchmarkComparisons;
109
using ManagedCode.Communication.CQRS;
1110
using Microsoft.Extensions.Configuration;
1211
using Microsoft.Extensions.DependencyInjection;
@@ -66,12 +65,8 @@ or BenchmarkAdditionalTargets.MySql or BenchmarkAdditionalTargets.ClickHouse or
6665
private static async Task RunNativeAsync(DistributedApplication application, string target,
6766
Func<IComparisonTarget, CancellationToken, Task> verify, NativeDatabaseResourceObservation observations, CancellationToken token)
6867
{
69-
var settings = application.Services.GetRequiredService<LoadTestClientSettings>();
7068
await application.StartAsync(token);
71-
foreach (var resource in settings.Resources)
72-
{
73-
await application.ResourceNotifications.WaitForResourceHealthyAsync(resource, WaitBehavior.StopOnResourceUnavailable, token);
74-
}
69+
var settings = await BenchmarkSessionClientRuntime.WaitForReadinessAsync(application, token);
7570
await observations.VerifyReadyAsync(token);
7671
var values = await settings.ResolveAsync(token);
7772
using var configuration = new ConfigurationManager();

‎benchmarks/KeyLoad.ComparisonHost/DatabaseAdapters/Resources/NativeDatabaseResourceObservation.cs‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ internal NativeDatabaseResourceObservation(DistributedApplication app, string ro
3131
.OfType<ContainerResource>().Where(resource => settings.Resources.Contains(resource.Name))];
3232
collector = app.Services.GetRequiredService<ScaleServerResourceEvidenceCollector>();
3333
observation = collector.StartAsync(resources,
34-
ready => ScaleServerResourceReadiness.WaitAsync(app, resources, ready), token);
34+
ready => BenchmarkSessionClientRuntime.WaitForReadinessAsync(app, ready), token);
3535
}
3636

3737
internal async Task VerifyReadyAsync(CancellationToken token)
@@ -56,6 +56,7 @@ internal async Task VerifyReadyAsync(CancellationToken token)
5656
await Assert.That(actual.Length).IsGreaterThan(0);
5757
await Assert.That(actual.Length).IsLessThanOrEqualTo(budget.Settings.MaxMounts);
5858
await Assert.That(actual.Distinct(StringComparer.Ordinal).Count()).IsEqualTo(actual.Length);
59+
await VerifyMountIdentityAsync(descriptor);
5960
ScaleServerMount[] mounts = [];
6061
if (OperatingSystem.IsLinux())
6162
{
@@ -66,6 +67,25 @@ internal async Task VerifyReadyAsync(CancellationToken token)
6667
}
6768
}
6869

70+
private static async Task VerifyMountIdentityAsync(string descriptor)
71+
{
72+
const string Changed = "-changed";
73+
const int First = 0, Fields = 3;
74+
var rows = descriptor.Split(MountSeparator, StringSplitOptions.RemoveEmptyEntries);
75+
var identity = ScaleServerMountReader.NormalizeDescriptor(descriptor);
76+
var reordered = string.Join(MountSeparator, rows.Reverse());
77+
await Assert.That(ScaleServerMountReader.NormalizeDescriptor(reordered)).IsEqualTo(identity);
78+
for (var field = First; field < Fields; field++)
79+
{
80+
var changed = rows.ToArray();
81+
var values = changed[First].Split(MountFieldSeparator);
82+
values[field] += Changed;
83+
changed[First] = string.Join(MountFieldSeparator, values);
84+
await Assert.That(ScaleServerMountReader.NormalizeDescriptor(string.Join(MountSeparator, changed)))
85+
.IsNotEqualTo(identity);
86+
}
87+
}
88+
6989
private async Task CaptureHostMountsAsync(CancellationToken token)
7090
{
7191
const string MountInfoFile = "host-mountinfo.txt";

‎benchmarks/KeyLoad.ComparisonHost/Orchestration/Execution/BenchmarkSessionClientRuntime.cs‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,17 @@ internal static class BenchmarkSessionClientRuntime
1515

1616
internal static async Task<int> RunAsync(DistributedApplication application, string evidence, CancellationToken token)
1717
{
18-
var settings = application.Services.GetRequiredService<LoadTestClientSettings>();
1918
await application.StartAsync(token);
19+
var settings = await WaitForReadinessAsync(application, token);
20+
var environment = (await settings.ResolveAsync(token)).ToDictionary(StringComparer.Ordinal);
21+
var policy = application.Services.GetRequiredService<IOptions<TestExecutionOptions>>();
22+
return await IsolatedClientExecution.RunAsync(application, settings.Resources,
23+
lifetime => BenchmarkSessionClientProcess.RunAsync(environment, evidence, policy, lifetime), token);
24+
}
25+
26+
internal static async Task<LoadTestClientSettings> WaitForReadinessAsync(DistributedApplication application, CancellationToken token)
27+
{
28+
var settings = application.Services.GetRequiredService<LoadTestClientSettings>();
2029
foreach (var resource in settings.Resources)
2130
{
2231
await application.ResourceNotifications.WaitForResourceHealthyAsync(resource, WaitBehavior.StopOnResourceUnavailable, token);
@@ -30,9 +39,6 @@ internal static async Task<int> RunAsync(DistributedApplication application, str
3039
throw new InvalidOperationException(TheSelectedDatabaseBootstrapProcessFailedMessage);
3140
}
3241
}
33-
var environment = (await settings.ResolveAsync(token)).ToDictionary(StringComparer.Ordinal);
34-
var policy = application.Services.GetRequiredService<IOptions<TestExecutionOptions>>();
35-
return await IsolatedClientExecution.RunAsync(application, settings.Resources,
36-
lifetime => BenchmarkSessionClientProcess.RunAsync(environment, evidence, policy, lifetime), token);
42+
return settings;
3743
}
3844
}

‎benchmarks/KeyLoad.ComparisonHost/Orchestration/Fixtures/BenchmarkSessionAspireFixture.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ await IsolatedReportAssertions.VerifyModelAsync(
6969
item.Target == selected.Target && item.NodeCounts.Contains(selected.NodeCount)))
7070
{
7171
work.Observation = work.Collector.StartAsync(containers,
72-
readinessToken => ScaleServerResourceReadiness.WaitAsync(application, containers, readinessToken), token);
72+
readinessToken => BenchmarkSessionClientRuntime.WaitForReadinessAsync(application, readinessToken), token);
7373
}
7474
else
7575
{

‎benchmarks/KeyLoad.ComparisonHost/Orchestration/Lifecycle/ScaleServerResourceReadiness.cs‎

Lines changed: 0 additions & 21 deletions
This file was deleted.

‎benchmarks/KeyLoad.Comparisons/Documents/Adapters/OpenSearchTarget.cs‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ namespace KeyLoad.Comparisons.Targets;
22

33
public sealed partial class OpenSearchTarget
44
{
5-
// Sixteen clients retain concurrent coordinating, primary and replica work. Keep each request near 1 MiB
6-
// so the selected small-heap topology admits the whole cohort without indexing-pressure rejections.
5+
// Bound each bulk request independently of the total corpus and the number of active clients.
76
private const int BulkRequestDocuments = 1_000;
87
/// <inheritdoc/>
98
public int IngestionBatchRecords => BulkRequestDocuments;

‎benchmarks/KeyLoad.Comparisons/Targets/OpenSearch/Transport/OpenSearchHttp.cs‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,13 @@ private static async Task<JsonDocument> SendAsync(HttpClient client, HttpMethod
5252

5353
if (mutation is null && !response.IsSuccessStatusCode)
5454
{
55-
throw new ComparisonFailureException(HttpFailure + (int)response.StatusCode);
55+
var failure = HttpFailure + (int)response.StatusCode;
56+
if (policy is not null)
57+
{
58+
using var error = await ReadJsonAsync(response.Content, policy, cancellationToken);
59+
failure += ReadFailureKind(error.RootElement);
60+
}
61+
throw new ComparisonFailureException(failure);
5662
}
5763
var json = await ReadJsonAsync(response.Content, policy, cancellationToken);
5864
try
@@ -70,6 +76,21 @@ private static async Task<JsonDocument> SendAsync(HttpClient client, HttpMethod
7076
}
7177
}
7278

79+
private static string ReadFailureKind(JsonElement response)
80+
{
81+
const string FailureKindSeparator = ".";
82+
const char TypeWordSeparator = '_';
83+
const int MaximumTypeLength = 128;
84+
if (response.ValueKind == JsonValueKind.Object && response.TryGetProperty(OpenSearchNames.Error, out var error)
85+
&& error.ValueKind == JsonValueKind.Object && error.TryGetProperty(OpenSearchNames.ErrorType, out var kind)
86+
&& kind.ValueKind == JsonValueKind.String && kind.GetString() is { Length: > 0 and <= MaximumTypeLength } text
87+
&& text.All(character => char.IsAsciiLetterOrDigit(character) || character == TypeWordSeparator))
88+
{
89+
return FailureKindSeparator + text;
90+
}
91+
return string.Empty;
92+
}
93+
7394
private static async Task<JsonDocument> ReadJsonAsync(HttpContent content, ComparisonExecutionOptions? policy, CancellationToken cancellationToken)
7495
{
7596
if (policy is not null)

‎build/KeyLoad.LoadTests.AppHost/Observations/ScaleServerMountReader.cs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ internal static class ScaleServerMountReader
1212
private const int First = 0, Source = 0, Target = 1, MountType = 2, MountFields = 3;
1313
private const int StatFields = 4, BlockSize = 1, Blocks = 2, AvailableBlocks = 3;
1414

15+
internal static string NormalizeDescriptor(string text)
16+
=> string.Join(MountSeparator, text.Split(MountSeparator, StringSplitOptions.RemoveEmptyEntries)
17+
.Order(StringComparer.Ordinal));
18+
1519
internal static async Task<ScaleServerMount[]> ReadAsync(string text, string[] expectedTargets,
1620
ScaleServerResourceSampleBudget budget, CancellationToken token)
1721
{

0 commit comments

Comments
 (0)