> ## Documentation Index
> Fetch the complete documentation index at: https://docs.goakt.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Reentrancy

> Reentrant messaging modes and StashNonReentrant.

## Concept

By default, an actor processes messages strictly one at a time. **Reentrancy** allows an actor to process other messages while waiting for an async response from `Request` or `RequestName`.

`Request` and `RequestName` are the non-blocking counterparts of `Ask` and `SendSync`. They return immediately with a `RequestCall` handle instead of blocking until a reply arrives. The reply is delivered back through the actor's mailbox, preserving single-threaded processing.

## Modes

| Mode                  | Behavior                                                                                                                                                                                                                 |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Off**               | Strict sequential; `Request`/`RequestName` disabled. Use `Ask`/`SendSync` for sync calls.                                                                                                                                |
| **AllowAll**          | All incoming messages can be processed while awaiting a response. Maximizes throughput.                                                                                                                                  |
| **StashNonReentrant** | User messages are stashed while any stash-mode request is in flight; only async responses and system messages (e.g. `PoisonPill`, `HealthCheckRequest`) are processed. Preserves strict ordering at the cost of latency. |

## Enabling reentrancy

Reentrancy is **opt-in**. Pass `WithReentrancy` when spawning the actor:

```go theme={"theme":{"light":"github-light","dark":"dracula"}}
import "github.com/tochemey/goakt/v4/reentrancy"

cfg := reentrancy.New(
    reentrancy.WithMode(reentrancy.AllowAll),
    reentrancy.WithMaxInFlight(10),  // optional: cap concurrent requests
)
pid, err := system.Spawn(ctx, "my-actor", actor, actor.WithReentrancy(cfg))
```

| Option               | Purpose                                                                                                                                             |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `WithMode(mode)`     | Set reentrancy mode: `Off`, `AllowAll`, or `StashNonReentrant`.                                                                                     |
| `WithMaxInFlight(n)` | Cap the number of outstanding async requests. When reached, `Request`/`RequestName` return `ErrReentrancyInFlightLimit`. Use `n <= 0` for no limit. |

Without `WithReentrancy`, calls to `Request` or `RequestName` fail with `ErrReentrancyDisabled`.

## Request and RequestName

| Method                                    | Purpose                                                                                        |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `ctx.Request(to, message, opts...)`       | Send an async request to a PID. Returns a `RequestCall` or `nil` on failure.                   |
| `ctx.RequestName(name, message, opts...)` | Send an async request to an actor by name (local or remote). Returns a `RequestCall` or `nil`. |

On failure to initiate the request, `Err` is set on the context and the returned call is `nil`. Check `ctx.Err()` or use `ctx.getError()` in tests.

## RequestCall

The returned `RequestCall` lets you:

| Method           | Purpose                                                                                                                                                                                                                                        |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Then(callback)` | Register a continuation `func(any, error)` invoked when the request completes (success, error, timeout, or cancel). Only the first call is honored. If the request already completed, the callback runs immediately in the caller's goroutine. |
| `Cancel()`       | Request cancellation. Best-effort; the request may still complete. Idempotent. Returns `nil` if already completed or cancel requested.                                                                                                         |

Continuations registered with `Then` run on the actor's mailbox thread when the request completes, preserving single-threaded access to actor state. Call `Then` from within `Receive` to ensure correct execution.

## Per-call options

| Option                        | Purpose                                                                                                |
| ----------------------------- | ------------------------------------------------------------------------------------------------------ |
| `WithReentrancyMode(mode)`    | Override the actor-level mode for this request only. Cannot enable if actor has reentrancy off.        |
| `WithRequestTimeout(timeout)` | Set a per-request timeout. On expiry, the request completes with `ErrRequestTimeout`. `<= 0` disables. |

```go theme={"theme":{"light":"github-light","dark":"dracula"}}
call := ctx.Request(target, msg,
    actor.WithReentrancyMode(reentrancy.StashNonReentrant),
    actor.WithRequestTimeout(5*time.Second),
)
if call != nil {
    call.Then(func(resp any, err error) {
        if err != nil {
            // handle timeout, cancel, or remote error
            return
        }
        // use resp
    })
}
```

## Errors

| Error                        | When                                                                    |
| ---------------------------- | ----------------------------------------------------------------------- |
| `ErrReentrancyDisabled`      | Actor was not spawned with `WithReentrancy`, or per-call mode is `Off`. |
| `ErrReentrancyInFlightLimit` | `MaxInFlight` cap reached; no new async requests until some complete.   |
| `ErrRequestTimeout`          | Request timed out (via `WithRequestTimeout`).                           |
| `ErrRequestCanceled`         | Request was canceled via `Cancel()`.                                    |

## StashNonReentrant and stashing

In `StashNonReentrant` mode, user messages are automatically stashed while any stash-mode request is in flight. A stash buffer is created on demand; you do **not** need `WithStashing()` for reentrancy-driven stashing. When the last blocking request completes, stashed messages are unstashed and processed in order.

## When to use

* **Actors that make async requests** and need to stay responsive (e.g. fan-out, long I/O via PipeTo).
* **Avoiding deadlock** in call cycles (A -> B -> A). Use `AllowAll` so A can process B's reply while waiting.
* **Strict ordering** when you must not interleave user messages with async responses—use `StashNonReentrant`.

## Production notes

* Prefer **AllowAll** for throughput and to avoid deadlocks in call cycles.
* Use **StashNonReentrant** only when strict message ordering is required. Pair it with:
  * A finite `MaxInFlight` limit to bound memory.
  * Per-request timeouts (`WithRequestTimeout`) to avoid unbounded stashing if dependencies stall.
* `AllowAll` can introduce state races if your logic assumes strict ordering between request and response.
* Mixed-version clusters may decode unknown modes as `Off`, disabling async requests.

## Example

Reentrancy enables safe request/response cycles without deadlock. Flow: Client → ActorA → ActorB → ActorA → Client. ActorA uses `RequestName` (non-blocking); ActorB uses `Ask` (blocking). Without reentrancy, ActorA would block and the cycle would deadlock.

```go theme={"theme":{"light":"github-light","dark":"dracula"}}
package main

