Skip to content

Commit dbe3509

Browse files
committed
Fix native Aspire launch and maintenance work ownership
Use the public native AddProject launch, retain exact process/source evidence, and make maintenance work transfer and joined shutdown explicit. Validated primary AppHost plus runtime dependencies with Release build -warnaserror: 0 warnings, 0 errors. Full solution and live storage-mode qualification remain open.
1 parent fb249c9 commit dbe3509

11 files changed

Lines changed: 52 additions & 35 deletions

File tree

‎build/KeyLoad.AppHost/ClusterReplication/Commands/ProcessKillCommand.cs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ internal static void Apply(IDistributedApplicationBuilder builder, IResourceBuil
3939

4040
private static InteractionInput Input(string name) => new()
4141
{
42-
Name = name, InputType = InputType.Text, Required = true,
42+
Name = name,
43+
InputType = InputType.Text,
44+
Required = true,
4345
MaxLength = ProcessFaultCommandProtocol.MaximumIdentityCharacters
4446
};
4547

‎build/KeyLoad.AppHost/ClusterReplication/Observations/CompiledSourceEvidence.cs‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ internal static class CompiledSourceEvidence
1616
private const string Central = Prefix + "Central";
1717
private const string Producer = Prefix + "Producer";
1818
private const string SourceCount = Prefix + "SourceCount";
19+
private const string AttributeSeparator = "=";
20+
private const char InputSeparator = '\n';
21+
private const string InvalidAttribute = "The compiled source attribute is invalid.";
22+
private const string IncompleteIdentity = "The observed managed assembly has no complete compilation source identity.";
23+
private const int UnobservedSourceCount = 0;
1924
private const int CustomAttributeProlog = 1;
2025
private const int MinimumSources = 1;
2126
private const int MaximumSources = 5_000;
@@ -26,7 +31,7 @@ internal static (int Count, string Sha256) Read(string assembly)
2631
using var portable = new PEReader(stream, PEStreamOptions.PrefetchMetadata);
2732
var reader = portable.GetMetadataReader();
2833
var inputs = new List<string>();
29-
var count = 0;
34+
var count = UnobservedSourceCount;
3035
foreach (var handle in reader.GetAssemblyDefinition().GetCustomAttributes())
3136
{
3237
var attribute = reader.GetCustomAttribute(handle);
@@ -37,24 +42,25 @@ internal static (int Count, string Sha256) Read(string assembly)
3742
var value = reader.GetBlobReader(attribute.Value);
3843
if (value.ReadUInt16() != CustomAttributeProlog)
3944
{
40-
throw new InvalidDataException("The compiled source attribute is invalid.");
45+
throw new InvalidDataException(InvalidAttribute);
4146
}
4247
var key = value.ReadSerializedString();
4348
var text = value.ReadSerializedString();
4449
if (key is Source or Central or Producer && text is not null)
4550
{
46-
inputs.Add(key + "=" + text);
51+
inputs.Add(key + AttributeSeparator + text);
4752
}
4853
if (key == SourceCount)
4954
{
5055
count = int.Parse(text!, NumberStyles.None, CultureInfo.InvariantCulture);
5156
}
5257
}
53-
if (count is < MinimumSources or > MaximumSources || inputs.Count(value => value.StartsWith(Source + "=", StringComparison.Ordinal)) != count)
58+
if (count is < MinimumSources or > MaximumSources
59+
|| inputs.Count(value => value.StartsWith(Source + AttributeSeparator, StringComparison.Ordinal)) != count)
5460
{
55-
throw new InvalidDataException("The observed managed assembly has no complete compilation source identity.");
61+
throw new InvalidDataException(IncompleteIdentity);
5662
}
57-
var canonical = string.Join('\n', inputs.Order(StringComparer.Ordinal));
63+
var canonical = string.Join(InputSeparator, inputs.Order(StringComparer.Ordinal));
5864
return (count, Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(canonical))));
5965
}
6066

