Skip to main content

Concept

Grains are virtual actors: identity-addressed and managed by the framework. They are activated on first message and deactivated when idle. You don’t spawn them explicitly; you send to an identity and the framework routes to the right instance.

The Grain interface

Every grain must implement the Grain interface:
The Grain interface
  • OnActivate: called when the grain is loaded into memory (first message). Initialize state or resources. Return error to prevent activation.
  • OnReceive: called for each message. Handle messages. Use ctx.Response for Ask, ctx.NoErr for Tell success.
  • OnDeactivate: called before the grain is removed (passivation). Persist state, release resources.

GrainContext

Inside OnReceive, GrainContext provides:
  • Message: The message being processed.
  • Self: Grain identity.
  • Context: The context of the message; for an Ask it ends when the caller stops waiting, see Ask: the timeout reaches the grain.
  • Response: Reply to an Ask.
  • NoErr: Signal success for Tell (no response).
  • Err: Signal failure.
  • Unhandled: Mark message as unhandled.
  • TellActor: Send to an actor by name.
  • AskActor: Ask an actor by name.
  • AskGrain: Ask another grain.
  • TellGrain: Tell another grain; add WithOneWay() to return without waiting for the acknowledgement.
  • RequestGrain / RequestActor: Non-blocking async request; needs reentrancy.
  • DeferResponse: Hand off this Ask’s reply to a GrainReply for a later turn.
  • PipeToSelf, PipeToGrain, PipeToActor: Run a task off the message loop, deliver its result as a message; see Grain PipeTo.
  • ScheduleOnce, Schedule, ScheduleWithCron, CancelSchedule: Manage grain timers, which live with the activation. For schedules that outlive it, see grain scheduling.

Identity and messaging

Use the generic actor.GrainOf function to obtain an identity. The type parameter must be a pointer to your grain struct; the framework derives the grain kind from it, so no factory is required. Then use TellGrain or AskGrain with that identity. The instance name (e.g., "user-123") uniquely identifies the grain within its kind.
Identity and messaging

Options per kind

Grain options such as WithGrainMailboxCapacity and WithGrainDeactivateAfter can be given to GrainOf, but they then belong to that call: a bare AskGrain or TellGrain that finds the grain passivated reactivates it with the package defaults, and a grain activated on another node gets the default idle timeout. Declare them once per kind with WithGrainDefaultOptions on the actor system instead; a GrainOf call can still override them for one grain. Each kind gets its own declaration:
Options per kind
With the defaults declared, a bare send is safe and GrainOf is only needed to place or activate a grain with other options than its kind’s.

Ask: the timeout reaches the grain

The timeout of AskGrain is not only how long the caller waits. The grain knows the moment the caller stops waiting: the ask timeout, or the deadline of the caller’s context when that comes first.
  • An ask that expired in the mailbox is skipped. When the grain dequeues an ask whose caller already gave up, it does not hand it to OnReceive. Under load this keeps a slow grain from spending its turns on answers nobody reads, and a caller that timed out and retried does not get its work done twice.
  • Context() ends with the ask. Inside OnReceive, ctx.Context() carries that deadline, so a handler that passes it to a database query or an HTTP call stops when nobody waits for the answer. The context also ends when OnReceive returns, unless the reply was handed over with DeferResponse; in that case it ends at the deadline.
Stop working when the caller is gone
This holds whether the grain is on the calling node or on another one, with or without reentrancy. Tells are not affected: a tell is handled even when its sender no longer waits for the acknowledgement, and its context carries no deadline of its own.
An ask that is already in OnReceive when its caller gives up still runs to the end, unless the handler watches ctx.Context(). The caller sees ErrRequestTimeout while the work may have been done, so a handler whose callers retry must be idempotent.

Tell: acknowledged or one-way

TellGrain never returns a response payload, but by default it does wait for the grain to acknowledge the message: the call returns once OnReceive called NoErr, Err or Unhandled, bounded by DefaultGrainRequestTimeout, and surfaces the error the handler reported. Use this when the caller needs to know the message was processed. Add WithOneWay() to make the call fire-and-forget. The message is enqueued in the grain mailbox, locally or on the node that owns the grain, and the call returns without waiting for the grain to process it.
One-way tell
Why send one-way:
  • Lower latency for the caller. The call costs one enqueue locally, or one round trip to the owning node. It does not pay for the grain’s processing time, nor for the messages already queued ahead of it.
  • No coupling to the grain’s pace. A slow handler, a long mailbox, or a grain paused in StashNonReentrant mode cannot make the caller wait or time out. The message simply waits its turn.
  • Higher throughput. A producer is bounded only by the enqueue rate, so notifications, event fan-out, telemetry and other high-rate traffic flow without blocking the sender on each message.
  • Simpler senders. When the sender has nothing to do with the outcome, there is no acknowledgement to handle and no timeout to choose. The default TellGrain remains the right choice when the caller needs to know the message was processed.
