Actors stay unreachable ("Actor not found") after an engine node is lost, with the Postgres driver in multi-node mode
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 5/5
- 预计耗时
- 一周以上
- 新手友好度
- 22/100
- Issue 类型
- 缺陷
- 描述清晰度
- 基本清楚
- 活跃度
- 活跃
- 技术栈
- kubernetes, postgresql, rust, sqlite
调研方向
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.
由索引模型根据 Issue 内容生成。
描述
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 rivetkit2.3.30 (envoy mode,RIVETKIT_RUNTIME_MODEdefault)@rivet-dev/pi0.5.3,@earendil-works/pi-durable1.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_ENDPOINTpointing at that Service,RIVET_NAMESPACE,RIVET_TOKENa namespace JWT withrunner: create), so each runner's WebSocket lands on either engine node. - One namespace, runner config
{"datacenters":{"default":{"normal":{}}}}.
Steps to reproduce
- Create 100 actors (one Pi Durable agent each,
crash_policydefaultsleep). From one client, send each actor 6 sequentialpromptactions; each prompt runs one tool call, so each action commits several times to the actor's SQLite. - While this load runs, find the engine pod holding the most runner WebSockets (from its
envoy websocket connected/closedlog lines) and delete it:kubectl delete pod <engine-pod> --force --grace-period=0. - 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,
ActorStateStoppedamong 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_actor2workflow dies withLeaseFenceMismatch; 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-durable1.4.0, released since; only 1.3.0 was used.
- 主要语言
- Rust
- 星标
- 6.6k
- 派生
- 304
- 平均合并
- 19 小时 50 分钟
- 30 天内合并 PR
- 28
环境准备
这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
rivet-dev/rivet 的其他 Issue
-
难度 1/5 1-3 小时 新手友好度 66/100
rivet-dev/rivet#5971 · 1 条评论 ·
维护者通常 1 天内回复
-
workflow-engine: step timeout over ~24.8 days (or Infinity) fires after 1ms可能已有人在做 @poojagosika 于 4 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 84/100
rivet-dev/rivet#5970 · 1 条评论 ·
维护者通常 1 天内回复
-
rivetkit client: ActorConn debug log prints action arguments可能重新可做 关联的 PR 已关闭且未合并。 未关闭
难度 1/5 1 小时以内 新手友好度 80/100
rivet-dev/rivet#5955 · 4 条评论 ·
维护者通常 1 天内回复
-
effect: action on a missing actor instance fails as internal_error instead of actor.aborted可能已有人在做 @kunalgoesbyken 于 17 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 82/100
rivet-dev/rivet#5891 · 1 条评论 ·
维护者通常 1 天内回复
-
Rust client drops connection-level errors when actionId is null (JSON/CBOR)可能已有人在做 @DibbayajyotiRoy 于 39 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 78/100
rivet-dev/rivet#5819 · 1 条评论 ·
维护者通常 1 天内回复
相似的 Issue
-
SPLADE `/embed_sparse` output depends on the other inputs in the batch (candle BertModel / DistilBertModel)可能已有人在做 @0z5a 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 72/100
huggingface/text-embeddings-inference#979 ·
维护者通常 1 天内回复
-
good first issue help wanted
难度 2/5 1-3 小时 新手友好度 78/100
servo/rust-smallvec#874 ·
-
难度 1/5 1 小时以内 新手友好度 94/100
维护者通常 1 天内回复
-
bug
难度 2/5 1-3 小时 新手友好度 65/100
维护者通常 2 天内回复
-
ci enhancement need-triage
难度 2/5 1-3 小时 新手友好度 86/100
维护者通常 1 天内回复