‎build/KeyLoad.AppHost/ClusterReplication/Observations/ProjectProcessEvidence.cs‎

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ internal static class ProjectProcessEvidence
1212
private const string WorkDirectoryProperty = "executable.workdir";
1313
private const string AssemblySuffix = ".dll";
1414
private const string SymbolsSuffix = ".pdb";
15+
private const string ExecutableUnavailable = "The native executable is unavailable.";
16+
private const string EntryAssemblyUnavailable = "The managed entry assembly is unavailable.";
17+
private const string ProcessChanged = "The native process changed during evidence capture.";
18+
private const string EvidenceMismatch = "The native process or its compiled files differ from the observed identity.";
19+
private const string AmbiguousEntryAssembly = "The native project launch does not identify exactly one managed entry assembly.";
20+
private const int SingleEntryAssembly = 1;
21+
private const int NoExplicitEntryAssembly = 0;
1522

1623
internal static NativeProcessEvidence Read(ResourceNotificationService notifications, string resourceName)
1724
{
@@ -21,14 +28,14 @@ internal static NativeProcessEvidence Read(ResourceNotificationService notificat
2128
throw new InvalidOperationException(ProcessFaultCommandProtocol.Refused);
2229
}
2330
using var process = Process.GetProcessById(identity.ProcessId);
24-
var executable = process.MainModule?.FileName ?? throw new InvalidOperationException("The native executable is unavailable.");
31+
var executable = process.MainModule?.FileName ?? throw new InvalidOperationException(ExecutableUnavailable);
2532
var directory = (string)current.Snapshot.Properties.Single(property => property.Name == WorkDirectoryProperty).Value!;
2633
var arguments = (IEnumerable<string>)current.Snapshot.Properties.Single(property => property.Name == ArgumentsProperty).Value!;
2734
var managed = ResolveAssembly(project.GetProjectMetadata().ProjectPath, executable, directory, arguments);
2835
var binary = CaptureBinary(executable, managed);
2936
if (ProjectProcessIdentity.Read(notifications, resourceName) != identity)
3037
{
31-
throw new InvalidOperationException("The native process changed during evidence capture.");
38+
throw new InvalidOperationException(ProcessChanged);
3239
}
3340
return new(resourceName, identity.ProcessId, new DateTime(identity.ProcessStartTimeUtcTicks, DateTimeKind.Utc),
3441
new DateTime(identity.AspireStartTimeUtcTicks, DateTimeKind.Utc), binary);
@@ -37,8 +44,8 @@ internal static NativeProcessEvidence Read(ResourceNotificationService notificat
3744
internal static NativeProcessEvidence ReadCurrent(string name)
3845
{
3946
using var process = Process.GetCurrentProcess();
40-
var executable = process.MainModule?.FileName ?? throw new InvalidOperationException("The native executable is unavailable.");
41-
var assembly = Assembly.GetEntryAssembly()?.Location ?? throw new InvalidOperationException("The managed entry assembly is unavailable.");
47+
var executable = process.MainModule?.FileName ?? throw new InvalidOperationException(ExecutableUnavailable);
48+
var assembly = Assembly.GetEntryAssembly()?.Location ?? throw new InvalidOperationException(EntryAssemblyUnavailable);
4249
return new(name, process.Id, process.StartTime.ToUniversalTime(), null, CaptureBinary(executable, assembly));
4350
}
4451

@@ -51,7 +58,7 @@ internal static void Verify(NativeProcessEvidence expected)
5158
|| executable != expected.Binary.ExecutablePath
5259
|| CaptureBinary(executable, expected.Binary.AssemblyPath) != expected.Binary)
5360
{
54-
throw new InvalidDataException("The native process or its compiled files differ from the observed identity.");
61+
throw new InvalidDataException(EvidenceMismatch);
5562
}
5663
}
5764

