Actors stay unreachable ("Actor not found") after an engine node is lost, with the Postgres driver in multi-node mode

Abierto
#5,915 1 comentario 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
5/5
Tiempo estimado
Más de una semana
Aptitud para principiantes
22/100
Tipo de issue
Error
Claridad
Bastante claro
Estado de actividad
Activo
Stack tecnológico
kubernetes, postgresql, rust, sqlite

Línea de trabajo

Start by reading the related open issues #5859, #5894, #5896, and #5908, then trace actor event dispatch, envoy reconnection, and remote SQLite commit handling in the engine and runner. The report supplies reproduction steps and logs, but the cause and responsible component are unconfirmed. Done means reproducing the stuck actor state, identifying why recovery fails, and verifying affected actors become reachable after node loss.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Summary

Human: This is a real issue i have reproduced multiple time. Below the report made by claude opus on this problem.

With two engine nodes on Postgres and NATS, killing the engine node that holds the runners' (envoys') WebSocket connections while actors are busy leaves some actors unreachable for at least 5 minutes (up to about 15 minutes checked), until the runners restart. Requests return Actor not found; the runner logs received request for unknown actor; the engine still reports the actor as connectable. Neither POST /actors/{id}/sleep nor POST /actors/{id}/reschedule recovers it. Restarting the runner processes does.

