Zum Inhalt springen
Deutsch

Remember Entities

Sharded Entities sind standardmäßig lazy: ein Entity-Actor existiert erst, nachdem er seine erste Nachricht empfangen hat. Ohne rememberEntities bedeutet ein Cluster-Kaltstart oder Koordinator-Failover, dass die aktive Entity-Menge leer ist — Entities werden bei Bedarf neu gespawnt, sobald Nachrichten ankommen.

Für viele Workloads ist das in Ordnung. Manchmal aber willst du, dass Entities eager neu erstellt werden:

  • Sie haben geplante Hintergrundarbeit, die unabhängig von eingehenden Nachrichten laufen sollte.
  • Sie haben etwas Externes abonniert und müssen am Leben sein, um es zu verarbeiten.
  • Failover sollte das volle Arbeitsset sofort wiederherstellen, nicht durchsickern, sobald Nutzer auf jede Entity treffen.

rememberEntities: true ist der Schalter.

const startShardingOptions = StartShardingOptions.create()
.withTypeName('session')
.withEntityActor(SessionActor)
.withExtractEntityId((message) => message.userId)
.withRememberEntities(true);
sharding.start(
startShardingOptions,
);

Was es tut:

  • Die Existenz jeder Entity (ihre ID) wird in einem Remember-Entities-Store persistiert.
  • Beim Cluster-Kaltstart oder nach einem Koordinator-Failover liest der Koordinator die Menge bekannter Entity-IDs und bittet jede Shard-Region, sie eager neu zu spawnen.

Der Default-Store nutzt das Journal des Systems, sodass du das umsonst bekommst, sobald Persistenz konfiguriert ist. Überschreibe mit rememberEntitiesStore, wenn du einen anderen Backing-Store brauchst.

Nur die Identitäten aktiver Entities — nicht ihr Zustand. Der Entity-Zustand ist die Verantwortung der Entity selbst (PersistentActor journalisiert Zustandsänderungen; Remember-Entities verfolgt, welche IDs zurückgebracht werden sollen).

Zwei-Speicher-Aufteilung:

Journal (deine Events) RememberEntitiesStore (nur IDs)
persistenceId=cart-user-42 entityIds = ['cart-user-42', 'cart-user-43', ...]
Event 1
Event 2
Event 3

Das ist absichtlich — der Remember-Store hat andere Zugriffsmuster (kleine, häufige Writes, wenn Entities starten/stoppen) und profitiert davon, separierbar zu sein.

  • Der Shard, zu dem eine gegebene Entity gehört hat. Bei Respawn läuft die Allokation erneut (per Allokationsstrategie).
  • Der Zustand der Entity. Wenn du willst, dass Zustand überlebt, muss die Entity ihn selbst persistieren — über PersistentActor oder externen Storage.

Wenn du rememberEntities: true verwendest, aber kein Journal konfiguriert hast:

const startShardingOptions = StartShardingOptions.create()
.withTypeName('...')
.withEntityActor(...)
.withExtractEntityId(...)
.withRememberEntities(true)
.withRememberEntitiesStore(null);
sharding.start(
startShardingOptions, // explizites Opt-Out aus Persistenz
);

null zu übergeben fällt zurück auf das v1-In-Memory-Only-Verhalten — die Entity-Menge wird im Prozessspeicher verfolgt und bei Koordinator-Failover verloren. Nützlich für Dev / Tests; sinnlos in Produktion.

Ohne rememberEntitiesStore: null und ohne konfiguriertes Journal instanziiert das Framework automatisch einen JournalRememberEntitiesStore mit dem (standardmäßig In-Memory) Journal — was zum Testen in Ordnung ist, aber einen Neustart nicht überlebt. Stelle sicher, dass dein Journal in Produktion dauerhaft ist.

import type { RememberEntitiesStore, RememberEvent } from 'actor-ts';
class RedisRememberEntitiesStore implements RememberEntitiesStore {
async append(typeName: string, event: RememberEvent): Promise<void> { /* RPUSH das Event als JSON */ }
async load(typeName: string): Promise<RememberEvent[]> { /* LRANGE 0 -1 → jeweils JSON.parse */ return []; }
async clear(typeName: string): Promise<void> { /* DEL den Key */ }
}
const shardingOptions = StartShardingOptions.create()
// ...
.withRememberEntities(true)
.withRememberEntitiesStore(new RedisRememberEntitiesStore());
sharding.start(shardingOptions);

Die Schnittstelle ist klein. Nützlich, wenn du die Entity-Registry auf einem anderen Backing-Store als den Rest deiner Persistenz willst (z. B. Redis für latenzarme Entity-Set-Lookups, während das Journal auf Cassandra liegt).

rememberEntities: true fügt zwei Write-Operationen pro Entity-Lebenszyklus hinzu:

  1. Ein Write, wenn die Entity spawnt (ein append eines started-Events).
  2. Ein Write, wenn die Entity passiviert / stoppt (ein append eines stopped-Events).

Für High-Churn-Entities (Millionen kurzlebiger Sessions pro Stunde) ist das echter Overhead. Erwäge:

  • rememberEntities für kurzlebige Workloads überspringen.
  • Einen schnellen Store (In-Memory-Redis) statt des Default-Journals nutzen.
  • Writes batchen, wenn du einen eigenen Store implementierst.

Für Low-Churn-Workloads (10K stabile Entities) sind die Kosten vernachlässigbar.

SzenariorememberEntities nutzen?
Per-User-Sessions, kurzlebig, beim nächsten Request neu spawnenNein
Lange laufende Koordinatoren, müssen leben, um geplante Arbeit zu tunJa
IoT-Device-Handler, müssen beim Start MQTT-Topics abonnierenJa
Per-Tenant-Cache, teuer zu rebauenJa
Per-Order-Saga, muss nach Failover dort weiter, wo sie aufgehört hatJa
Per-Request-temporärer-Actor, stirbt in SekundenNein

Die Regel: wenn “brauche ich diese Entity am Leben auch ohne kürzliche Nachricht?” ein Ja ist, aktivieren.

Passivierung stoppt idle Entities. Mit rememberEntities:

Entity aktiv → passiviere (idle) → Entity stoppt → Store: append stopped?

Verhalten des Frameworks:

  • Idle-Timeout-Passivierung (passivationIdleMs) — hängt ein stopped-Event an. Nächste Nachricht spawnt sie neu; der Store empfängt erneut ein started-Event.
  • maxEntities-LRU-Passivierung — dasselbe.
  • Passivate von der Entity selbst — dasselbe.

Für Workloads, in denen du eager Re-Spawn auch von kürzlich inaktiven Entities willst, setze passivationIdleMs: 0, damit sie bleiben.

Ein Shard-Actor kann außerhalb eines Rebalance sterben — etwa jenseits seines Supervision-Budgets. Die Region behält die Ownership (so erstellt die nächste Nachricht den Shard neu) und fragt beim Koordinator nach, was er sich für diesen Shard merkt, sodass die Entities von allein zurückkommen, ohne auf Verkehr zu warten. Es wird dabei nichts als stopped verzeichnet: die Entities sollten nie gehen.

Shard-Passivierung berührt die Registry gar nicht — passiviert wird immer nur ein leerer Shard, und seine Entities haben ihre eigenen stopped-Events zu diesem Zeitpunkt längst gemeldet.