@@ -61,15 +68,15 @@ private static string ResolveAssembly(string project, string executable, string
6168
var expected = name + AssemblySuffix;
6269
var explicitPaths = arguments.Where(argument => Path.GetFileName(argument) == expected)
6370
.Select(argument => Path.GetFullPath(argument, directory)).Distinct(StringComparer.Ordinal).ToArray();
64-
if (explicitPaths.Length == 1)
71+
if (explicitPaths.Length == SingleEntryAssembly)
6572
{
6673
return explicitPaths.Single();
6774
}
68-
if (Path.GetFileNameWithoutExtension(executable) == name && explicitPaths.Length == 0)
75+
if (Path.GetFileNameWithoutExtension(executable) == name && explicitPaths.Length == NoExplicitEntryAssembly)
6976
{
7077
return Path.ChangeExtension(executable, AssemblySuffix);
7178
}
72-
throw new InvalidOperationException("The native project launch does not identify exactly one managed entry assembly.");
79+
throw new InvalidOperationException(AmbiguousEntryAssembly);
7380
}
7481

7582
private static NativeBinaryEvidence CaptureBinary(string executable, string assembly)

‎build/KeyLoad.AppHost/ClusterReplication/Resources/ClusterResources.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ internal static class ClusterResources
3131
private const string ServerUrlsEnvironment = "ASPNETCORE_URLS";
3232
private const string LoopbackAddress = "127.0.0.1";
3333
private const int FirstNodeIndex = 0;
34+
private const int LastNodeOffset = 1;
3435

