Skip to content

ateapi: reach an atelet again after its node restarts - #2118

Merged
Luiz Oliveira (laoj2) merged 2 commits into
agent-substrate:mainfrom
ericcurtin:fix-atelet-dial-stale-ip
Oct 2, 2026
Merged

Luiz Oliveira (laoj2) merged 2 commits into
agent-substrate:mainfrom
ericcurtin:fix-atelet-dial-stale-ip

Conversation

@ericcurtin

Copy link
Copy Markdown
Contributor

Fixes #2116

Redial when an atelet pod keeps its UID but gets a new IP.

  • Tests pass
  • Appropriate changes to documentation are included in the PR

A node restart keeps the atelet pod UID but changes its IP, and the conn cache kept the old one.
@eren23

Copy link
Copy Markdown
Contributor

Tested this PR (86ffa4dd) against its base (6a35150) on a real node restart. It fixes #2116.

Setup: kind with 1 control-plane node and 2 worker nodes, gVisor sandbox class. The services ran on the control plane. Both workers of the pool ran on one worker node (the "victim"). I used kubectl ate only.

Each round:

  1. Create 3 actors from one template (DATA snapshot scope, one durable dir). Resume and suspend two of them, and leave the third running. ate-api-server now has a cached connection to the victim's atelet.
  2. Restart the victim node (docker stop, docker start). Check that the atelet pod has the same UID and a new IP. If the IP was reused, restart again.
  3. delete --any-state the running actor. Recreate the worker pods, so that stale worker IPs ([Bug]: Actor's worker IP is not revalidated after a node reboot — stays RUNNING, unreachable #1837) cannot cause a failure. Do not touch the atelet pod.
  4. Resume the two suspended actors.
Build Rounds Resumes after the restart Failures
base 6a35150 2 0 of 4 All 4 dialed the old atelet IP after the new IP was live (dial tcp <old>:8085: i/o timeout). The actors went to ACTOR_STATE_CRASHED.
this PR 86ffa4dd 8 14 of 16 One connect: connection refused on the new IP, a few seconds after the restart. One more Unavailable connection error, but I did not keep the IP that it dialed.

In one round on this PR, I compared timestamps. The last call to the old IP was 32 s before the new atelet container started. After that, the only log lines with the old IP were ListActors responses that carry an actor's stored error from the downtime.

Notes:

  • The connection refused failure looks like a separate race: the new IP is visible before atelet listens on it. That resume set the actor to ACTOR_STATE_CRASHED. kubectl ate revert actor and a new resume brought it back. Treating Unavailable during the restore step as retryable, instead of crashing the actor, would remove this.
  • delete --any-state worked on both builds, so on current main only the restore path showed the bug.
  • run-tests fails only on errcheck: value.(*ateletConn).conn.Close() in the eviction function (dialer.go:109).

func newAteletConnCache(size int) *lru.Cache {
return lru.NewWithEvictionFunc(size, func(_ lru.Key, value interface{}) {
value.(*grpc.ClientConn).Close()
value.(*ateletConn).conn.Close()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linter is failing with

Error: cmd/ateapi/internal/controlapi/dialer.go:109:33: Error return value of `.Close` is not checked (errcheck)
		value.(*ateletConn).conn.Close()
		                              ^
1 issues:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 086ccfc: now _ = ...Close().

ateletKey := string(selectedAtelet.ObjectMeta.UID)
ateletIP := selectedAtelet.Status.PodIPs[0].IP

if cached, ok := d.ateletConns.Get(ateletKey); ok {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a race condition now when we call DialForAteletOnNode for many actors, because Get and Remove are not under the same lock.

For example, let's say that A and B are actors

Node just restarted, then:

  • A and B both Get(ateletKey) a stale entry.
  • A calls Remove(ateletKey), which closes the stale connection.
  • A dials connA, calls Add(ateletKey, connA), and returns connA.
  • B calls Remove(ateletKey). That removes connA, and the eviction function closes it.

B just removed the good connection.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed in 086ccfc: a mutex now covers the lookup, evict and insert. Added a concurrent test that fails without it.

@laoj2
Luiz Oliveira (laoj2) added this pull request to the merge queue Oct 2, 2026
Merged via the queue into agent-substrate:main with commit edaa0f7 Oct 2, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/api-machinery area/reliability kind/bug Something isn't working / bugfixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

After a node restart, ate-api-server keeps dialing the old atelet IP until it restarts

4 participants