How to prevent duplicated timer execution when deploying new Ergo nodes in Kubernetes? #263
|
Hello Ergo team, We are currently using Ergo to build a game server system, and we are running our services in a Kubernetes environment. We have encountered a design problem regarding Actor timers and rolling deployments. Our current architecture is roughly:
The problem appears when we deploy a new version using Kubernetes. During a rolling update: Now we may have two timers running for the same logical Actor: This can cause duplicated execution of business logic. For example:
We are trying to understand the recommended approach in the Actor model. Some possible solutions we are considering:
Example:
Example: However, we are not sure which approach fits best with the Ergo design philosophy. My questions are:
Any guidance or architectural recommendations would be greatly appreciated. Thank you very much. |
Replies: 1 comment 1 reply
|
Short answer first: the framework treats timers as local by design, and the duplicate you see is not really a timer problem. Below are the facts you can rely on, then brief sketches of the available solutions, because which one fits depends on numbers only you have. Timers. The cause. Two timers fire because two live activations of the same logical actor exist at the same moment. Process names are node-scoped ( The invariant worth building around. Treat the actor as a write-through cache with an ownership claim and storage as the source of truth: due work lives in a row, every unit of work has an identity, writes are conditional. Then a second activation cannot produce a second reward, only a rejected write. No scheduler gives exactly-once by itself; at-least-once firing plus an idempotent handler is what also survives a crash between "timer fired" and "reward written". Sketches: schedulingLocal timer, guarded handler. For work that only matters while the actor is alive (offline cleanup, periodic sync): keep the timer in the actor, re-derive it on activation from state you already persist ( Durable due-work. For work that must happen whether or not anyone is around (rewards, delayed actions with side effects): a row with One runner per cluster, plus a sweep. For global schedules and for picking up overdue work nobody activated: elect a single runner with Sketches: ownershipRegistry-first. The entity claims a key when it activates and any node resolves the owner with a local read: grid (ships with 3.3.0) gives key-to-owner, lifecycle monitors and per-key groups. Simplest for client-driven activation. Read its docs before committing: it is AP, so a claim is not a lock, a losing owner is stopped by a refusable exit and must not flush stale state, and the shard count is fixed for the life of the domain. Dispatcher-first. One leader-elected process decides where each entity activates; the target node's Sequenced handoff. When you want proof rather than a window: the new activation is told the current owner's PID, monitors it, then the old one is stopped through its own supervisor (a parent's exit cannot be refused, anyone else's can), and the new one starts working on the down message. A down with a real termination reason is proof the old owner flushed and its timers are gone; a down with Coarse-grained ownership. At large scale, do not own per player: own a room, a battle or a hash shard and keep players inside its owner. Fewer keys, co-location of players who talk to each other, and none of the per-login churn. One hosting detail either way: give each entity instance its own restart Sketch: the safety netExternal lease. Where a duplicate is expensive, a lease in Redis, etcd or a Kubernetes On the rollout, and what decides the restStop being chosen first (readiness, route), then stop entities (cancel timers, flush state), and keep the grace period above your real drain time. But Which combination fits depends on: how many entities are live at once, hundreds or millions; the activation rate during a rollout; and whether your storage can do a conditional update. A couple of hundred players with Postgres wants the first two scheduling sketches plus registry-first ownership and nothing else. Millions with heavy intra-group messaging wants coarse-grained ownership, and probably a dispatcher, before anything else. Tell us the scale and the constraints and we can be specific. Availability: |
Short answer first: the framework treats timers as local by design, and the duplicate you see is not really a timer problem. Below are the facts you can rely on, then brief sketches of the available solutions, because which one fits depends on numbers only you have.
Timers.
SendAfterandSendEverylive inside the process that created them: not persisted, not migrated, not deduplicated. Node cron is the same one level up: stateless, node-scoped, and a job name is unique within the node, so the same job on every pod fires once per pod. That answers your second question. Local only, never responsible for reliable execution.The cause. Two timers fire because two live activations of the same …