3536
/// <summary>Adds real native project silos; each process retains its own canonical storage directory.</summary>
3637
internal static IResourceBuilder<ProjectResource>[] Add(IDistributedApplicationBuilder builder,
@@ -42,7 +43,7 @@ internal static IResourceBuilder<ProjectResource>[] Add(IDistributedApplicationB
4243
var nodeNames = ClusterTopologyNodeNames.Read(benchmarkNodeCount, functionalNodeCount, ephemeral,
4344
options.Cluster.Value.SiloCount);
4445
var replicas = benchmarkNodeCount ?? options.Cluster.Value.ReplicaCount;
45-
if (!ephemeral && (long)options.Cluster.Value.FirstPublicPort + nodeNames.Length - 1 > IPEndPoint.MaxPort)
46+
if (!ephemeral && (long)options.Cluster.Value.FirstPublicPort + nodeNames.Length - LastNodeOffset > IPEndPoint.MaxPort)
4647
{
4748
throw new OptionsValidationException(Options.DefaultName, typeof(ClusterDeploymentOptions),
4849
[ClusterDeploymentOptions.ValidationMessage]);
@@ -84,7 +85,6 @@ private static IResourceBuilder<ProjectResource> AddNode(IDistributedApplication
8485
ClusterProfileStore.PrepareDirectory(directory);
8586
var port = ephemeral ? (int?)null : AppHostOptionsRegistration.Get(builder).Cluster.Value.FirstPublicPort + index;
8687
var resource = builder.AddProject<Projects.KeyLoad_Server>(resourceName, launchProfileName: null)
87-
.WithAnnotation(new ForceProcessExecutionAnnotation())
8888
.WithHttpEndpoint(port: port, name: HttpEndpoint, isProxied: false)
8989
.WithEndpoint(name: SiloEndpoint, scheme: TcpScheme, isExternal: false, isProxied: false);
9090
resource.WithEnvironment(ClusterEnvironment, ClusterPrefix + profile.Incarnation.ToString(ClusterGuidFormat))

‎build/KeyLoad.AppHost/Hosting/AppHostControlOptionsRegistration.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ internal static class AppHostControlOptionsRegistration
1717
private const string EphemeralSetting = "KeyLoad:Ephemeral";
1818
private const string BenchmarksEnabledSetting = "Benchmarks:Enabled";
1919
private const string BenchmarkTargetSetting = "Benchmarks:Target";
20+
private const string KeyLoadTarget = "KeyLoad";
2021
private const string BenchmarkSessionSetting = "Benchmarks:Session";
2122
private const string BenchmarkDataRootSetting = "Benchmarks:DataRoot";
2223
private const string DataRootSetting = "KeyLoad:DataRoot";
@@ -71,7 +72,7 @@ internal static IOptions<AppHostControlOptions> Bind(IConfiguration configuratio
7172
private static bool ReadProcessFaults(IConfiguration configuration, AppHostControlOptions options)
7273
{
7374
var enabled = configuration.GetValue<bool>(ProcessFaultCommandProtocol.EnabledSetting);
74-
var selectedLoad = options.BenchmarksEnabled && configuration[BenchmarkTargetSetting] == "KeyLoad"
75+
var selectedLoad = options.BenchmarksEnabled && configuration[BenchmarkTargetSetting] == KeyLoadTarget
7576
&& configuration.GetValue<bool>(BenchmarkSessionSetting)
7677
&& configuration[BenchmarkDataRootSetting] is { } root && Path.IsPathFullyQualified(root);
7778
if (enabled && (!options.Ephemeral || !selectedLoad && (options.BenchmarksEnabled || options.TargetSelected

‎src/KeyLoad.Server/Search/Execution/FullText/Incremental/NativeTextIncrementalBegin.cs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ namespace KeyLoad.Server.Features.Search;
88

99
internal static class NativeTextIncrementalBegin
1010
{
11+
private const int OriginalFailureOnlyCount = 1;
1112
internal static TextMaintenanceCapabilityResult Run(DatabaseEngine database, string root, PrincipalRecord principal,
1213
TextMaintenanceCapabilityRequest request, ServerRuntimeOptions configured, TimeProvider clock,
13-
CancellationToken shutdown, NativeTextIncrementalSessions sessions, Action<NativeTextFaultStage>? faultObserver,
14+
CancellationTokenSource shutdown, NativeTextIncrementalSessions sessions, Action<NativeTextFaultStage>? faultObserver,
1415
NativeTextResourceOwnership? resources, Action<string, NativeTextResourceReservation> retainGeneration,
1516
DateTimeOffset? expiry, IResourceLifetimeWorkBinding? binding, CancellationToken token)
1617
{
1718
var work = new NativeMaintenanceSessionWork(configured.Core.DatabaseLimits, clock,
18-
expiry ?? clock.GetUtcNow().AddSeconds(configured.Core.DatabaseLimits.Value.QueryDeadlineSeconds), shutdown);
19+
expiry ?? clock.GetUtcNow().AddSeconds(configured.Core.DatabaseLimits.Value.QueryDeadlineSeconds), shutdown.Token);
1920
var captureStarted = false;
2021
var captured = false;
2122
try
@@ -95,7 +96,7 @@ void CaptureOwnership(NativeTextIncrementalSession actual)
9596
{
9697
ServerFailureObserver.Observe(reservation.CompleteAfterJoinedCleanup, failures);
9798
}
98-
if (captured is null && failures.Count == 1)
99+
if (captured is null && failures.Count == OriginalFailureOnlyCount)
99100
{
100101
ServerFailureObserver.Observe(work.DisposeAfterStage, failures);
101102
}

‎src/KeyLoad.Server/Search/Execution/FullText/Incremental/NativeTextIncrementalMaintenanceService.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ private TextMaintenanceCapabilityResult ExecuteExclusive(PrincipalRecord princip
101101
if (request.Kind == TextMaintenanceCapabilityKind.Begin)
102102
{
103103
return NativeTextIncrementalBegin.Run(database, root, principal, request, configured, clock,
104-
shutdown.Token, sessions, faultObserver, resources, generations.RetainGeneration, originalExpiry, binding, token);
104+
shutdown, sessions, faultObserver, resources, generations.RetainGeneration, originalExpiry, binding, token);
105105
}
106106
var retained = sessions.Require(request.SessionId);
107107
retained.Work.RequireExpiry(originalExpiry);
@@ -161,7 +161,7 @@ private async Task DisposeCoreAsync()
161161
var failures = new List<Exception>();
162162
await ServerFailureObserver.ObserveAsync(shutdown.CancelAsync, failures).ConfigureAwait(false);
163163
await ServerFailureObserver.ObserveAsync(() => NativeTextIncrementalOwnerShutdown.RunAsync(
164-
sessions, selectedAdmission, worker, selectedGate, selectedSlots), failures).ConfigureAwait(false);
164+
sessions, selectedAdmission, worker.DisposeAsync().AsTask(), selectedGate, selectedSlots), failures).ConfigureAwait(false);
165165
ServerFailureObserver.ThrowIfAny(failures);
166166
shutdown.Dispose();
167167
}

‎src/KeyLoad.Server/Search/Execution/FullText/Online/NativeTextOnlineMaintenanceService.cs‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,15 @@ private async Task<OnlineTextCapabilityResult> ExecuteOwnedAsync(PrincipalRecord
6868
throw NativeTextErrors.Ownership();
6969
}
7070
var session = request.Kind == OnlineTextCapabilityKind.ResolveOriginal ? null : sessions.Require(request.SessionId);
71-
var work = session?.Work ?? new NativeMaintenanceSessionWork(options.Core.DatabaseLimits, clock, expiry, shutdown.Token);
72-
var retained = session is not null;
71+
NativeMaintenanceSessionWork? created = null;
7372
try
7473
{
75-
if (!retained)
74+
var work = session?.Work;
75+
if (work is null)
7676
{
77-
sessions.BeginWork(request.SessionId, work);
77+
created = new NativeMaintenanceSessionWork(options.Core.DatabaseLimits, clock, expiry, shutdown.Token);
78+
work = created;
79+
sessions.BeginWork(request.SessionId, created);
7880
}
7981
using var stage = work.EnterStage(binding, token);
8082
work.RequireExpiry(expiry);
@@ -87,17 +89,17 @@ private async Task<OnlineTextCapabilityResult> ExecuteOwnedAsync(PrincipalRecord
8789
}
8890
session = await sessions.AddOwnedAsync(new(request.SessionId, request.Request, principal.Id, expiry, work))
8991
.ConfigureAwait(false);
90-
retained = true;
92+
created = null;
9193
}
9294
return await ExecuteStageAsync(session ?? throw NativeTextErrors.Ownership(), principal, request, expiry)
9395
.ConfigureAwait(false);
9496
}
9597
finally
9698
{
97-
if (!retained)
99+
if (created is not null)
98100
{
99-
work.DisposeAfterStage();
100-
sessions.RemoveWorkWithoutSession(request.SessionId, work);
101+
created.Dispose();
102+
sessions.RemoveWorkWithoutSession(request.SessionId, created);
101103
}
102104
}
103105
}

‎src/KeyLoad.Server/Search/Lifecycle/Ann/NativeAnnMaintenanceAdmissions.cs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using KeyLoad.Clustering;
2-
using KeyLoad.Core;
32

43
namespace KeyLoad.Server.Features.Search;
54

‎src/KeyLoad.Server/Search/Lifecycle/FullText/Incremental/NativeTextIncrementalOwnerShutdown.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ namespace KeyLoad.Server.Features.Search;
33
internal static class NativeTextIncrementalOwnerShutdown
44
{
55
internal static async Task RunAsync(NativeTextIncrementalSessions sessions, NativeTextSelectedReadAdmission selectedAdmission,
6-
NativeTextIncrementalWorker worker, Lock selectedGate, Dictionary<string, NativeTextSelectedIndexSlot> selectedSlots)
6+
Task originalWorker, Lock selectedGate, Dictionary<string, NativeTextSelectedIndexSlot> selectedSlots)
77
{
88
sessions.CloseAdmission();
99
var originalReaders = selectedAdmission.CloseAndJoinReaders();
1010
var failures = new List<Exception>();
1111
try
1212
{
13-
await worker.DisposeAsync().ConfigureAwait(false);
13+
await originalWorker.ConfigureAwait(false);
1414
}
1515
catch (Exception error) when (ManagedCode.Communication.CQRS.CqrsRuntimeFailures.FindFatal(error) is null)
1616
{

0 commit comments

Comments
 (0)