Summary
stopBackendExact returns a stopped backend's gRPC port to freePorts synchronously, and startBackend pops it for the very next backend. A controller holding a cached NodeModel row for the old host:port can then pass probeHealth against a different backend now bound to that port, and dispatch a model load to it.
Why this is newly reachable
Before the reaping fix, backend.delete never actually stopped a process (it resolved to zero process keys and silently no-op'd), so a delete never recycled a port. Once deletes genuinely reap, they do.
The delete case is the worst one: unlike model.unload, the controller has no idea the address just became invalid, so nothing prompts it to re-probe.
Failure shape
- Backend A is running for model M on node N at port P; the controller has a
NodeModel row pointing at N:P.
- Backend A is deleted. Its process is now correctly reaped, and P returns to
freePorts.
- Backend B starts on node N and is assigned P.
- The controller routes a request for M, finds the cached row,
probeHealth succeeds — because something is listening on P — and dispatches to backend B.
The probe verifies liveness, not identity, so this is silent. The request goes to the wrong backend rather than failing.
Scope
Narrow: requires a delete immediately followed by an install on the same worker, with an in-flight route still holding the old address. It is also strictly better than the status quo it replaces, which was routing into a backend whose directory had been deleted out from under it (observed in production: a certifi path resolving into a removed directory).
But it converts a loud failure into a potentially silent misroute, which is worth closing deliberately rather than leaving implicit.
Options
- Quarantine freed ports for a grace period before returning them to
freePorts. Simple, narrows the window, does not eliminate it.
- Verify backend identity in
probeHealth rather than mere liveness. Fixes the class of bug rather than the window, and would also catch any other stale-address path. Larger change.
Option 2 is the more complete fix; option 1 is the cheaper one.
Related
- Reaping fix: deleting a backend previously orphaned its process (the change that makes this reachable).
- Deliberately deferred alternative:
BackendDeleteReply could return the stopped process keys so the controller drops matching NodeModel rows eagerly rather than waiting for the next probeHealth. That was skipped because it adds a message-schema change and cross-version compat surface, but it would also mitigate this.
Found while debugging a distributed LongCat-Video load failure on an ARM64 L4T worker.
Summary
stopBackendExactreturns a stopped backend's gRPC port tofreePortssynchronously, andstartBackendpops it for the very next backend. A controller holding a cachedNodeModelrow for the oldhost:portcan then passprobeHealthagainst a different backend now bound to that port, and dispatch a model load to it.Why this is newly reachable
Before the reaping fix,
backend.deletenever actually stopped a process (it resolved to zero process keys and silently no-op'd), so a delete never recycled a port. Once deletes genuinely reap, they do.The delete case is the worst one: unlike
model.unload, the controller has no idea the address just became invalid, so nothing prompts it to re-probe.Failure shape
NodeModelrow pointing atN:P.freePorts.probeHealthsucceeds — because something is listening on P — and dispatches to backend B.The probe verifies liveness, not identity, so this is silent. The request goes to the wrong backend rather than failing.
Scope
Narrow: requires a delete immediately followed by an install on the same worker, with an in-flight route still holding the old address. It is also strictly better than the status quo it replaces, which was routing into a backend whose directory had been deleted out from under it (observed in production: a certifi path resolving into a removed directory).
But it converts a loud failure into a potentially silent misroute, which is worth closing deliberately rather than leaving implicit.
Options
freePorts. Simple, narrows the window, does not eliminate it.probeHealthrather than mere liveness. Fixes the class of bug rather than the window, and would also catch any other stale-address path. Larger change.Option 2 is the more complete fix; option 1 is the cheaper one.
Related
BackendDeleteReplycould return the stopped process keys so the controller drops matchingNodeModelrows eagerly rather than waiting for the nextprobeHealth. That was skipped because it adds a message-schema change and cross-version compat surface, but it would also mitigate this.Found while debugging a distributed LongCat-Video load failure on an ARM64 L4T worker.