perf(reconciler): batch workloads per tick, drop redundant image inspect
Load every workload once per tick into a map instead of a per-container GetWorkloadByID (N+1) in the upsert loop plus a second ListWorkloads in the plugin pass: one query per tick, zero GetWorkloadByID. The ListWorkloads error path returns before the missing-sweep so a failed load can't flip live container rows to 'missing'. image.Reconcile is now a no-op: the generic upsert+markMissing pass already syncs every labeled container's state from the single ListAllForReconciler (docker ps -a) snapshot earlier in the same tick, so the former per-container IsContainerRunning loop was N redundant Docker calls/tick. (Its no-op body sits in image.go, which landed with the preceding commit; the tests are here.) compose/static reconcile do non-redundant work and are intentionally untouched. Reviewed: go APPROVE.
This commit is contained in:
@@ -257,6 +257,138 @@ func TestReconcileSkipsProjectInsertWithoutDeployerRow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestReconcileBatchingPreservesBehavior locks Fix A: loading all workloads
|
||||
// once per tick (and resolving labels from that in-memory map instead of an
|
||||
// N+1 GetWorkloadByID) must produce the same outcome as the per-container
|
||||
// lookup did. With multiple containers across multiple workloads plus a forged
|
||||
// label and a stale row, after one ReconcileOnce: known-workload containers
|
||||
// are upserted with the snapshot State, the forged-label container is skipped,
|
||||
// and the absent stale row is flipped to missing.
|
||||
func TestReconcileBatchingPreservesBehavior(t *testing.T) {
|
||||
st := newTestStore(t)
|
||||
|
||||
w1 := makeWorkload(t, st, "batch-a", "stack")
|
||||
w2 := makeWorkload(t, st, "batch-b", "stack")
|
||||
|
||||
// A stale row for w2 whose container is gone — must be marked missing.
|
||||
if err := st.UpsertContainer(store.Container{
|
||||
ID: w2.ID + ":old", WorkloadID: w2.ID, WorkloadKind: "stack",
|
||||
Role: "old", ContainerID: "docker-vanished", State: "running",
|
||||
}); err != nil {
|
||||
t.Fatalf("seed stale row: %v", err)
|
||||
}
|
||||
|
||||
fake := &fakeDocker{items: []docker.ReconcileItem{
|
||||
{
|
||||
ID: "docker-a1", Name: "batch-a-web-1", Image: "nginx:1.27", State: "running",
|
||||
Labels: map[string]string{
|
||||
docker.LabelManaged: "true",
|
||||
docker.LabelWorkloadID: w1.ID,
|
||||
docker.LabelWorkloadKind: "stack",
|
||||
docker.LabelRole: "web",
|
||||
},
|
||||
Ports: []uint16{8080},
|
||||
},
|
||||
{
|
||||
ID: "docker-b1", Name: "batch-b-api-1", Image: "redis:7", State: "exited",
|
||||
Labels: map[string]string{
|
||||
docker.LabelManaged: "true",
|
||||
docker.LabelWorkloadID: w2.ID,
|
||||
docker.LabelWorkloadKind: "stack",
|
||||
docker.LabelRole: "api",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Forged label — no such workload. Must be skipped entirely.
|
||||
ID: "docker-evil", Name: "evil", Image: "nginx", State: "running",
|
||||
Labels: map[string]string{
|
||||
docker.LabelManaged: "true",
|
||||
docker.LabelWorkloadID: "wl-forged",
|
||||
docker.LabelWorkloadKind: "stack",
|
||||
docker.LabelRole: "web",
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
r := New(st, fake, 0)
|
||||
if err := r.ReconcileOnce(context.Background()); err != nil {
|
||||
t.Fatalf("ReconcileOnce: %v", err)
|
||||
}
|
||||
|
||||
// w1: one row, bound to docker-a1, running.
|
||||
w1Rows, _ := st.ListContainersByWorkload(w1.ID)
|
||||
if len(w1Rows) != 1 {
|
||||
t.Fatalf("w1: expected 1 row, got %d", len(w1Rows))
|
||||
}
|
||||
if w1Rows[0].ContainerID != "docker-a1" || w1Rows[0].State != "running" || w1Rows[0].Role != "web" {
|
||||
t.Fatalf("w1 row wrong: %+v", w1Rows[0])
|
||||
}
|
||||
|
||||
// w2: the new api container is present (exited→stopped); the stale row is missing.
|
||||
api, _ := st.GetContainerByID(w2.ID + ":api")
|
||||
if api.ContainerID != "docker-b1" || api.State != "stopped" {
|
||||
t.Fatalf("w2 api row wrong: %+v", api)
|
||||
}
|
||||
old, _ := st.GetContainerByID(w2.ID + ":old")
|
||||
if old.State != "missing" {
|
||||
t.Fatalf("w2 stale row should be missing, got %q", old.State)
|
||||
}
|
||||
|
||||
// Forged label produced no row anywhere.
|
||||
all, _ := st.ListContainers(store.ContainerFilter{})
|
||||
for _, c := range all {
|
||||
if c.ContainerID == "docker-evil" {
|
||||
t.Fatalf("forged-label container was adopted: %+v", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestReconcileSyncsImageContainerState locks the Fix B coupling: the generic
|
||||
// reconciler upsert pass — NOT image.Reconcile — is what syncs an image
|
||||
// container's State from the snapshot. An image container carries the
|
||||
// workload_id / kind=image / role=image labels at create time, so a present
|
||||
// container's row gets its State written here, proving the per-container
|
||||
// inspect formerly in image.Reconcile is redundant.
|
||||
func TestReconcileSyncsImageContainerState(t *testing.T) {
|
||||
st := newTestStore(t)
|
||||
w := makeWorkload(t, st, "img", "image")
|
||||
|
||||
// Deployer pre-created the image container row (running). Docker now
|
||||
// reports it exited — the generic pass must sync it to stopped.
|
||||
if err := st.UpsertContainer(store.Container{
|
||||
ID: "img-deploy-uuid", WorkloadID: w.ID, WorkloadKind: "image",
|
||||
Role: "image", ContainerID: "docker-img", State: "running",
|
||||
}); err != nil {
|
||||
t.Fatalf("seed image row: %v", err)
|
||||
}
|
||||
|
||||
fake := &fakeDocker{items: []docker.ReconcileItem{{
|
||||
ID: "docker-img", Image: "ghcr.io/owner/app:v1", State: "exited",
|
||||
Labels: map[string]string{
|
||||
docker.LabelManaged: "true",
|
||||
docker.LabelWorkloadID: w.ID,
|
||||
docker.LabelWorkloadKind: "image",
|
||||
docker.LabelRole: "image",
|
||||
},
|
||||
Ports: []uint16{3000},
|
||||
}}}
|
||||
|
||||
// No plugin reconciler wired — proves the state sync comes from the
|
||||
// generic upsert pass, not from image.Reconcile.
|
||||
r := New(st, fake, 0)
|
||||
if err := r.ReconcileOnce(context.Background()); err != nil {
|
||||
t.Fatalf("ReconcileOnce: %v", err)
|
||||
}
|
||||
|
||||
got, _ := st.GetContainerByID("img-deploy-uuid")
|
||||
if got.State != "stopped" {
|
||||
t.Fatalf("image container state not synced by generic pass: got %q want stopped", got.State)
|
||||
}
|
||||
if got.Port != 3000 || got.ImageRef != "ghcr.io/owner/app:v1" {
|
||||
t.Fatalf("image container docker fields not synced: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileNormalizesState(t *testing.T) {
|
||||
st := newTestStore(t)
|
||||
w := makeWorkload(t, st, "norm", "stack")
|
||||
|
||||
Reference in New Issue
Block a user