Nebula
Persistence

NebulaPersistence

The worker's persistence service: it checkpoints every authoritative entity carrying a PersistentEntity into an IPersistenceStore, and brings those entities back when this worker gains the lease of a container they were saved in.

generated from the sources
public sealed class NebulaPersistence

The worker's persistence service: it checkpoints every authoritative entity carrying a PersistentEntity into an IPersistenceStore, and brings those entities back when this worker gains the lease of a container they were saved in. Owned by NebulaWorker and reached as worker.Persistence; null when persistence is off.

It is never on the per-tick path. Checkpoints run from the worker's Update, at most MaxSavesPerFrame of them per frame, and a store that is unreachable only means the saves are retried later: the mesh keeps simulating either way.

Restores are deliberately timid, because a persisted entity coming back must never become a second copy of one that is still alive somewhere. A freshly leased container waits NebulaConfig.PersistenceRestoreGraceSeconds so the previous owner's handover wins; a record whose key is already alive here is skipped; a record written moments ago by a worker that is still alive waits another grace period, in case that worker is about to hand the entity over; and if a handover arrives after a restore anyway, the incoming entity wins and the restored duplicate is despawned without deleting the record.

A worker that was declared dead but is still running is the case those rules cannot see, so two more hold the line there (docs/persistence-durability.md D8–D10). A worker whose own control-plane heartbeat is older than NebulaConfig.WorkerTimeoutSeconds is fenced: it saves and restores nothing until the heartbeat lands. And a restored entity's first checkpoint is taken straight away, so the record carries the new epoch and the store refuses whatever the previous owner still had queued.

Restores are also bounded in how hard they ask. A worker can gain hundreds of containers at once (a re-deal after another worker died, a rebalance, a burst of runtime chunks), and each frame the containers that are due are read in batches of at most MaxContainersPerRestoreLoad through IPersistenceStore.LoadContainers, with at most MaxRestoreLoadsInFlight of those batches waiting on the store at a time. The rest wait for a later frame. Nothing about a lease change can make the worker send an unbounded burst of requests to the orchestrator, which serves the control plane's heartbeats from the same address.

What rode in a carrier comes back with the carrier, and those reads are batched the same way. A carrier that spawns with authority is marked due; each frame the due carriers are read together through IPersistenceStore.LoadCarried, at most MaxCarriersPerRestoreLoad per read and at most MaxRestoreLoadsInFlight of those reads waiting at a time, so a restore that brings back a hangar full of vehicles costs one read rather than one per vehicle.

Fields

MaxSavesPerFrame

public const int MaxSavesPerFrame = 64;

Most saves issued in one frame, so a container full of dirty entities cannot spike the frame.

MinSaveIntervalSeconds

public const float MinSaveIntervalSeconds = 0.5f;

An entity saves at most this often, however often it changes.

PoseMoveThreshold

public const float PoseMoveThreshold = 0.25f;

Metres an entity must move since its last save before the move alone is worth a checkpoint.

PoseTurnThresholdDegrees

public const float PoseTurnThresholdDegrees = 5f;

Degrees an entity must turn since its last save before the turn alone is worth a checkpoint.

MaxContainersPerRestoreLoad

public const int MaxContainersPerRestoreLoad = PersistenceHost.MaxContainersPerLoad;

Most containers one restore read asks the store for (IPersistenceStore.LoadContainers).

MaxRestoreLoadsInFlight

public const int MaxRestoreLoadsInFlight = 2;

Most restore reads waiting on the store at once, counted separately for container reads and carrier reads. Containers or carriers that are due beyond that wait for a later frame.

MaxCarriersPerRestoreLoad

public const int MaxCarriersPerRestoreLoad = PersistenceHost.MaxCarriersPerLoad;

Most carriers one read of their cargo asks the store for (IPersistenceStore.LoadCarried).

Properties

Store

public IPersistenceStore Store { get; }

The store behind the service, for game code that wants to read or write records directly.

IsConnected

public bool IsConnected { get; }

The store is reachable. When false the mesh keeps running and saves are retried.

TrackedCount

public int TrackedCount { get; }

Persistent entities alive in this process (authoritative and ghosts).

SavedCount

public int SavedCount { get; }

Records this service has written since the process started.

RestoredCount

public int RestoredCount { get; }

Entities this service has brought back since the process started.

RestoreLoadsInFlight

public int RestoreLoadsInFlight { get; }

Restore reads (IPersistenceStore.LoadContainers) waiting on the store right now.

CarriedLoadsInFlight

public int CarriedLoadsInFlight { get; }

Reads of what carriers hold (IPersistenceStore.LoadCarried) waiting on the store right now.

OldestDirtyAgeSeconds

public float OldestDirtyAgeSeconds { get; }

