Keep bootstrap run records cold on restart

This commit is contained in:
Meta Vens 2026-07-20 03:00:28 +01:00
commit c20752cb85
4 changed files with 79 additions and 23 deletions

View file

@ -94,9 +94,9 @@ if (persistedRuntimeCatalog == null ||
nativeCatalogChanged)
await node.RefreshRuntimeCatalogAsync().ConfigureAwait(false);
TraceStartup("runtime-catalog");
await EnsurePlayableRunDocumentsAsync(node, options, startedAtUtc).ConfigureAwait(false);
await EnsurePlayableRunDocumentsAsync(node, options, startedAtUtc, latestFrame).ConfigureAwait(false);
TraceStartup("playable-run");
await EnsureGameSessionAsync(node, options, startedAtUtc).ConfigureAwait(false);
await EnsureGameSessionAsync(node, options, startedAtUtc, latestFrame).ConfigureAwait(false);
TraceStartup("game-session");
var verseHost = await EnsureVerseHostSettingsAsync(node, options, startedAtUtc).ConfigureAwait(false);
TraceStartup("verse-host");
@ -2964,7 +2964,8 @@ static async Task<bool> EnsureTradeValuePolicyAsync(AetheriaStateNode node, stri
static async Task EnsurePlayableRunDocumentsAsync(
AetheriaStateNode node,
AetheriaDaemonHostOptions options,
string now)
string now,
AetheriaRuntimeDaemonFrameDocument? latestFrame)
{
if (options.UseTerminusFixture)
{
@ -2973,6 +2974,9 @@ static async Task EnsurePlayableRunDocumentsAsync(
return;
}
if (HasPlayableRun(latestFrame?.Run))
return;
var settings = await node.MutableDocument<AetheriaPlayerSettings>(AetheriaStateNode.PlayerSettingsKey).ReadAsync().ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(settings?.ActiveRunKey))
{
@ -2991,15 +2995,13 @@ static async Task EnsurePlayableRunDocumentsAsync(
static async Task EnsureGameSessionAsync(
AetheriaStateNode node,
AetheriaDaemonHostOptions options,
string now)
string now,
AetheriaRuntimeDaemonFrameDocument? latestFrame)
{
var player = await node.MutableDocument<AetheriaPlayerSettings>(AetheriaStateNode.PlayerSettingsKey)
.ReadAsync().ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(player?.ActiveRunKey))
throw new InvalidDataException("Aetheria game session requires an active daemon run.");
var run = await node.MutableDocument<AetheriaRunState>(new CultRecordKey(player.ActiveRunKey))
.ReadAsync().ConfigureAwait(false)
?? throw new InvalidDataException($"Active Aetheria run is missing: {player.ActiveRunKey}");
var run = !options.UseTerminusFixture && HasPlayableRun(latestFrame?.Run)
? latestFrame!.Run
: await ReadRuntimeRunCheckpointAsync(node, options.RenderSettings).ConfigureAwait(false)
?? throw new InvalidDataException("Aetheria game session requires an authoritative frame or active bootstrap run.");
var expectedMode = string.IsNullOrWhiteSpace(run.GameMode)
? run.IsTutorial ? AetheriaGameSessionState.AetheriaMode : AetheriaGameSessionState.TerminusMode
: run.GameMode;
@ -3088,7 +3090,9 @@ static async Task<AetheriaRuntimeRunCheckpointCommit?> ReadRuntimeRunCheckpointA
if (string.IsNullOrWhiteSpace(settings?.ActiveRunKey))
return null;
var run = await node.MutableDocument<AetheriaRunState>(new CultRecordKey(settings.ActiveRunKey)).ReadAsync().ConfigureAwait(false);
var run = await ReadDurableBootstrapDocumentAsync<AetheriaRunState>(
node,
new CultRecordKey(settings.ActiveRunKey)).ConfigureAwait(false);
if (run == null)
return null;
@ -3097,7 +3101,9 @@ static async Task<AetheriaRuntimeRunCheckpointCommit?> ReadRuntimeRunCheckpointA
var zoneKeys = run.ZoneKeys ?? Array.Empty<string>();
for (var zoneIndex = 0; zoneIndex < zoneKeys.Length; zoneIndex++)
{
var zone = await node.MutableDocument<AetheriaZoneState>(new CultRecordKey(zoneKeys[zoneIndex])).ReadAsync().ConfigureAwait(false);
var zone = await ReadDurableBootstrapDocumentAsync<AetheriaZoneState>(
node,
new CultRecordKey(zoneKeys[zoneIndex])).ConfigureAwait(false);
if (zone == null)
continue;
@ -3179,7 +3185,9 @@ static async Task<AetheriaRuntimeZoneSnapshotCommit> ToRuntimeZoneAsync(
var entities = new List<AetheriaRuntimeEntitySnapshotCommit>();
for (var entityIndex = 0; entityIndex < entityKeys.Length; entityIndex++)
{
var entity = await node.MutableDocument<AetheriaEntitySnapshot>(new CultRecordKey(entityKeys[entityIndex])).ReadAsync().ConfigureAwait(false);
var entity = await ReadDurableBootstrapDocumentAsync<AetheriaEntitySnapshot>(
node,
new CultRecordKey(entityKeys[entityIndex])).ConfigureAwait(false);
if (entity != null)
entities.Add(ToRuntimeEntity(entityKeys[entityIndex], entity, entityIndex, entityIndices, catalog));
}
@ -3212,6 +3220,20 @@ static async Task<AetheriaRuntimeZoneSnapshotCommit> ToRuntimeZoneAsync(
};
}
static async Task<TDocument?> ReadDurableBootstrapDocumentAsync<TDocument>(
AetheriaStateNode node,
CultRecordKey key)
where TDocument : class
{
if (node.Cache.Get<TDocument>(key) == null)
{
await node.Cache.PullBackingStoreRecordsAsync(metadata =>
string.Equals(metadata.Key, key.ToString(), StringComparison.Ordinal)).ConfigureAwait(false);
}
return await node.MutableDocument<TDocument>(key).ReadAsync().ConfigureAwait(false);
}
static AetheriaRuntimeEntitySnapshotCommit ToRuntimeEntity(
string entityKey,
AetheriaEntitySnapshot entity,

View file

@ -717,6 +717,31 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s
await node.FlushAsync();
}
await using (var bootNode = await AetheriaStateNode.OpenAsync(
statePath,
"aetheria-state-smoke-daemon-boot",
hydrationProfile: AetheriaStateHydrationProfile.DaemonBoot))
{
if (bootNode.Cache.Get<AetheriaRuntimeDaemonFrameDocument>(AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest) == null)
throw new InvalidOperationException("Daemon boot did not hydrate the authoritative restart frame.");
if (bootNode.Cache.Get<AetheriaRunState>(runKey) != null ||
bootNode.Cache.Get<AetheriaZoneState>(zoneKey) != null ||
bootNode.Cache.Get<AetheriaEntitySnapshot>(entityKey) != null)
{
throw new InvalidOperationException("Daemon boot eagerly hydrated bootstrap run, zone, or entity records.");
}
await bootNode.Cache.PullBackingStoreRecordsAsync(metadata =>
string.Equals(metadata.Key, runKey.ToString(), StringComparison.Ordinal) ||
string.Equals(metadata.Key, zoneKey.ToString(), StringComparison.Ordinal) ||
string.Equals(metadata.Key, entityKey.ToString(), StringComparison.Ordinal));
if (bootNode.Cache.Get<AetheriaRunState>(runKey)?.RunId != "smoke" ||
bootNode.Cache.Get<AetheriaZoneState>(zoneKey) == null ||
bootNode.Cache.Get<AetheriaEntitySnapshot>(entityKey) == null)
{
throw new InvalidOperationException("Cold bootstrap records could not be pulled explicitly for frame recovery.");
}
}
await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aetheria-state-smoke-reopen"))
{
var world = await reopened.MutableDocument<AetheriaWorldState>(AetheriaStateNode.WorldKey).ReadAsync();

View file

@ -126,9 +126,6 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
var schemaTypes = new[]
{
typeof(AetheriaLoadoutTemplate),
typeof(AetheriaRunState),
typeof(AetheriaZoneState),
typeof(AetheriaEntitySnapshot),
typeof(AetheriaRuntimeAuthorityLeaseDocument),
typeof(AetheriaRuntimeDaemonCommandDocument),
typeof(AetheriaRuntimeCommittedCommandFactDocument),

View file

@ -77,22 +77,24 @@ under Unity `Assets/Scripts/ServerShared/Behaviors`.
retention. A service registry contains only Aetheria's explicitly declared
document types.
- **Inputs:** the indexed manifest, exact singleton boot keys, and the schema
identities for live commands, receipts, item/loadout definitions, and
current run/zone/entity state.
identities for live commands, receipts, and loadout definitions. The latest
committed daemon frame supplies restored gameplay truth; source run, zone,
and entity records remain cold.
- **Outputs:** a compact live CultCache working set. The runtime catalog core
contains items, corporations, trade settings, and name-file summaries; the
full name corpus is a separate typed cold record loaded explicitly only when
generation needs it.
- **Derived state:** the catalog Eve surface and ordinary daemon ticks use the
compact core. Authored name files remain migration/compiler inputs, not boot
compact core. Authored name files and source run/zone/entity documents remain
migration, generation, and missing-frame recovery inputs, not ordinary boot
dependencies.
- **Forbidden writers:** daemon boot must not rescan record pages, reread legacy
name files, or let global assembly discovery expand its registry. A filtered
flush must not erase cold records.
- **Shared paths:** first import writes the indexed representation; daemon boot
selects its working set from that index; new-run generation uses CultCache's
selective pull primitive to hydrate the name corpus through the same open
store.
selects its working set from that index; new-run generation and missing-frame
recovery use CultCache's selective pull primitive to hydrate exact bootstrap
records through the same open store.
- **Cut line:** the monolithic runtime catalog no longer embeds twelve megabytes
of names, and menu bootstrap no longer rescans loadout pages from disk.
- **Verification layer:** backing-store tests hold an unrelated cold page under
@ -116,6 +118,16 @@ canonical MessagePack fallback. This reduced the profiled 52-page hydration
from about 295 ms to 223 ms and cache open from about 353 ms to 296 ms. The
1.12 MB daemon frame and 574 KB runtime catalog remain the largest hot records.
The daemon frame is also the sole restored-run owner. `AetheriaRunState`,
`AetheriaZoneState`, and `AetheriaEntitySnapshot` are bootstrap/import records;
they are not tick-time mirrors and cannot override a playable committed frame.
Removing their schema-wide boot hydration reduced the profiled working set from
52 to 38 pages and one measured state-node open from 485 ms to 385 ms. If no
playable frame exists, checkpoint reconstruction explicitly pulls the active
run, then its referenced zones and entities by exact key. The state smoke proves
both that these records remain cold on an ordinary daemon boot and that the
durable fallback graph can still be recovered.
Ymir reconstruction remains full replay within a session generation, but it is
now bounded by an explicit quiescent generation rollover. When a private world
or payload journal reaches 64 commands, `AetheriaYmirWorldPhysics` asks Ymir to