The agent has always logged a percentage as it fetches weights, with a comment saying a replica pulling a hundred gigabytes and one that is wedged look identical from the control plane. They still did: the log is on the node, and the node is not where anyone is looking when a replica has been starting for ten minutes. Progress now rides on the shard status and appears under the replica in `ctl replicas` — a line rather than a column, because most replicas are not downloading anything and a column would be empty for all of them. It is deliberately not a state transition. Treating each chunk as one would re-mark the shard as starting on every report and reset the timer that decides when a slow start has become a failed one, so an hour-long download would keep a doomed replica alive indefinitely. It arrives on its own method for that reason, and a real transition clears it — otherwise a finished download sits in the listing at ninety-nine percent under a shard that has been serving for an hour. Watchers are keyed by source rather than by shard, because a fetch is single-flight: eight shards starting together produce one download and all eight are waiting on it. No percentage is invented when the server sent no length. One that stops moving is worse than a byte count that keeps rising, so the field is absent and the listing says "so far". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
71 lines
2.5 KiB
Go
71 lines
2.5 KiB
Go
package reconciler
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/agent/supervisor"
|
|
)
|
|
|
|
func TestProgressIsVisibleWithoutBeingATransition(t *testing.T) {
|
|
// The failure this replaces: a replica pulling a hundred gigabytes and one
|
|
// that is wedged look identical from the control plane — both are a shard
|
|
// that has been starting for ten minutes.
|
|
//
|
|
// And progress must not count as a state change. Treating each chunk as
|
|
// one would reset the timer that decides when a slow start has become a
|
|
// failed one, so a download would keep a replica alive forever.
|
|
h := newHarness(t, testModel(1), gpuNode("n1", 8))
|
|
h.r.Reconcile(t.Context())
|
|
|
|
reps := h.r.Replicas()
|
|
if len(reps) != 1 || len(reps[0].Shards) == 0 {
|
|
t.Fatalf("expected a placed replica with shards, got %+v", reps)
|
|
}
|
|
rep, shard := reps[0].ID, reps[0].Shards[0].ID
|
|
|
|
h.r.OnFetchProgress(rep, shard, "hf:org/repo/model.gguf", 3<<30, 12<<30)
|
|
|
|
got := h.r.Replicas()[0].Shards[0]
|
|
if got.Fetch == nil {
|
|
t.Fatal("the download is not visible on the shard")
|
|
}
|
|
if got.Fetch.BytesDone != 3<<30 || got.Fetch.BytesTotal != 12<<30 {
|
|
t.Errorf("progress came back as %+v", got.Fetch)
|
|
}
|
|
if p, ok := got.Fetch.Percent(); !ok || p != 25 {
|
|
t.Errorf("percent = %d (known %v), want 25", p, ok)
|
|
}
|
|
}
|
|
|
|
func TestAnUnknownTotalHasNoPercentage(t *testing.T) {
|
|
// A server that sends no Content-Length is common. An invented percentage
|
|
// that stops moving is worse than a byte count that keeps rising.
|
|
f := FetchProgress{Source: "https://x/y.gguf", BytesDone: 500}
|
|
if _, ok := f.Percent(); ok {
|
|
t.Error("a percentage was invented from an unknown total")
|
|
}
|
|
}
|
|
|
|
func TestATransitionClearsTheDownload(t *testing.T) {
|
|
// Left in place it would sit in the listing at ninety-nine percent under
|
|
// a shard that has been serving for an hour.
|
|
h := newHarness(t, testModel(1), gpuNode("n1", 8))
|
|
h.r.Reconcile(t.Context())
|
|
reps := h.r.Replicas()
|
|
rep, shard := reps[0].ID, reps[0].Shards[0].ID
|
|
|
|
h.r.OnFetchProgress(rep, shard, "hf:org/repo/model.gguf", 1, 2)
|
|
h.r.OnShardStatus(rep, shard, supervisor.Ready, "")
|
|
|
|
if got := h.r.Replicas()[0].Shards[0]; got.Fetch != nil {
|
|
t.Errorf("a finished shard still shows a download: %+v", got.Fetch)
|
|
}
|
|
}
|
|
|
|
func TestProgressForAReplicaThatIsGoneIsIgnored(t *testing.T) {
|
|
// A download that finishes after its replica was torn down should not
|
|
// resurrect anything or panic.
|
|
h := newHarness(t, testModel(1), gpuNode("n1", 8))
|
|
h.r.OnFetchProgress("no-such-replica", "no-such-shard", "x", 1, 2)
|
|
}
|