Concept
A grain processes one message at a time. Reentrancy lets a grain issue non-blocking requests withRequestGrain and
RequestActor and keep processing while the replies are in flight. Replies are delivered back through the grain’s own
turn stream and continuations run on its turn, so grain state stays single-threaded throughout. This is what lets
request cycles (A requests B while B requests A back) complete instead of deadlocking until a timeout.
Reentrancy also changes how the grain is asked: an AskGrain against a reentrant grain is delivered as a correlated
request instead of a channel-backed call. That unlocks DeferResponse, which answers an incoming Ask only after an
outbound request completes, without ever blocking a turn.
Modes, the RequestCall handle (Then, Cancel), and per-call options are shared with
actor reentrancy. This page covers the grain-side API and semantics.
Modes
- Off: The default.
RequestGrain/RequestActorfail withErrReentrancyDisabled; asks take the legacy blocking path. - AllowAll: The grain keeps processing every incoming message while requests are in flight. Required for request cycles.
- StashNonReentrant: The grain stops consuming its mailbox while any stash-mode request is in flight. Buffered messages, including timer ticks, wait in place; only responses are processed.
Unlike actors, a grain in
StashNonReentrant mode does not move messages to a stash buffer. Consumption of the
mailbox pauses, nothing is relocated, and processing resumes in exact arrival order when the last blocking request
completes.Enabling reentrancy
At activation, passWithGrainReentrancy:
Enabling reentrancy
Enabling reentrancy
EnableReentrancy installs the policy or retunes an existing one (mode, MaxInFlight) and takes effect for requests
issued from that point on. DisableReentrancy flips the default mode back to Off: requests already in flight complete
with the mode they were admitted with, a paused grain still unpauses, asks revert to the legacy path, and new requests
fail with ErrReentrancyDisabled. After a runtime disable, a per-call WithReentrancyMode override still admits an
individual request.
RequestGrain and RequestActor
- ctx.RequestGrain: Async request to another grain, including the grain itself. The target activates on demand, locally or on its owning node.
- ctx.RequestActor: Async request to a named actor, local or remote. The actor replies with
ctx.Responseas it does for actor-to-actor requests.
RequestCall handle actors use: register a continuation with Then(func(any, error)), cancel with
Cancel(). Continuations run on the grain’s turn when the response arrives.
Failures never abort the current message and never complete the grain’s own caller: a request that cannot be issued
(reentrancy disabled, invalid target, in-flight limit, delivery error) returns an already-completed handle whose Then
fires immediately with the error. This differs from the actor-side Request, which records the failure on the
ReceiveContext.
In the opposite direction, an actor requests a grain with ReceiveContext.RequestGrain; see
actor reentrancy.
Per-call options and timeout
Grain requests default toDefaultGrainRequestTimeout (5 seconds), unlike actor requests, which have no default.
- WithRequestTimeout: Change the timeout for this request. On expiry the continuation runs with
ErrRequestTimeout.d <= 0disables the timeout. - WithReentrancyMode: Override the grain’s mode for this request only.
Deferring a reply
A grain has noSender() to capture. To answer an incoming Ask only after an outbound request completes, the handler
transfers reply ownership with DeferResponse and completes the returned GrainReply from the continuation:
Deferring a reply
DeferResponseis one-shot. After the call, the turn’s ownResponse,Err,NoErr, andUnhandledbecome no-ops for this message; exactly one reply owner exists at any time.- The handle is one-shot too: the first of
GrainReply.Response,Err, orNoErrwins, later calls do nothing. - The handle holds only reply metadata, never the context, so it stays valid indefinitely and can complete the request on any later turn.
- For a message that did not arrive as a correlated request (a Tell, an Ask against a non-reentrant grain, or a timer
tick) there is nothing to defer:
DeferResponsereturns nil, and the nil handle is safe to complete.
ctx.CorrelationID() returns the correlation ID of the request being processed, or an empty string for ordinary
messages; useful for tracing a request across grains.
Errors
All request failures surface through the returned call’sThen continuation.
- ErrReentrancyDisabled: The grain has no reentrancy policy, or the effective mode is
Offwith no per-call override. - ErrReentrancyInFlightLimit: The
MaxInFlightcap is reached; no new requests until some complete. - ErrRequestTimeout: The request timed out (default
DefaultGrainRequestTimeout, or the per-callWithRequestTimeoutvalue). - ErrRequestCanceled: The request was canceled with
Cancel, or the system shut down while it was in flight.
Passivation interaction
- While requests are in flight the passivation clock is suspended: the grain cannot be deactivated mid-request. When the last request completes, passivation re-arms with a fresh idle deadline.
- Issuing a request and receiving its response both count as activity for idle tracking.
- A deferred reply obligation alone does not keep the grain alive; only in-flight requests do. Complete deferred replies from the request continuation, not from a detached goroutine.
- On system shutdown, in-flight requests are canceled first: continuations run with
ErrRequestCanceled, deferred callers receive the error, and the grain then deactivates normally. No new request is admitted once the system is stopping:RequestGrainandRequestActorcomplete at once withErrActorSystemNotStarted, so a request issued after the cancellation cannot pause the grain past its deactivation.
Example: a request cycle
OrderGrain answers an external AskGrain by requesting a price from PricingGrain. Before answering, PricingGrain
requests the customer discount back from OrderGrain. Both grains run AllowAll, so every hop processes without
pausing and the cycle completes without a timeout.
Example: a request cycle
Limitations
- In-flight requests do not survive requester relocation or a node crash; late replies arriving at a fresh activation are dropped and logged at debug level.
- Requests that cross nodes carry their payload as protobuf: the message must be a
proto.Message, like every other remote send. Local requests accept any Go value. TellGrainkeeps its acknowledgement semantics by default; deferred replies apply to Ask and Request flows. Consequently aTellGrainagainst a grain paused inStashNonReentrantmode can returnErrRequestTimeouteven though the message is delivered and processes after resume. PreferRequestGraintoward reentrant grains, or send withWithOneWay()when no acknowledgement is needed: the message then waits in the mailbox until the grain resumes.StashNonReentrantwithWithRequestTimeout(d)whered <= 0pauses the grain until the reply arrives; if the reply is lost (target node crash), only shutdown unblocks it. Keep the default timeout in stash mode.- Interval and cron timer ticks accumulate in the mailbox during a long pause and replay on resume. With a bounded
mailbox, once a pause fills it, new user messages are refused with
ErrMailboxFulland timer ticks are dropped with a warning; the shutdownPoisonPillis a system message and is always accepted. Use an unbounded mailbox (the default) withStashNonReentrant.
See also
- Reentrancy for actors: modes in depth,
RequestCall, and the actor-sideRequestGrain. - Grains for grain lifecycle, identity, and messaging.
- Grain Timers for how timer ticks interact with a paused grain.