From 1f88375a7e1c4bbdc8e5faa1c8b00c930a8dcb52 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 13 Jun 2026 15:54:02 +0100 Subject: [PATCH] Publish runtime drain operations surface --- .voidbot/state/aetheria.cc | Bin 36575 -> 36913 bytes Aetheria.State.Smoke/Program.cs | 23 +++++ Aetheria.State/AetheriaDocumentRegistry.cs | 2 + .../AetheriaOperationsSurfaceProjector.cs | 80 +++++++++++++++++ Aetheria.State/AetheriaStateNode.cs | 22 +++++ .../AetheriaRuntimeCommitDrainStatus.cs | 51 +++++++++++ Aetheria.State/README.md | 5 ++ Economy.Server/Program.cs | 84 +++++++++++++++--- docs/aetheria-perfect-machine-map.md | 5 ++ 9 files changed, 259 insertions(+), 13 deletions(-) create mode 100644 Aetheria.State/AetheriaOperationsSurfaceProjector.cs create mode 100644 Aetheria.State/Documents/AetheriaRuntimeCommitDrainStatus.cs diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index 0473a60bc25847453b755fe92919f6a2aff6ccd9..d81b74d05a2e20831c55c08b16ae4000fea4d6e9 100644 GIT binary patch delta 1256 zcmZuxOKTKC5H_1AiI79o7!;xf;se~>yklZS5X~VXNzChzTklNmPMewTp}S|31!0jW z9z3YbSr8u)JgMtlJb3pS5%e#3@oe?%%$n#PhMleHs`~2tzUm*(a_^t#-tFWLPmEn2 zohpt`4X;1#=#RhT9| zmOd7w@M}p{q>HZ|pYh;ZNAEQ>bq^J1OOY@GH;8rL#5~V?43FD$FokUS6VNF`4NsTfQ>VZY;GZQ~K+n;|pk6Q*- z=Z0jcVN(f;?DEjKgMetE3S){SHaC=-SI zx6(uA8?UjjoEx0oon2&FqdNmSkQ-J%+G4#QnANil1GEB~Yx11a;NjH4Bk5%_IlBJJ zunO@z-)KY~T}MCs%`K$tH9ppQdwN<4@z>51*&Q=|DY;{E+n>_N#PDc(7e(F8jEUj+ aSLZ-%?%!)2nX*S3#~ys{$wDI|>wf^UV#xsj delta 1027 zcmZuwJ!=$E6pfo062V4@Xc4@XX*QE=;*!-)LW+QqpeuoNZ)fiAJo4tf$-8fMvkKWl zBZ!r3VI@e2A6O-9Wo2b=CxL)Jz|ue9otd3Qqs@HWx%bRD=bm@+w)Epo>G*x=(yf&{ zm1S?GR@r`aVW$7J^s=bjUdk$F{PCz#%_>t9v!(r`!Q4c6qvzIqqfONH9-?id1#LPI zSRpmRBQ0GQJm2?X)PdA3u!(juLhwx#Ne8Igs7#Cmolq)-(2z^pT!?TN`D&$r)kL`s zq!7pnhm0Sf_s&;BMyJ0vvG-qK5ywjU!bzh+63>7#y2b2_R$UMf37rT9tM}HSVP()l z56;QR5SUoOJZDS{kn*rDiOUchrK$u?!>Y0KPiwA;C2Q7H2B8C>B#w=B)UUK>df8yZ zBs#zV*gDx9QeI$7tZ)p=!{mK%sEL)FzNoV=nYS#*#HljDPd+PYruLG-GM6YP2r<=BK)*% zJsSCZdUzlAT-Jz|74S}t-2n%s26Ur?s z77$?ULJ$P58?#?UDi)aKFc};R_t#b9^xQ(registry), CultNetDocumentBinding.ForDocument(registry), CultNetDocumentBinding.ForDocument(registry), + CultNetDocumentBinding.ForDocument(registry), CultNetDocumentBinding.ForDocument(registry) }); } @@ -56,6 +57,7 @@ public static class AetheriaDocumentRegistry typeof(AetheriaRunState), typeof(AetheriaZoneState), typeof(AetheriaEntitySnapshot), + typeof(AetheriaRuntimeCommitDrainStatus), typeof(EveSurfaceState) ]; } diff --git a/Aetheria.State/AetheriaOperationsSurfaceProjector.cs b/Aetheria.State/AetheriaOperationsSurfaceProjector.cs new file mode 100644 index 000000000..6b6c44cc9 --- /dev/null +++ b/Aetheria.State/AetheriaOperationsSurfaceProjector.cs @@ -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 + }; + } +} diff --git a/Aetheria.State/AetheriaStateNode.cs b/Aetheria.State/AetheriaStateNode.cs index c86c91900..1944b168b 100644 --- a/Aetheria.State/AetheriaStateNode.cs +++ b/Aetheria.State/AetheriaStateNode.cs @@ -173,6 +173,16 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable return Database.GetAsync(new CultRecordKey(AetheriaCatalogSurfaceProjector.SurfaceKey)); } + public Task> PutOperationsSurfaceAsync(EveSurfaceState surface) + { + return Database.PutAsync(new CultRecordKey(AetheriaOperationsSurfaceProjector.SurfaceKey), surface); + } + + public Task GetOperationsSurfaceAsync() + { + return Database.GetAsync(new CultRecordKey(AetheriaOperationsSurfaceProjector.SurfaceKey)); + } + public Task> 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(key); } + public Task> PutRuntimeCommitDrainStatusAsync( + AetheriaRuntimeCommitDrainStatus status) + { + return Database.PutAsync(new CultRecordKey("global:aetheria.runtime_commit_drain_status.v1"), status); + } + + public Task GetRuntimeCommitDrainStatusAsync() + { + return Database.GetAsync( + new CultRecordKey("global:aetheria.runtime_commit_drain_status.v1")); + } + public Task FlushAsync(bool soft = false) { return _node.FlushAsync(soft); diff --git a/Aetheria.State/Documents/AetheriaRuntimeCommitDrainStatus.cs b/Aetheria.State/Documents/AetheriaRuntimeCommitDrainStatus.cs new file mode 100644 index 000000000..9cb42d16e --- /dev/null +++ b/Aetheria.State/Documents/AetheriaRuntimeCommitDrainStatus.cs @@ -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"; +} diff --git a/Aetheria.State/README.md b/Aetheria.State/README.md index 247d3908a..250fc2b33 100644 --- a/Aetheria.State/README.md +++ b/Aetheria.State/README.md @@ -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: diff --git a/Economy.Server/Program.cs b/Economy.Server/Program.cs index bacd10b21..3f9a51bcd 100644 --- a/Economy.Server/Program.cs +++ b/Economy.Server/Program.cs @@ -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) diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 9f467826d..7544db418 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -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.