With WithOneWay():
  • The caller still receives errors that occur before the enqueue: an invalid identity, a stopped system, an activation failure, a full bounded mailbox, or a transport failure toward a remote owner.
  • A failure the handler reports through Err or Unhandled, or a panic in OnReceive, is recorded as a deadletter on the node that ran the handler. The Deadletter event names the grain as receiver, using its identity kind/name as the name, and carries the message and the reason. A panic is logged as well.
  • The message rides the grain mailbox like any other, so ordering with other messages is preserved and the grain’s passivation activity is refreshed.
  • CorrelationID() is empty and DeferResponse() returns nil inside the handler, since there is nothing to reply to.
A one-way tell does not apply backpressure: with the default unbounded mailbox, a fast producer can grow the mailbox without limit. Use WithGrainMailboxCapacity when the producer must be slowed down by ErrMailboxFull.
A send that fails with ErrMailboxFull, ErrRequestTimeout or ErrDead returns the same error whether the grain is on the calling node or on another one, so errors.Is gives the same answer in both cases. When activation is needed, the framework constructs the grain as a zero value of its struct type. Put initialization logic in OnActivate and supply external resources (database handles, clients, configuration) through WithGrainDependencies; they are available from the GrainProps passed to OnActivate. This is the same construction contract the cluster applies when it recreates or relocates a grain on another node.
Calling GrainOf automatically registers the grain kind on the calling node. In cluster mode, nodes that can host a grain without ever resolving it themselves (for example, targets of relocation or remote activation) must register the kind at startup, either with RegisterGrainKind or through ClusterConfig.WithGrains.
The factory-based GrainIdentity(ctx, name, factory, opts...) API is deprecated. It still works, but factories that capture dependencies in their closure only take effect on the local node; a grain recreated or relocated elsewhere is always built as a zero value. Migrate to GrainOf with OnActivate-based initialization.

Scheduled delivery

The actor system’s scheduler can deliver a message to a grain later: once after a delay, at a fixed interval, or on a cron expression. ScheduleGrainOnce, ScheduleGrain and ScheduleGrainWithCron take the grain identity, deliver with a one-way tell, and activate the grain again if it has passivated. See Grain scheduling.
Scheduled delivery

Reentrancy

By default a grain processes one message at a time. Reentrancy lets a grain issue non-blocking requests to other grains or actors (RequestGrain, RequestActor) and keep processing while the replies are in flight, which is what lets request cycles (A asks B while B asks A back) complete instead of deadlocking. It also unlocks DeferResponse, which answers an incoming Ask only after an outbound request completes. Enable it at activation with WithGrainReentrancy or at runtime with ctx.EnableReentrancy. Modes, per-call options, deferred replies, the passivation interaction, and a complete request-cycle example are on the Grain Reentrancy page.

PipeTo

A grain processes one message at a time, so blocking on slow I/O inside OnReceive stalls every queued message. PipeToSelf, PipeToGrain, and PipeToActor offload that work: the task runs in a goroutine outside the message loop, the grain keeps processing, and the outcome comes back as an ordinary message (*StatusFailure on failure for grain targets). Methods, failure semantics, options, and a complete example are on the Grain PipeTo page.

When to use grains

  • Entity-per-identity patterns (users, sessions, devices)
  • Large populations that are mostly idle
  • When you want the framework to manage lifecycle and placement

When to use actors

  • Long-lived services
  • Explicit lifecycle control
  • Infrastructure components

Grain lifecycle

The framework keeps three promises to a grain: at most one activation per identity at a time, one message at a time, and OnDeactivate last.
OnDeactivate runs when a grain is passivated or its node shuts down cleanly. It does not run when a node crashes or is killed. Do not rely on OnDeactivate as the only place a grain saves its state.
  1. Activation. The first message for an identity activates the grain and calls OnActivate. If OnActivate fails, the caller gets the error.
  2. Messages. Each message runs through OnReceive, one at a time, in the order it reached the mailbox.
  3. Passivation. Once the grain has been idle for its deactivation timeout (WithGrainDeactivateAfter), it is deactivated and OnDeactivate runs.
    • Passivation waits for the message in progress to finish. OnDeactivate never runs during OnReceive.
    • Passivation is skipped when messages are waiting in the mailbox, because the grain is not idle. The idle timer starts again.
    • A reentrant grain is not passivated while a request is in flight. See Reentrancy.
  4. Messages that arrive during deactivation are not handled by the deactivating instance. Once OnDeactivate has finished, they are sent to a fresh activation in arrival order, and their callers get the reply from it. A caller that reached the instance in the last instant before it deactivated can still get ErrDead; sending again reaches the fresh activation.
  5. Reactivation. After passivation, the next message for the identity activates the grain again, on a new instance. While the grain is active, a message goes straight to it and GrainOf activates nothing.

During shutdown

