Keep bootstrap run records cold on restart
This commit is contained in:
parent
3b829f4924
commit
c20752cb85
4 changed files with 79 additions and 23 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue