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.
Nebula · Packages/com.1by3.nebula/Runtime/Persistence/NebulaPersistence.cspublic sealed class NebulaPersistenceThe 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 RestorePlanWhat the service decided to do with one record when a container's records came back.
Members
| Name | Value | Description |
|---|---|---|
Restore | Bring it back here and now. | |
SkipOwned | A client owned it: there is no client to own the result, so the game restores it on demand. | |
SkipAlive | An entity with that key is already alive in this process. | |
SkipCarried | It lives inside a carrier and comes back with it, not with the container. | |
Wait | Its saver is still alive and the record is fresh: it may yet arrive by handover. |