How long the oldest currently-dirty tracked entity has been waiting for its next checkpoint, in seconds; 0 when nothing is dirty. A live reading of how much of the documented durability window (docs/persistence-durability.md) is in use on this worker right now; reported per worker on the heartbeat (WorkerInfo.OldestDirtySeconds) for the orchestrator dashboard.

Events

EntityRestored

public event Action<NetworkIdentity> EntityRestored;

An entity was brought back from the store and spawned on this worker.

ContainerRestored

public event Action<string, int> ContainerRestored;

Every persisted record of a container this worker has just gained has been read and dealt with: the container id, and how many entities were actually brought back (0 when there was nothing saved). Raised once per lease, on the main thread, after the grace period and the load. This is the seam the scope lifecycle waits on before a restored scope admits clients (docs/scope-lifecycle.md); NEB-242 exposes it to games as OnContainerRestored.

Methods

IsContainerRestored

public bool IsContainerRestored(string containerId);

The container's records have been read and restored since this worker gained its lease.

RestoredCountFor

public int RestoredCountFor(string containerId);

How many entities that restore brought back; 0 when it has not finished or there was nothing saved.

CheckpointContainer

public int CheckpointContainer(string containerId);

Save every authoritative persistent entity in containerId right now, whatever the checkpoint schedule says, and return how many were saved. The forced checkpoint of the retire sequence: the saves are issued here and IPersistenceStore.WhenWritten is the barrier that says they reached the store. Does not despawn anything. An entity riding in a vehicle in the container, at any depth, is in it too: emptying the container takes it (NebulaWorker.EmptyContainer), so it is saved here with everything else, aboard its carrier.

Find

public NetworkIdentity Find(string key);

The live entity saving under key in this process, or null.

Load

public void Load(string key, Action<PersistedEntityRecord> onLoaded);

Read one record. onLoaded runs on the main thread with null when there is none.

Delete

public void Delete(string key);

Delete the record for key. The live entity, if any, keeps running unsaved.

BuildRecord

public PersistedEntityRecord BuildRecord(NetworkIdentity identity);

Build the record for identity as it is right now: prefab or scene id, container and pose, and the state blob (PersistentStateCodec). Null when the entity is not persistent.

SaveNow

public void SaveNow(NetworkIdentity identity);

Checkpoint identity now, whatever its timer says. Use it at a moment the game knows is worth saving (a player logging out, a quest completed); the periodic checkpoint covers everything else. Nothing is written while the worker is fenced (NebulaWorker.IsFenced): the entity keeps its changes and is saved once the worker's heartbeat lands again.

Apply

public void Apply(PersistedEntityRecord record, NetworkIdentity identity, bool applyPose = true);

Put a record's state back onto identity. Works both before the entity is spawned (the restore path) and on an entity this worker already owns, which is what a rejoining player's pawn wants: the game spawns the pawn, asks for the record with Load and applies it when the answer arrives. Pass applyPose false to keep the entity where it is and take only its state. A spawned entity also keeps its current pose when this process has not registered the record's container or carrier, such as an unloaded chunk, because the saved position is relative to that container.

The entity also continues the record's epoch, as a restored one does (see Restore). An entity this worker owns moves to record.Epoch + 1 if its epoch is lower, and one that is not spawned yet spawns at that epoch, whichever spawn call the game uses. Either way its next checkpoint is taken at the next opportunity. Without this, a pawn spawned at epoch 1 and given a record saved at a later epoch would have every save refused by the store. A copy this worker does not own takes the state only.

Restore

public NetworkIdentity Restore(PersistedEntityRecord record);

Instantiate and spawn the entity a record describes, on this worker, with authority. Null when the prefab is unknown to this build or the scene object is not resident. The entity comes back at record.Epoch + 1, so anything still holding the old epoch is stale everywhere, and its first checkpoint is taken at the next opportunity rather than on the schedule: once the record carries the new epoch, the store refuses a late save from whoever held the previous life.

Plan

public static RestorePlan Plan(PersistedEntityRecord record, bool aliveHere, bool saverAlive, double recordAgeSeconds, float checkpointSeconds);

Whether a record should be restored, given what is alive here and whether the worker that wrote it is still running. A pure decision, so the rule that keeps a persisted entity from becoming a second copy of a live one can be reasoned about (and tested) on its own.

Nested types

RestorePlan

public enum RestorePlan

What the service decided to do with one record when a container's records came back.

Members

NameValueDescription
RestoreBring it back here and now.
SkipOwnedA client owned it: there is no client to own the result, so the game restores it on demand.
SkipAliveAn entity with that key is already alive in this process.
SkipCarriedIt lives inside a carrier and comes back with it, not with the container.
WaitIts saver is still alive and the record is fresh: it may yet arrive by handover.

On this page