It happened in 7 of 46 kills. The affected actors use @rivet-dev/pi (Pi Durable on the actor's SQLite). It did not happen in 30 kills with plain actors that only write to their SQLite.

Report written with the help of an AI coding assistant; the runs and logs below are real.

Versions

  • Engine: rivetdev/engine:slim-2.3.30
  • rivetkit 2.3.30 (envoy mode, RIVETKIT_RUNTIME_MODE default)
  • @rivet-dev/pi 0.5.3, @earendil-works/pi-durable 1.3.0
  • Node 24.22.1
  • PostgreSQL 18.6 (single instance)
  • NATS 2.15.1 (3-node cluster)

Environment

Local Kubernetes 1.36 (Docker Desktop, kind, 3 nodes, arm64). Engine config, all defaults otherwise:

{
  "postgres": { "url": "postgresql://<user>:<password>@postgres:5432/rivet?sslmode=disable" },
  "nats": { "addresses": ["nats-0:4222", "nats-1:4222", "nats-2:4222"], "username": "<user>", "password": "<password>" },
  "auth": { "admin_token": "<token>" },
  "telemetry": { "enabled": false }
}
  • Two engine replicas (Deployment, required pod anti-affinity), behind one ClusterIP Service.
  • Two runner pods (RIVET_ENDPOINT pointing at that Service, RIVET_NAMESPACE, RIVET_TOKEN a namespace JWT with runner: create), so each runner's WebSocket lands on either engine node.
  • One namespace, runner config {"datacenters":{"default":{"normal":{}}}}.

Steps to reproduce

  1. Create 100 actors (one Pi Durable agent each, crash_policy default sleep). From one client, send each actor 6 sequential prompt actions; each prompt runs one tool call, so each action commits several times to the actor's SQLite.
  2. While this load runs, find the engine pod holding the most runner WebSockets (from its envoy websocket connected / closed log lines) and delete it: kubectl delete pod <engine-pod> --force --grace-period=0.
  3. When the load ends, call a trivial action on each of the 100 actors. Repeat 5 minutes after the kill.

Expected

Actors interrupted by the lost node are rescheduled and answer within roughly the envoy lost threshold (15 s) plus start time, as most do.

Actual

In 7 of 46 runs, between 1 and 89 of the 100 actors answer Actor not found at the end of the load and still 5 minutes after the kill (the longest wait checked was about 15 minutes). For one of them, GET /actors?namespace=<ns>&name=agent&key=<key> returned:

"connectable_ts": <set, time of the kill>, "sleep_ts": null, "reschedule_ts": null, "destroy_ts": null, "crash_policy": "sleep"

POST /actors/{id}/sleep returned 200; the next call then timed out. POST /actors/{id}/reschedule returned 200; calls still returned Actor not found. Restarting the runner pods made every actor answer again, with its state intact.

Logs

One stuck actor (<actor-1>, key agent-52) from a run on 2026-10-24, UTC. Runner (the pod that hosted it):

01:11:14.895 error sqlite atomic commit failed actor_id=<actor-1> err="remote sqlite commit result is indeterminate after envoy disconnect"
01:11:29.899 warn  stopping all actors due to envoy lost threshold
01:11:29.899 error sqlite flush commit failed actor_id=<actor-1> err="envoy shut down"
01:11:29.900 warn  slow sqlite transaction database=<actor-1> duration_ms=15069
             warn  pi durable stopped after a failed commit, reopening it from storage actorKey=["agent-52"]
01:11:34.148 warn  actor framework action error response actor_id=<actor-1> generation=1 code=internal_error
             warn  pi durable busy check failed error="Database is closed. ..."
01:11:34.198 error graceful cleanup callback failed actor_id=<actor-1> error="Session failed after a storage error; close and reopen it"
01:11:34 .. 01:16:46  warn received request for unknown actor actor_id=<actor-1> actor_generation=None   (17 times)

Surviving engine node, same seconds:

01:11:33.644 info envoy websocket connected envoy_key=<envoy-A>
01:11:34.018 warn actor not allocated, ignoring events envoy_key=<envoy-A> events=[... EventActorStateUpdate { state: ActorStateRunning } ...]

The engine logged many actor not allocated, ignoring events lines after runners reconnected, including batches with ActorStateStopped. None of them names <actor-1>, so the logs do not show what happened to this actor's own events.

Frequency

Setup Engine node killed Runs Runs with actors stuck > 5 min
Pi Durable actors, runners connected to the engine Service the one holding most runner WebSockets 46 7
Same, runners behind a TCP proxy (socat) in their pod same 18 2
Same, runners direct a random engine pod 4 0
Same, no node lost: runner proxies frozen for 5 s and 10 s none 2 0
Plain actors (actor() with db(), one INSERT per action), runners direct the one holding most runner WebSockets 30 0

No acknowledged action was lost and no tool effect was applied twice in any run.

Workaround

Restart the runner processes. We now run a guard in the runner that restarts it when it keeps receiving requests for an actor it does not know; in 72 further kills, the 5 stuck cases recovered on their own in 43 to 58 s.

Possibly related (hypothesis)

  • #5859 (open): a failed actor event dispatch drops later events, including "stopped". The engine warnings above (events ignored, ActorStateStopped among them) point the same way.
  • #5894 (open): an existing actor stays unreachable after a runner restart.
  • #5896 (open): stale envoy routing survives after a pegboard_actor2 workflow dies with LeaseFenceMismatch; Sleep does not revive the actor there either. That report is single-node RocksDB and was seen once.
  • #5908 (open): a lost runner parks v2 actors (Sleep) regardless of crash_policy. Different symptom (the actor sleeps, it is not stuck as running), same area.
  • The failing cleanup is the actor's Pi Durable session, run by @rivet-dev/pi (published from rivet-dev/agents, packages/pi); the error text "Session failed after a storage error" comes from @earendil-works/pi-durable. Part of the problem may be there rather than in the engine.

The common factor in the stuck cases is an actor whose SQLite commit becomes indeterminate when the runner's connection to the dead engine node drops, and whose cleanup then fails. Plain actors did not reach that state in our runs. We have not confirmed this in the engine source.

Not tested

  • A reproduction without @rivet-dev/pi: the plain-actor variant above did not trigger it.
  • The file-system (single-node) driver.
  • Engine versions before 2.3.30, and the patch from #5859.
  • @earendil-works/pi-durable 1.4.0, released since; only 1.3.0 was used.
Lenguaje dominante
Rust
Estrellas
6.6k
Forks
304
Merge medio
18 h 5 min
PR fusionados (30 d)
31

Preparar el entorno

Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de rivet-dev/rivet

Todos los issues de rivet-dev/rivet

Issues similares

Más issues de Rust

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.