Skip to content

Consistency and delivery

ankka is strongly consistent within one entity and eventually consistent everywhere else. An entity sees every command in order, one at a time, against its current state. Views and consumers see an entity's changes shortly after they happen, in order per entity, exactly once or at least once depending on the source. This page states each guarantee and what it asks of your code.

One entity, one writer, one command at a time

Every entity id — of an event sourced entity, a key value entity, a workflow, or an agent session — is hosted by exactly one instance of the service at a time. Commands to that id are processed one after another: a handler sees the state left by the previous command, and its effect is applied before the next command is read.

So a check and the change it guards are atomic within one entity, with no locks and no retries on conflict. A command that reads the entity after another command succeeded always sees that command's effect. Reading an entity by id is therefore strongly consistent, and the right way to read your own write.

The price is that one entity's throughput is one command at a time. Many entities run in parallel across the instances of the service; one very busy entity does not. Designing a service covers sizing entities.

The journal is the source of truth

An event sourced entity's events are written to the journal in Postgres before the command's reply is sent. The entity's state in memory is a cache of the fold of those events. An idle entity is dropped from memory after two minutes, and an instance that stops or fails loses its entities; either way the next command to that id rebuilds the state by replaying the journal, starting from the latest snapshot. By default an event sourced entity is snapshotted every 100 events, so a rebuild replays at most that many.

The same holds for the other stateful kinds. A key value entity's value is in the durable state table, a workflow's progress is in its journal, and an agent session's conversation is an entity's events. Nothing that matters lives only in an instance's memory.

A timed-out call may still have happened

A call through the component client waits for a reply for ten seconds by default (ankka.ask-timeout) and then fails with the Timeout error code. A timeout means the caller stopped waiting, not that the command was not applied: the entity may have persisted its events a moment later. Timeouts and Unavailable are the two error codes a caller can reasonably retry, and a retry of a command that did happen must be safe. Make commands idempotent where a retry is likely — for example by carrying an id the entity can recognise as already handled. A call granted from another project is no different: it is delivered at least once, and the idempotency key the callee recognises is the callee's own, whatever the caller sends.

Views are eventually consistent

A view is maintained from its source's changes by a projection that runs in the background. A change is reflected in the view shortly after it is persisted, usually well within a second, but not before the command's reply is sent. So:

  • After a successful command, the entity reflects the change immediately; the view may not yet.
  • A view query can return a row that is slightly behind its entity.
  • A view is the right place to list and search, and the wrong place to check a rule that must hold.

Delivery to views and consumers

How often a view or consumer sees each change depends on its source:

Source Views Consumers
An event sourced entity's events exactly once at least once
A key value entity's state changes at least once at least once
A broker topic at least once at least once

A view over an event sourced entity gets exactly-once delivery because the row write and the record of how far the projection has read happen in one Postgres transaction: either both happen or neither does. Everywhere else the offset is recorded after the handler has run, so a crash between the two delivers the change again. A handler that is delivered a change at least once must produce the same result the second time:

  • A view handler that sets a row from a key value entity's new value is naturally repeatable.
  • A consumer that calls a command should call one the target can recognise as already done.
  • A consumer that produces to a topic may produce a duplicate, and the receiving side must tolerate it.

A consumer that publishes several messages for one change publishes them at least once, in order, and not atomically. The change counts as handled when the broker has accepted every one of its messages. If one is refused, the change is delivered again and all of them are published again, so a reader may see a message twice and never sees one missing; the messages already accepted are not withdrawn. Progress through an entity's changes is saved in batches, so a failure redelivers every change since it was last saved, not only the one that failed.

A graph consumer is built so that the repeats cost nothing: every record it publishes is one element's whole state at the version of the change, and a reader that keeps the highest version is unmoved by a record it has already seen.

A view or consumer over a key value entity sees the entity's state as changes arrive. It is not a history, so it may not see every intermediate value when several changes happen close together. Use an event sourced entity when each individual change matters to what reacts to it.

Ordering

Changes from one entity reach a view or consumer in the order they happened. Changes from different entities have no defined order relative to each other.

Messages from a broker topic keep their order per record key. ankka keys each published message by its subject, the id of the entity it is about, unless the message names a key of its own, so every message about one entity lands on the same partition and is delivered in order. The several messages of one change are handed to the broker in the order the handler returned them. A topic source cannot be rebuilt from history: a component reading one sees only what was published after it started consuming, because a broker's retention is not an event journal. Broker topics covers the details.

Workflows

A workflow records each transition — its new state and the next step — before that step runs. After a crash, the workflow resumes at the step it was on. A step that had already acted but whose outcome was not yet recorded runs again, so each step's calls must be safe to repeat, for example by passing the workflow id as an idempotency key to an external system.

A step that throws is retried as the workflow's settings declare, then failed over to the declared recovery step. A step that returns a failure on purpose ends the workflow without retry. Workflows shows how to declare both.

Timers

A timer is stored in the database when it is scheduled and survives any restart. When it is due, one instance of the service calls its timed action. The call is made at least once: a handler that returns an error, throws, or cannot be reached is retried with backoff, and the timer is deleted only when the handler reports success. So a timed action must check the current state before acting, and report success when there is nothing left to do — returning an error for work that no longer applies retries it forever.

Agents

An agent session is handled one request at a time, like an entity. Two requests to the same session never interleave, so each turn sees the whole conversation so far and appends to it. Different sessions run in parallel. A streaming response holds its session until the stream ends.