Publish runtime drain operations surface
This commit is contained in:
parent
9fbc77e4e9
commit
1f88375a7e
9 changed files with 257 additions and 11 deletions
Binary file not shown.
|
|
@ -90,6 +90,20 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s
|
|||
await node.PutCatalogSurfaceAsync(
|
||||
AetheriaCatalogSurfaceProjector.Build(node.ReadCatalogSnapshot(), now));
|
||||
|
||||
var drainStatus = new AetheriaRuntimeCommitDrainStatus
|
||||
{
|
||||
RuntimeId = "smoke-runtime",
|
||||
StatePath = statePath,
|
||||
LastPollAtUtc = now,
|
||||
LastAppliedAtUtc = now,
|
||||
PendingBeforeApply = 1,
|
||||
CommandsApplied = 1,
|
||||
AppliedPlayerSettings = 1,
|
||||
Status = "ok"
|
||||
};
|
||||
await node.PutRuntimeCommitDrainStatusAsync(drainStatus);
|
||||
await node.PutOperationsSurfaceAsync(AetheriaOperationsSurfaceProjector.Build(drainStatus));
|
||||
|
||||
await node.PutLoadoutTemplateAsync(loadoutKey, new AetheriaLoadoutTemplate
|
||||
{
|
||||
Name = "Smoke Aether Runner",
|
||||
|
|
@ -240,6 +254,8 @@ await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aether
|
|||
var nameFile = await reopened.GetNameFileByLegacyIdAsync(nameFileLegacyId);
|
||||
var quarantine = await reopened.GetLegacyCatalogQuarantineAsync();
|
||||
var catalogSurface = await reopened.GetCatalogSurfaceAsync();
|
||||
var drainStatus = await reopened.GetRuntimeCommitDrainStatusAsync();
|
||||
var operationsSurface = await reopened.GetOperationsSurfaceAsync();
|
||||
var playerSettings = await reopened.GetPlayerSettingsAsync();
|
||||
var loadout = await reopened.GetLoadoutTemplateAsync(loadoutKey);
|
||||
var runState = await reopened.GetRunStateAsync(runKey);
|
||||
|
|
@ -277,6 +293,13 @@ await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aether
|
|||
throw new InvalidOperationException("Eve catalog surface did not survive flush/reopen.");
|
||||
}
|
||||
|
||||
if (drainStatus?.RuntimeId != "smoke-runtime" ||
|
||||
drainStatus.CommandsApplied != 1 ||
|
||||
operationsSurface?.Surface.Id != AetheriaOperationsSurfaceProjector.SurfaceId)
|
||||
{
|
||||
throw new InvalidOperationException("Runtime commit drain status or operations surface did not survive flush/reopen.");
|
||||
}
|
||||
|
||||
if (playerSettings?.ActiveRunKey != runKey.ToString() ||
|
||||
playerSettings.PlayerName != "Smoke Pilot" ||
|
||||
!playerSettings.TutorialPassed ||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ public static class AetheriaDocumentRegistry
|
|||
CultNetDocumentBinding.ForDocument<AetheriaRunState>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaZoneState>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaEntitySnapshot>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeCommitDrainStatus>(registry),
|
||||
CultNetDocumentBinding.ForDocument<EveSurfaceState>(registry)
|
||||
});
|
||||
}
|
||||
|
|
@ -56,6 +57,7 @@ public static class AetheriaDocumentRegistry
|
|||
typeof(AetheriaRunState),
|
||||
typeof(AetheriaZoneState),
|
||||
typeof(AetheriaEntitySnapshot),
|
||||
typeof(AetheriaRuntimeCommitDrainStatus),
|
||||
typeof(EveSurfaceState)
|
||||
];
|
||||
}
|
||||
|
|
|
|||
80
Aetheria.State/AetheriaOperationsSurfaceProjector.cs
Normal file
80
Aetheria.State/AetheriaOperationsSurfaceProjector.cs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
using Aetheria.State.Documents;
|
||||
|
||||
namespace Aetheria.State;
|
||||
|
||||
public static class AetheriaOperationsSurfaceProjector
|
||||
{
|
||||
public const string SurfaceKey = "eve:surface:aetheria.operations";
|
||||
public const string SurfaceId = "aetheria.operations";
|
||||
|
||||
public static EveSurfaceState Build(AetheriaRuntimeCommitDrainStatus drainStatus, long version = 1)
|
||||
{
|
||||
return new EveSurfaceState
|
||||
{
|
||||
ProviderId = "aetheria",
|
||||
ProviderKind = "game.runtime",
|
||||
Title = "Aetheria Operations",
|
||||
Version = version,
|
||||
UpdatedAtUtc = drainStatus.LastPollAtUtc,
|
||||
Surface = new EveSurface
|
||||
{
|
||||
Id = SurfaceId,
|
||||
Root = Node(
|
||||
"aetheria.operations.root",
|
||||
"surface",
|
||||
[],
|
||||
Node(
|
||||
"aetheria.operations.commitDrain",
|
||||
"card",
|
||||
[("title", "Runtime Commit Drain")],
|
||||
Metric("commitDrain.status", "Status", drainStatus.Status),
|
||||
Metric("commitDrain.pending", "Pending Before Apply", drainStatus.PendingBeforeApply.ToString()),
|
||||
Metric("commitDrain.applied", "Commands Applied", drainStatus.CommandsApplied.ToString()),
|
||||
Metric("commitDrain.settings", "Settings", drainStatus.AppliedPlayerSettings.ToString()),
|
||||
Metric("commitDrain.loadouts", "Loadouts", drainStatus.AppliedLoadoutTemplates.ToString()),
|
||||
Metric("commitDrain.runs", "Run Checkpoints", drainStatus.AppliedRunCheckpoints.ToString()),
|
||||
Metric("commitDrain.failures", "Consecutive Failures", drainStatus.ConsecutiveFailures.ToString()),
|
||||
Row(
|
||||
"commitDrain.last",
|
||||
("runtime", drainStatus.RuntimeId),
|
||||
("lastPoll", drainStatus.LastPollAtUtc),
|
||||
("lastApplied", drainStatus.LastAppliedAtUtc),
|
||||
("error", drainStatus.LastError))))
|
||||
},
|
||||
Commands =
|
||||
[
|
||||
new EveCommandTemplate
|
||||
{
|
||||
Command = "aetheria.operations.refresh",
|
||||
Label = "Refresh",
|
||||
Transport = "cultmesh"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
private static EveSurfaceComponent Metric(string id, string label, string value)
|
||||
{
|
||||
return Node(id, "metric", [("label", label), ("value", value)]);
|
||||
}
|
||||
|
||||
private static EveSurfaceComponent Row(string id, params (string Key, string Value)[] props)
|
||||
{
|
||||
return Node(id, "row", props);
|
||||
}
|
||||
|
||||
private static EveSurfaceComponent Node(
|
||||
string id,
|
||||
string kind,
|
||||
(string Key, string Value)[] props,
|
||||
params EveSurfaceComponent[] children)
|
||||
{
|
||||
return new EveSurfaceComponent
|
||||
{
|
||||
Id = id,
|
||||
Kind = kind,
|
||||
Props = props.ToDictionary(prop => prop.Key, prop => prop.Value),
|
||||
Children = children
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -173,6 +173,16 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
|
|||
return Database.GetAsync<EveSurfaceState>(new CultRecordKey(AetheriaCatalogSurfaceProjector.SurfaceKey));
|
||||
}
|
||||
|
||||
public Task<CultRecordHandle<EveSurfaceState>> PutOperationsSurfaceAsync(EveSurfaceState surface)
|
||||
{
|
||||
return Database.PutAsync(new CultRecordKey(AetheriaOperationsSurfaceProjector.SurfaceKey), surface);
|
||||
}
|
||||
|
||||
public Task<EveSurfaceState?> GetOperationsSurfaceAsync()
|
||||
{
|
||||
return Database.GetAsync<EveSurfaceState>(new CultRecordKey(AetheriaOperationsSurfaceProjector.SurfaceKey));
|
||||
}
|
||||
|
||||
public Task<CultRecordHandle<AetheriaPlayerSettings>> PutPlayerSettingsAsync(AetheriaPlayerSettings settings)
|
||||
{
|
||||
return Database.PutAsync(new CultRecordKey("global:aetheria.player_settings.v1"), settings);
|
||||
|
|
@ -227,6 +237,18 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
|
|||
return Database.GetAsync<AetheriaEntitySnapshot>(key);
|
||||
}
|
||||
|
||||
public Task<CultRecordHandle<AetheriaRuntimeCommitDrainStatus>> PutRuntimeCommitDrainStatusAsync(
|
||||
AetheriaRuntimeCommitDrainStatus status)
|
||||
{
|
||||
return Database.PutAsync(new CultRecordKey("global:aetheria.runtime_commit_drain_status.v1"), status);
|
||||
}
|
||||
|
||||
public Task<AetheriaRuntimeCommitDrainStatus?> GetRuntimeCommitDrainStatusAsync()
|
||||
{
|
||||
return Database.GetAsync<AetheriaRuntimeCommitDrainStatus>(
|
||||
new CultRecordKey("global:aetheria.runtime_commit_drain_status.v1"));
|
||||
}
|
||||
|
||||
public Task FlushAsync(bool soft = false)
|
||||
{
|
||||
return _node.FlushAsync(soft);
|
||||
|
|
|
|||
51
Aetheria.State/Documents/AetheriaRuntimeCommitDrainStatus.cs
Normal file
51
Aetheria.State/Documents/AetheriaRuntimeCommitDrainStatus.cs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
using GameCult.Caching;
|
||||
using MessagePack;
|
||||
|
||||
namespace Aetheria.State.Documents;
|
||||
|
||||
[CultDocument("aetheria.runtime_commit_drain_status", "aetheria.runtime_commit_drain_status.v1")]
|
||||
[CultGlobal]
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaRuntimeCommitDrainStatus
|
||||
{
|
||||
[Key(0)]
|
||||
[CultName]
|
||||
public string Name { get; set; } = "Aetheria runtime commit drain";
|
||||
|
||||
[Key(1)]
|
||||
[CultIndex("runtimeId")]
|
||||
public string RuntimeId { get; set; } = "";
|
||||
|
||||
[Key(2)]
|
||||
public string StatePath { get; set; } = "";
|
||||
|
||||
[Key(3)]
|
||||
public string LastPollAtUtc { get; set; } = "";
|
||||
|
||||
[Key(4)]
|
||||
public string LastAppliedAtUtc { get; set; } = "";
|
||||
|
||||
[Key(5)]
|
||||
public int PendingBeforeApply { get; set; }
|
||||
|
||||
[Key(6)]
|
||||
public int CommandsApplied { get; set; }
|
||||
|
||||
[Key(7)]
|
||||
public int AppliedPlayerSettings { get; set; }
|
||||
|
||||
[Key(8)]
|
||||
public int AppliedLoadoutTemplates { get; set; }
|
||||
|
||||
[Key(9)]
|
||||
public int AppliedRunCheckpoints { get; set; }
|
||||
|
||||
[Key(10)]
|
||||
public int ConsecutiveFailures { get; set; }
|
||||
|
||||
[Key(11)]
|
||||
public string LastError { get; set; } = "";
|
||||
|
||||
[Key(12)]
|
||||
public string Status { get; set; } = "ok";
|
||||
}
|
||||
|
|
@ -137,6 +137,11 @@ one-shot drain without leaving the host running:
|
|||
dotnet run --project .\Economy.Server\Economy.Server.csproj -- --apply-pending-once
|
||||
```
|
||||
|
||||
The server publishes `AetheriaRuntimeCommitDrainStatus` and the
|
||||
`aetheria.operations` Eve surface after each drain attempt, so pending depth,
|
||||
applied counts, failures, and timestamps are typed state rather than console-only
|
||||
status.
|
||||
|
||||
`Aetheria.State.ApplyPending` is the smaller local operator applicator for
|
||||
queued Unity runtime commits when the server host is not being used:
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ namespace Aetheria.Server;
|
|||
|
||||
internal static class Program
|
||||
{
|
||||
private const string RuntimeId = "aetheria-economy-server";
|
||||
|
||||
private static async Task Main(string[] args)
|
||||
{
|
||||
var statePath = ResolveStatePath(args);
|
||||
|
|
@ -12,7 +14,7 @@ internal static class Program
|
|||
|
||||
await using var node = await AetheriaStateNode.OpenAsync(
|
||||
statePath,
|
||||
runtimeId: "aetheria-economy-server",
|
||||
runtimeId: RuntimeId,
|
||||
startServer: true).ConfigureAwait(false);
|
||||
|
||||
await EnsureWorldDocumentAsync(node).ConfigureAwait(false);
|
||||
|
|
@ -68,19 +70,75 @@ internal static class Program
|
|||
|
||||
private static async Task ApplyPendingRuntimeCommitsAsync(AetheriaStateNode node)
|
||||
{
|
||||
var report = await AetheriaRuntimeCommitLogApplier.ApplyPendingAsync(node).ConfigureAwait(false);
|
||||
if (report.AppliedPaths.Length == 0)
|
||||
{
|
||||
Console.WriteLine("No pending Aetheria runtime commits.");
|
||||
return;
|
||||
}
|
||||
var pendingBefore = CountPendingRuntimeCommits(node.StatePath);
|
||||
var now = DateTimeOffset.UtcNow.ToString("O");
|
||||
|
||||
Console.WriteLine(
|
||||
"Applied pending Aetheria runtime commits: " +
|
||||
$"settings={report.AppliedPlayerSettings}, " +
|
||||
$"loadouts={report.AppliedLoadoutTemplates}, " +
|
||||
$"runs={report.AppliedRunCheckpoints}, " +
|
||||
$"files={report.AppliedPaths.Length}");
|
||||
try
|
||||
{
|
||||
var report = await AetheriaRuntimeCommitLogApplier.ApplyPendingAsync(node).ConfigureAwait(false);
|
||||
var status = new AetheriaRuntimeCommitDrainStatus
|
||||
{
|
||||
RuntimeId = RuntimeId,
|
||||
StatePath = node.StatePath,
|
||||
LastPollAtUtc = now,
|
||||
LastAppliedAtUtc = report.AppliedPaths.Length > 0 ? now : "",
|
||||
PendingBeforeApply = pendingBefore,
|
||||
CommandsApplied = report.AppliedPaths.Length,
|
||||
AppliedPlayerSettings = report.AppliedPlayerSettings,
|
||||
AppliedLoadoutTemplates = report.AppliedLoadoutTemplates,
|
||||
AppliedRunCheckpoints = report.AppliedRunCheckpoints,
|
||||
ConsecutiveFailures = 0,
|
||||
Status = "ok"
|
||||
};
|
||||
await PublishDrainStatusAsync(node, status).ConfigureAwait(false);
|
||||
|
||||
if (report.AppliedPaths.Length == 0)
|
||||
{
|
||||
Console.WriteLine("No pending Aetheria runtime commits.");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
"Applied pending Aetheria runtime commits: " +
|
||||
$"settings={report.AppliedPlayerSettings}, " +
|
||||
$"loadouts={report.AppliedLoadoutTemplates}, " +
|
||||
$"runs={report.AppliedRunCheckpoints}, " +
|
||||
$"files={report.AppliedPaths.Length}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var existing = await node.GetRuntimeCommitDrainStatusAsync().ConfigureAwait(false);
|
||||
var status = new AetheriaRuntimeCommitDrainStatus
|
||||
{
|
||||
RuntimeId = RuntimeId,
|
||||
StatePath = node.StatePath,
|
||||
LastPollAtUtc = now,
|
||||
LastAppliedAtUtc = existing?.LastAppliedAtUtc ?? "",
|
||||
PendingBeforeApply = pendingBefore,
|
||||
ConsecutiveFailures = (existing?.ConsecutiveFailures ?? 0) + 1,
|
||||
LastError = ex.ToString(),
|
||||
Status = "error"
|
||||
};
|
||||
await PublishDrainStatusAsync(node, status).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task PublishDrainStatusAsync(
|
||||
AetheriaStateNode node,
|
||||
AetheriaRuntimeCommitDrainStatus status)
|
||||
{
|
||||
await node.PutRuntimeCommitDrainStatusAsync(status).ConfigureAwait(false);
|
||||
await node.PutOperationsSurfaceAsync(AetheriaOperationsSurfaceProjector.Build(status)).ConfigureAwait(false);
|
||||
await node.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static int CountPendingRuntimeCommits(string statePath)
|
||||
{
|
||||
var pendingDirectory = statePath + ".pending";
|
||||
return Directory.Exists(pendingDirectory)
|
||||
? Directory.EnumerateFiles(pendingDirectory, "*.cc").Count()
|
||||
: 0;
|
||||
}
|
||||
|
||||
private static async Task RunUntilShutdownAsync(AetheriaStateNode node, TimeSpan pendingInterval)
|
||||
|
|
|
|||
|
|
@ -175,6 +175,9 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
|
|||
- `Economy.Server` hosts the CultMesh state node and now owns the long-running
|
||||
pending runtime commit drain loop. `--apply-pending-once` runs the same drain
|
||||
path once for smoke/operator use without keeping the process alive.
|
||||
- `AetheriaRuntimeCommitDrainStatus` and the `aetheria.operations` Eve surface
|
||||
publish pending-drain health, pending depth, applied counts, failures, and
|
||||
timestamps as typed state. Console logs are notification-only.
|
||||
- `AetheriaCatalogSurfaceProjector` now emits the first provider-owned Eve
|
||||
surface from typed catalog state. The importer materializes a
|
||||
`gamecult.eve.surface.v1` catalog operator document at
|
||||
|
|
@ -537,6 +540,8 @@ First Aetheria surfaces to publish:
|
|||
queued runtime commits until the drain loop is hosted as a daemon.
|
||||
- Done: move the pending runtime commit drain into `Economy.Server` startup
|
||||
and daemon polling, with `--apply-pending-once` for bounded operation.
|
||||
- Done: publish pending-drain health as typed CultCache state and an Eve
|
||||
operations surface.
|
||||
- Done: stop legacy catalog pull/read paths from writing entries back to
|
||||
their source backing store.
|
||||
- Done: delete legacy catalog backing-store write/realtime APIs.
|
||||
|
|
|
|||
Loading…
Reference in a new issue