When the actor system stops, every active grain handles the messages already in its mailbox, then is deactivated. This holds for a full bounded mailbox too: the shutdown request goes past its capacity instead of being refused. A grain whose OnDeactivate fails is named in the error Stop returns, and a grain still busy when the shutdown timeout expires is abandoned without running OnDeactivate. The node activates no new grains from the moment shutdown begins:
  • Local calls to GrainOf, TellGrain and AskGrain fail.
  • Requests from other nodes are refused with ErrSystemShuttingDown when they would activate a grain, while grains that are still active keep handling them until they are deactivated.
  • Messages queued behind a grain’s deactivation are refused with ErrSystemShuttingDown instead of being sent to a fresh activation.
In a cluster, GrainOf, GrainIdentity, TellGrain and AskGrain treat that refusal as a sign the node is leaving. GrainOf and GrainIdentity activate the grain on the calling node. TellGrain and AskGrain send the message once more in the same call, to the grain activated on the calling node, so a rolling restart needs no retry in the caller. See Activation guarantee.
A message refused with ErrSystemShuttingDown by the node was not processed, so it is safe to send it again. Outside a cluster, or when the caller cannot host the grain, retry such calls during a rolling restart.

Activation guarantee

In cluster mode, the system claims grain ownership in the cluster registry using an atomic put-if-absent operation before activating locally. If another node already owns the grain, requests are forwarded to that owner. If local activation fails after a successful claim, the claim is removed. A failed request to a remote owner rarely displaces that owner. An activation error or a handler error reported by the owner, or a request abandoned by the caller’s own context, is returned to the caller and the registry entry is kept. GrainOf, GrainIdentity, TellGrain and AskGrain release the entry, letting the grain re-activate elsewhere, in two cases only:
  • the owner answers that it is shutting down or has shut down. For a message, the owner’s node must have refused it before any handler ran: an ErrSystemShuttingDown that a grain handler returned does not release the entry;
  • a transport failure occurs, where the owner never answered, and the cluster membership confirms the owner’s node has left.
What TellGrain and AskGrain do with the message after the release depends on the case:
  • The owner refused the message. The message did not run, so it is sent once more in the same call. The calling node claims the grain with the configuration recorded in the released entry (role, mailbox capacity, reentrancy, dependencies, activation settings) and delivers the message to it. The second send uses what is left of the call’s timeout. When another node wins the claim, the message goes to that node. When the calling node cannot host the grain, because the grain kind is not registered on it or it lacks the grain’s role, the refusal is returned.
  • The owner left the cluster before the request could be sent. The calling node could not connect to the owner, so the message did not run. It is sent once more in the same call, the same way.
  • The owner left the cluster with the message in flight. The message may have run before the owner died, so it is not sent again: the transport error is returned. The next call reaches the grain on a live node.
A call sends the message again at most once. If the second send fails as well, its error is returned. A node that crashed cannot say it is gone. Until the cluster membership drops it, which takes a few seconds, calls to its grains fail with the transport error and its registry entries are kept: the node may only be unreachable. Right after the membership dropped it, a registry read can still time out with ErrClusterRegistryTimeout while the registry converges; that call can be sent again. While a node leaves, the registry can fail a write it still routes to that node. The writes an activation and a release need (the claim of the grain, the publication of its record and the release of an owner’s entry) are tried again a few times before the call fails.
The owner marks its refusals on the wire. During a rolling upgrade, an ErrSystemShuttingDown from an owner on an older version carries no mark: the error is returned and the entry is kept, as before.
A node that is shutting down takes no new grains (see During shutdown). A grain activation that finishes after the shutdown began is rolled back, so its registry entry does not outlive the node, and a node that is shutting down does not write the registry entry of a grain it already runs again. A grain becomes reachable on its node only once its registry entry is written. A message sent on that node meanwhile waits for the activation, then reaches the grain, or gets the activation’s error if the entry could not be written. The rollback of an activation whose entry could not be written therefore never runs OnDeactivate while a message sent to the grain is in OnReceive. A claim made on behalf of a remote peer follows the same rule. When the peer rejects the activation, the claim is rolled back so the next activation can pick another node. When the outcome is unknown, after a transport failure or an expired caller context, the claim is kept: the peer may already have activated the grain, and if it has not, it does so with the recorded configuration on the first message routed to it. Every release of a registry entry, by the departure cleanup, by an activation or a message that confirmed the owner left or was refused by it, by the rollback of a claim, or by the owner’s own deactivation and shutdown, deletes the entry only while it still names the expected node. The check and the deletion run under a cluster-wide lock on the grain, so a release that lost a race against a newer claim leaves that claim in place. When configured, the grain activation barrier delays grain activations until the cluster reaches the minimum peers quorum (or until the barrier timeout elapses).

See also

  • Placement: Where grains activate in a cluster: strategies, roles, and ownership
  • Grain Timers: Volatile, activation-scoped timers for periodic grain behavior
  • Grain Scheduling: Delayed, recurring and cron-based delivery of messages to grains
  • Passivation: Grains use passivation for idle-based deactivation
  • Actor Model: When to use actors vs grains