import (
    "context"
    "fmt"
    "time"

    "github.com/tochemey/goakt/v4/actor"
    "github.com/tochemey/goakt/v4/reentrancy"
)

// Message types for the reentrancy cycle.
type Ping struct{}
type GetCount struct{}
type Count struct{ Value int } // Expected response: Value 42

// Client starts the cycle on PostStart and signals doneCh when it receives Count.
type Client struct {
    target *actor.PID
    doneCh chan struct{}
}

func (c *Client) PreStart(ctx *actor.Context) error { return nil }
func (c *Client) PostStop(ctx *actor.Context) error  { return nil }

func (c *Client) Receive(ctx *actor.ReceiveContext) {
    switch msg := ctx.Message().(type) {
    case *actor.PostStart:
        ctx.Tell(c.target, &Ping{}) // 1. Kick off the cycle
    case *Count:
        fmt.Println("Count:", msg.Value) // 6. Expected: Count: 42
        select {
        case c.doneCh <- struct{}{}:
        default:
        }
    default:
        ctx.Unhandled()
    }
}

// ActorA uses RequestName (non-blocking) so it can process ActorB's Ask while waiting.
type ActorA struct{ targetName string }

func (a *ActorA) PreStart(ctx *actor.Context) error { return nil }
func (a *ActorA) PostStop(ctx *actor.Context) error  { return nil }

func (a *ActorA) Receive(ctx *actor.ReceiveContext) {
    switch ctx.Message().(type) {
    case *Ping:
        sender := ctx.Sender()
        self := ctx.Self()
        // 2. Non-blocking request to ActorB; returns immediately
        call := ctx.RequestName(a.targetName, &Ping{}, actor.WithRequestTimeout(2*time.Second))
        if call == nil { return }
        call.Then(func(resp any, err error) {
            if err != nil || sender == nil { return }
            // 5. Forward response back to Client
            _ = self.Tell(context.Background(), sender, resp)
        })
    case *GetCount:
        // 4. ActorB's Ask arrives while we wait; reentrancy allows us to handle it
        ctx.Response(&Count{Value: 42})
    default:
        ctx.Unhandled()
    }
}

// ActorB uses Ask (blocking) to call back into ActorA.
type ActorB struct{ actorA *actor.PID }

func (b *ActorB) PreStart(ctx *actor.Context) error { return nil }
func (b *ActorB) PostStop(ctx *actor.Context) error { return nil }

func (b *ActorB) Receive(ctx *actor.ReceiveContext) {
    switch ctx.Message().(type) {
    case *Ping:
        // 3. Ask ActorA for count; ActorA must be reentrant to respond
        resp := ctx.Ask(b.actorA, &GetCount{}, 2*time.Second)
        ctx.Response(resp)
    default:
        ctx.Unhandled()
    }
}

func main() {
    ctx := context.Background()
    system, _ := actor.NewActorSystem("reentrancy-demo", actor.WithLoggingDisabled())
    _ = system.Start(ctx)
    defer system.Stop(ctx)

    doneCh := make(chan struct{}, 1)
    actorA, _ := system.Spawn(ctx, "actor-a", &ActorA{targetName: "actor-b"},
        actor.WithReentrancy(reentrancy.New(
            reentrancy.WithMode(reentrancy.AllowAll),
            reentrancy.WithMaxInFlight(4),
        )))
    _, _ = system.Spawn(ctx, "actor-b", &ActorB{actorA: actorA})
    _, _ = system.Spawn(ctx, "client", &Client{target: actorA, doneCh: doneCh})

    // Expected: "Count: 42" then "Reentrancy cycle completed."
    select {
    case <-doneCh:
        fmt.Println("Reentrancy cycle completed.")
    case <-time.After(3 * time.Second):
        fmt.Println("Timeout.")
    }
}
```

## See also

* [Stashing](/actor/stashing) — Manual stashing with `Stash`, `Unstash`, `UnstashAll`; `StashNonReentrant` uses stashing internally.
* [Messaging](/actor/messaging) — `Ask`, `Tell`, `PipeTo`.
* [Behaviors](/actor/behaviors) — `Become`, `UnBecome` for state transitions.
