CI / test (push) Successful in 1m42s
A node's registry holds an adapter for every engine whether or not its binary exists, the agent never said which were installed, and placement carried an Engine field it had never once read. So a vLLM replica was as happily placed on a node with no vLLM: the node registered healthy, the shard failed minutes later with "executable file not found" in the engine's own words, and the reconciler tried again. That is how fifteen gigabytes of safetensors reached this cluster's mirror before anybody noticed it had no card and no vLLM to run them with. Agents now probe what is installed and report it — the protocol has carried engine_versions since it was written and nothing ever filled it in. Placement filters on the key set, `ctl nodes` has a column, and the mirror page's serve warning is a fact instead of a guess about cards, which would have waved through a fleet of eight-GPU nodes that never had vLLM. Probing resolves binaries and runs none of them. The first version asked each engine for --version, which is fine for an engine and not fine for the wrapper scripts people put in front of one: a script that shifts past flags it does not recognise and then execs the server will start an inference server, at agent startup, on every node. The agent's own test suite installs exactly such a script and caught it. So presence is a path lookup, and the version is left to `ctl engine-check`, which is run by somebody who meant to run the engine. SGLang is the exception and has to execute, because a module has no path to look up — but the command is llmgw's own one-line import rather than the engine's launcher. Its exit code is the answer: python3 is on almost every machine, and treating a failed import as "installed" reported SGLang on every node in a fleet that had none, which is the failure being fixed, inverted. Probed once, not per reconnect, and bounded by the context that registration waits on: an agent that cannot reach the control plane is worse than one reporting an incomplete list. A node that reports no engines at all is not excluded anywhere — by placement, or by the page. That is what every agent older than this reports, and emptying a fleet on a control plane upgrade is a worse failure than the one being fixed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
108 lines
3.5 KiB
Go
108 lines
3.5 KiB
Go
package engines
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// script writes an executable that stands in for an engine.
|
|
func script(t *testing.T, name, body string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, name)
|
|
if err := os.WriteFile(p, []byte("#!/bin/sh\n"+body+"\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func TestAnEngineThatIsNotInstalledIsAbsentFromTheReport(t *testing.T) {
|
|
r := NewRegistry(Config{})
|
|
r.engines = map[string]Engine{
|
|
"vllm": &VLLM{cfg: VLLMConfig{Bin: "/nonexistent/vllm"}},
|
|
}
|
|
// Absent, not present-with-unknown-version. Placement reads the keys, and
|
|
// a node listed for an engine it cannot run is the failure being fixed.
|
|
if got := r.Probe(t.Context()); len(got) != 0 {
|
|
t.Errorf("expected nothing, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestProbingNeverRunsTheEngine(t *testing.T) {
|
|
// A wrapper that shifts past flags it does not recognise and then starts
|
|
// the server. People put these in front of engines, and the agent's own
|
|
// tests install one. Probing with --version would launch an inference
|
|
// server at agent startup, on every node.
|
|
marker := filepath.Join(t.TempDir(), "ran")
|
|
bin := script(t, "fake-vllm", "touch "+marker+"; sleep 60")
|
|
|
|
r := NewRegistry(Config{})
|
|
r.engines = map[string]Engine{"vllm": &VLLM{cfg: VLLMConfig{Bin: bin}}}
|
|
|
|
got := r.Probe(t.Context())
|
|
if _, ok := got["vllm"]; !ok {
|
|
t.Error("a binary that is there should be reported present")
|
|
}
|
|
if _, err := os.Stat(marker); err == nil {
|
|
t.Fatal("the engine was executed by a probe")
|
|
}
|
|
}
|
|
|
|
func TestAModuleThatWillNotImportIsNotInstalled(t *testing.T) {
|
|
// python3 is on almost every machine, so the interpreter being there says
|
|
// nothing about SGLang. Reporting it present put every node in a fleet
|
|
// forward for an engine none of them had.
|
|
bin := script(t, "python3", "exit 1")
|
|
r := NewRegistry(Config{})
|
|
r.engines = map[string]Engine{"sglang": &SGLang{cfg: SGLangConfig{Python: bin}}}
|
|
|
|
if got := r.Probe(t.Context()); len(got) != 0 {
|
|
t.Errorf("a failed import means not installed, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestAModuleThatImportsIsReportedWithItsVersion(t *testing.T) {
|
|
bin := script(t, "python3", `echo "0.4.1"`)
|
|
r := NewRegistry(Config{})
|
|
r.engines = map[string]Engine{"sglang": &SGLang{cfg: SGLangConfig{Python: bin}}}
|
|
|
|
got := r.Probe(t.Context())
|
|
if got["sglang"] != "0.4.1" {
|
|
t.Errorf("expected the version, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestAVersionIsPickedOutOfWhateverIsPrinted(t *testing.T) {
|
|
// Only the module probe reaches this now, and a Python package version is
|
|
// dotted. A bare build number is deliberately not matched: it would turn a
|
|
// stray number in a line of prose into a version.
|
|
for in, want := range map[string]string{
|
|
"0.4.1": "0.4.1",
|
|
"sglang 0.4.post1": "0.4.post1",
|
|
"warning: noise\n0.4.1": "0.4.1",
|
|
"": Unknown,
|
|
"no numbers here": Unknown,
|
|
"just 4123": Unknown,
|
|
} {
|
|
if got := firstVersion(in); got != want {
|
|
t.Errorf("firstVersion(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProbingIsBoundedByTheContext(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
bin := script(t, "python3", "sleep 60")
|
|
r := NewRegistry(Config{})
|
|
r.engines = map[string]Engine{"sglang": &SGLang{cfg: SGLangConfig{Python: bin}}}
|
|
|
|
// Registration waits on this. An agent that cannot reach the control plane
|
|
// is worse than one reporting an incomplete list.
|
|
if got := r.Probe(ctx); len(got) != 0 {
|
|
t.Errorf("a cancelled probe should report nothing, got %v", got)
|
|
}
|
|
}
|