On a multi-server swarm, work posted on server A and won by an LLM on server
B never reached DONE. It sat in RUNNING until the 60s lease expired, then
requeued and re-auctioned forever. B’s inference completed and the result was
persisted on B’s own LLM node — the failure was specific to getting that
result back to the work node on A. Logs on A showed every cross-server wake
for the work node being dropped:
1
2
A: dropping cross-server wake x2887
A: cross-server wake for ... x0
The same root cause also explained a second symptom: with the wake path dead,
claims only landed via the periodic sweep fallback (~40s to ASSIGNED)
instead of the ~5s the event path should give. A same-server control (poster
and winner both on the same host) completed normally.
PeerObservationClient.resolveSource(hostId, identity) resolves the node
behind a SOURCE_TRIGGERED wake. hostId is the peer whose SSE stream the
wake arrived on; identity is a self-describing NodeIdentity(nodeId,
hostId) naming the node’s actual owner. For a cross-host swarm result these
are different hosts: B publishes the wake (so it arrives over B’s stream),
but the wake’s triggeringSource identity carries A’s hostId, because the
work node lives on A.
resolveSource ignored identity.hostId and looked the node up on the
SSE-source peer instead:
1
2
val peer = nodeManager.readNode(hostId) ?: return null
val fetched = runCatching { nodeHttp.readNode(peer, identity.nodeId) }.getOrNull() ?: return null
So A asked B (nodeManager.readNode(hostId) resolves B’s peer record, then
nodeHttp.readNode fetches from B) for a node that only exists on A. The HTTP
call 404’d, the fetch returned null, and the wake was dropped — every time,
for every cross-host swarm job.
resolveSource now branches on identity.hostId, not the SSE-source
hostId: if identity.hostId is our own install id, the node is read
locally (nodeManager.readNode(identity.nodeId)); otherwise the peer to
fetch from is resolved via identity.hostId, the node’s actual owner. The
hostId SSE-source parameter is kept only for logging.
installId() reads a real file (/etc/krill/install_id or
~/.krill/install_id), so PeerObservationClient picked up the same
seam already used elsewhere in the swarm code (ServerSwarmWorkProcessor,
BeaconSettingsTask): a selfId: () -> String = installId constructor
parameter, defaulted to the real platform value in production and injectable
in tests. server/src/jvmTest/kotlin/krill/zone/server/events/PeerObservationClientTest.kt
gained a case that publishes a SOURCE_TRIGGERED over a peer’s SSE stream for
a node whose identity.hostId is the local host, and asserts the node is
read locally and the peer’s /node endpoint is never called.
NodeIdentity carries its own ownership; do not substitute the
transport-layer peer for it. Any code resolving a node “from” some
connection (SSE stream, webhook source, HTTP client) should resolve against
the identity’s own hostId field, not the peer the message happened to
arrive from. The two coincide for same-host wiring, which is why this went
unnoticed until a genuinely cross-host job exercised the divergent case.PeerObservationClientTest cases all
used a single REMOTE_HOST for both the SSE-source and the mirrored node’s
owner, so the bug’s exact shape — SSE-source host != identity’s owning host
— had no test exercising it. The regression test isolates that divergence
explicitly.