Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions src/MeshWeaver.Graph/Configuration/NodeTypeCompilationHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ public static IDisposable InstallSourcesWatcher(
/// so <see cref="InstallSourcesWatcher"/> can share the same condition β€”
/// keeps the "what counts as static?" question in one place.</para>
/// </summary>
private static bool IsStaticOnlyNodeType(MeshNode node, NodeTypeDefinition def) =>
internal static bool IsStaticOnlyNodeType(MeshNode node, NodeTypeDefinition def) =>
node.HubConfiguration is not null
&& string.IsNullOrWhiteSpace(def.Configuration)
&& string.IsNullOrWhiteSpace(def.HubConfiguration)
Expand Down Expand Up @@ -1074,12 +1074,27 @@ curr.Content is NodeTypeDefinition def
// node, so no reader can subscribe to a phantom `compile-*` path.
var resolvedActivityPath = outcome.ActivityPath;

string? newReleasePath = null;
if (ok)
// 🚨 Release create is OBSERVED + BOUNDED (same guard as the activity
// create): TryCreateReleaseNode emits the path only after the create
// LANDED, or null on timeout/fault. The terminal write below runs in
// its OnNext, so LatestReleasePath is never stamped with a path that
// does not exist yet β€” a reader following the field right after the
// Ok write used to hit a hard path-resolution NotFound (the
// NodeTypeReleaseGateTest 2-core flake).
var releasePathObservable = ok
? MeshDataSourceExtensions.TryCreateReleaseNode(
hub, hubPath, outcome.Result!, outcome.PendingNode, resolvedActivityPath, logger)
: Observable.Return<string?>(null);

releasePathObservable
.Take(1)
.Subscribe(newReleasePath =>
{
newReleasePath = MeshDataSourceExtensions.TryCreateReleaseNode(
hub, hubPath, outcome.Result!, outcome.PendingNode, resolvedActivityPath, logger);
}
// Terminal writes run under System β€” same rule as every other
// deferred pipeline in RunCompile (the ambient scope from the
// compile subscription does not flow into this create-response
// callback thread).
using var terminalScope = accessService?.ImpersonateAsSystem();

// Write the FULL compile log + terminal status to the activity
// node (the official progress surface) in ONE atomic update:
Expand Down Expand Up @@ -1211,6 +1226,9 @@ curr.Content is NodeTypeDefinition def
},
ex => logger?.LogWarning(ex,
"Compile: failed to write post-compile status for {HubPath}", hubPath));
},
ex => logger?.LogWarning(ex,
"Compile: release-create observation faulted for {HubPath}", hubPath));
},
ex => logger?.LogWarning(ex, "Compile faulted for {HubPath}", hubPath));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,28 +344,45 @@ private static IMessageDelivery HandleCore(
$"Roslyn failed: {errMsg}", Microsoft.Extensions.Logging.LogLevel.Error));
}

// Release MeshNode created BEFORE the parent's Ok write so the
// parent's LatestReleasePath points at an existing node.
// Release.CompilationActivityPath links back to this activity so
// the UI can follow Release β†’ Activity to see the full build detail.
string? newReleasePath = null;
// Release MeshNode created β€” and CONFIRMED β€” BEFORE the parent's Ok
// write so the parent's LatestReleasePath points at an EXISTING node.
// TryCreateReleaseNode is observed + bounded: it emits the path only
// after the create landed (null on dispatch-failure/timeout), so a
// reader following LatestReleasePath right after the terminal write
// can never hit a phantom-path NotFound (the NodeTypeReleaseGateTest
// 2-core flake). Release.CompilationActivityPath links back to this
// activity so the UI can follow Release β†’ Activity for build detail.
var releasePathObservable = ok
? MeshDataSourceExtensions.TryCreateReleaseNode(
activityHub, parentPath, outcome.Result!, outcome.PendingNode, activityPath, logger)
: System.Reactive.Linq.Observable.Return<string?>(null);

releasePathObservable
.Take(1)
.Subscribe(newReleasePath =>
{
// Terminal writes run under System: the create-response emission can
// carry the release-REQUESTER's context (TryCreateReleaseNode stamps
// the user on the Release node), but the parent status write is
// infrastructure β€” a requester without Update on the parent partition
// must not fail the terminal write closed.
using var terminalScope = activityHub.ServiceProvider
.GetService<AccessService>()?.ImpersonateAsSystem();
if (ok)
{
newReleasePath = MeshDataSourceExtensions.TryCreateReleaseNode(
activityHub, parentPath, outcome.Result!, outcome.PendingNode, activityPath, logger);
if (newReleasePath is not null)
{
activityMessages.Add(new LogMessage(
$"Release created: {newReleasePath} β†’ assembly={outcome.Result!.AssemblyLocation}, activity={activityPath}",
Microsoft.Extensions.Logging.LogLevel.Information));
logger?.LogInformation(
"[NTCA] Release {ReleasePath} linked to activity {ActivityPath} + assembly {AssemblyLocation}",
newReleasePath, activityPath, outcome.Result.AssemblyLocation);
newReleasePath, activityPath, outcome.Result!.AssemblyLocation);
}
else
{
activityMessages.Add(new LogMessage(
"Release node NOT created (no IMeshService available) β€” assembly still usable directly",
"Release node NOT created (create did not land / no IMeshService) β€” assembly still usable directly",
Microsoft.Extensions.Logging.LogLevel.Warning));
}
}
Expand Down Expand Up @@ -436,6 +453,9 @@ private static IMessageDelivery HandleCore(
// β†’ activation falls back to default config and IWorkspace fails
// to activate (no AddData). Found via CodeEditRecompileTest.
compiledVersion: ok ? outcome.Result?.Version : null);
},
ex => logger?.LogWarning(ex,
"[NTCA] release-create observation faulted for {ParentPath}", parentPath));
},
ex => logger?.LogWarning(ex, "[NTCA] compile chain faulted for {ParentPath}", parentPath));

Expand Down
Loading
Loading