From 213c47ed1ebcdb81bd6be694aaebbc329eb21bb8 Mon Sep 17 00:00:00 2001 From: zhanghao Date: Sun, 25 Oct 2026 23:13:04 +0800 Subject: [PATCH 1/4] fix(inferences): stop showing a healthy scaler as failing The scaling step treated every LLMScaler condition with status False as a failure. ScalingDeferred is inverted: llm-operator sets it True while a rollout or scale-down settle guard holds a step back, and False otherwise (NotDeferred, or DeferralTimedOut after a forced write). Every healthy scaler therefore showed "NotDeferred" in red. - ScalingDeferred=False no longer counts as failing - ScalingDeferred=True shows as waiting, with its reason - any other condition at False (DecisionAvailable) still fails the step Fixes #100 --- web/src/modules/inferences/lib.test.ts | 26 +++++++++++++++++++ web/src/modules/inferences/lib.ts | 6 ++++- web/src/modules/inferences/locales/en-US.json | 1 + web/src/modules/inferences/locales/zh-CN.json | 1 + 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/web/src/modules/inferences/lib.test.ts b/web/src/modules/inferences/lib.test.ts index e8dcb89..13d1666 100644 --- a/web/src/modules/inferences/lib.test.ts +++ b/web/src/modules/inferences/lib.test.ts @@ -191,6 +191,32 @@ describe("installSteps", () => { it("calls a deleted ModelRoute bad", () => { expect(installSteps(status(), route({ missing: true }) as never, {})[2]!.state).toBe("bad"); }); + + const scaler = (conditions: { type: string; status: string; reason?: string }[], cur = 1, want = 1) => ({ + result: { ref: { apiVersion: "v1", kind: "LLMScaler", name: "r" } } as ObjectResult, + status: { currentReplicas: cur, desiredReplicas: want, conditions }, + }); + it("reads ScalingDeferred=False as the normal state, not a failure", () => { + const steps = installSteps(status(), {}, scaler([ + { type: "DecisionAvailable", status: "True", reason: "DecisionReceived" }, + { type: "ScalingDeferred", status: "False", reason: "NotDeferred" }, + ]) as never); + expect(steps[3]).toEqual({ key: "scaler", state: "ok", detail: "steady", params: { n: 1 } }); + }); + it("shows a deferred scale step as waiting, with its reason", () => { + const steps = installSteps(status(), {}, scaler([ + { type: "DecisionAvailable", status: "True", reason: "DecisionReceived" }, + { type: "ScalingDeferred", status: "True", reason: "RolloutInProgress" }, + ], 1, 2) as never); + expect(steps[3]).toEqual({ key: "scaler", state: "wait", detail: "deferred", params: { reason: "RolloutInProgress" } }); + }); + it("still calls a missing decision bad", () => { + const steps = installSteps(status(), {}, scaler([ + { type: "DecisionAvailable", status: "False", reason: "DecisionUnavailable" }, + { type: "ScalingDeferred", status: "False", reason: "NotDeferred" }, + ]) as never); + expect(steps[3]).toEqual({ key: "scaler", state: "bad", detail: "failing", params: { reason: "DecisionUnavailable" } }); + }); }); describe("age", () => { diff --git a/web/src/modules/inferences/lib.ts b/web/src/modules/inferences/lib.ts index a123e95..483c689 100644 --- a/web/src/modules/inferences/lib.ts +++ b/web/src/modules/inferences/lib.ts @@ -164,8 +164,12 @@ function scalerStep({ result, status }: Picked): if (!result) return { key, state: "off", detail: "none" }; if (result.error) return { key, state: "wait", detail: "unreadable" }; if (result.missing) return { key, state: "bad", detail: "deleted" }; - const failing = status?.conditions?.find((c) => c.status === "False"); + // ScalingDeferred is inverted: False is the normal state, True a guard holding a step back. + const conditions = status?.conditions ?? []; + const failing = conditions.find((c) => c.type !== "ScalingDeferred" && c.status === "False"); if (failing) return { key, state: "bad", detail: "failing", params: { reason: failing.reason || failing.type } }; + const deferred = conditions.find((c) => c.type === "ScalingDeferred" && c.status === "True"); + if (deferred) return { key, state: "wait", detail: "deferred", params: { reason: deferred.reason || deferred.type } }; const cur = status?.currentReplicas; const want = status?.desiredReplicas; if (cur === undefined) return { key, state: "wait", detail: "noDecision" }; diff --git a/web/src/modules/inferences/locales/en-US.json b/web/src/modules/inferences/locales/en-US.json index 6cac5b3..1fa75a6 100644 --- a/web/src/modules/inferences/locales/en-US.json +++ b/web/src/modules/inferences/locales/en-US.json @@ -197,6 +197,7 @@ "unreadable": "Unreadable", "deleted": "LLMScaler deleted", "failing": "{reason}", + "deferred": "Deferred: {reason}", "noDecision": "No decision yet", "scaling": "{cur} → {want} replicas", "steady": "{n} replicas, steady" diff --git a/web/src/modules/inferences/locales/zh-CN.json b/web/src/modules/inferences/locales/zh-CN.json index 70d5854..2854a7a 100644 --- a/web/src/modules/inferences/locales/zh-CN.json +++ b/web/src/modules/inferences/locales/zh-CN.json @@ -186,6 +186,7 @@ "unreadable": "读取失败", "deleted": "LLMScaler 已被删除", "failing": "{reason}", + "deferred": "伸缩已推迟:{reason}", "noDecision": "还没有伸缩决策", "scaling": "{cur} → {want} 副本", "steady": "{n} 副本,稳定" From b01111ae6d5adac02d1ab8d4446d6d13d195cd81 Mon Sep 17 00:00:00 2001 From: huskcass <333684917+huskcass@users.noreply.github.com> Date: Mon, 26 Oct 2026 01:27:53 -0700 Subject: [PATCH 2/4] fix(inferences): read LLMScaler health from AbleToScale and ScalingActive llm-operator now reports LLMScaler status the way HPA does. The scaling step still failed on any condition at False, so a healthy scaler with ScalingLimited=False (DesiredWithinRange) showed as failing. - only AbleToScale=False or ScalingActive=False fails the step, with the condition's reason and message - AbleToScale=True with RolloutInProgress, ScaleDownSettling or ScaleDownStabilized shows as holding; Synced and DeferralTimedOut are a normal scale - ScalingLimited and any other condition never fail the step - older operators: DecisionAvailable stands in when ScalingActive is absent, ScalingDeferred=True holds when AbleToScale is absent; a stale old condition next to the new one is ignored Co-authored-by: aceforeverd <11715200+aceforeverd@users.noreply.github.com> --- web/src/modules/inferences/lib.test.ts | 93 ++++++++++++++++--- web/src/modules/inferences/lib.ts | 29 +++++- web/src/modules/inferences/locales/en-US.json | 3 +- web/src/modules/inferences/locales/zh-CN.json | 3 +- 4 files changed, 108 insertions(+), 20 deletions(-) diff --git a/web/src/modules/inferences/lib.test.ts b/web/src/modules/inferences/lib.test.ts index 13d1666..37cb667 100644 --- a/web/src/modules/inferences/lib.test.ts +++ b/web/src/modules/inferences/lib.test.ts @@ -192,30 +192,97 @@ describe("installSteps", () => { expect(installSteps(status(), route({ missing: true }) as never, {})[2]!.state).toBe("bad"); }); - const scaler = (conditions: { type: string; status: string; reason?: string }[], cur = 1, want = 1) => ({ + const scaler = (conditions: { type: string; status: string; reason?: string; message?: string }[], cur = 1, want = 1) => ({ result: { ref: { apiVersion: "v1", kind: "LLMScaler", name: "r" } } as ObjectResult, status: { currentReplicas: cur, desiredReplicas: want, conditions }, }); - it("reads ScalingDeferred=False as the normal state, not a failure", () => { - const steps = installSteps(status(), {}, scaler([ + const step = (conditions: { type: string; status: string; reason?: string; message?: string }[], cur = 1, want = 1) => + installSteps(status(), {}, scaler(conditions, cur, want) as never)[3]; + + it("reads a healthy scaler as steady, including ScalingLimited=False", () => { + expect(step([ + { type: "ScalingActive", status: "True", reason: "DecisionReceived" }, + { type: "AbleToScale", status: "True", reason: "Synced" }, + { type: "ScalingLimited", status: "False", reason: "DesiredWithinRange" }, + ])).toEqual({ key: "scaler", state: "ok", detail: "steady", params: { n: 1 } }); + }); + it("calls AbleToScale=False a failure and shows its message", () => { + expect(step([ + { type: "ScalingActive", status: "True", reason: "DecisionReceived" }, + { type: "AbleToScale", status: "False", reason: "TargetNotFound", message: "deployment demo is gone" }, + ])).toEqual({ key: "scaler", state: "bad", detail: "failingMessage", params: { reason: "TargetNotFound", message: "deployment demo is gone" } }); + }); + it("calls ScalingActive=False a failure", () => { + expect(step([ + { type: "ScalingActive", status: "False", reason: "DecisionUnavailable" }, + { type: "AbleToScale", status: "True", reason: "Synced" }, + ])).toEqual({ key: "scaler", state: "bad", detail: "failing", params: { reason: "DecisionUnavailable" } }); + }); + it("prefers AbleToScale when it and ScalingActive are both False", () => { + expect(step([ + { type: "ScalingActive", status: "False", reason: "InvalidSpec", message: "bad query" }, + { type: "AbleToScale", status: "False", reason: "TargetWriteFailed", message: "apiserver rejected the scale" }, + ])).toEqual({ key: "scaler", state: "bad", detail: "failingMessage", params: { reason: "TargetWriteFailed", message: "apiserver rejected the scale" } }); + }); + it("uses the condition type when a failure gives no reason", () => { + expect(step([{ type: "AbleToScale", status: "False" }])).toEqual({ key: "scaler", state: "bad", detail: "failing", params: { reason: "AbleToScale" } }); + }); + it("does not treat ScalingLimited as a failure", () => { + expect(step([ + { type: "ScalingLimited", status: "False", reason: "DesiredWithinRange" }, + { type: "SomeOther", status: "False", reason: "Whatever" }, + ])).toEqual({ key: "scaler", state: "ok", detail: "steady", params: { n: 1 } }); + expect(step([ + { type: "ScalingActive", status: "True", reason: "MetricsAvailable" }, + { type: "AbleToScale", status: "True", reason: "Synced" }, + { type: "ScalingLimited", status: "True", reason: "TooManyReplicas" }, + ])).toEqual({ key: "scaler", state: "ok", detail: "steady", params: { n: 1 } }); + }); + it("holds while a rollout or scale-down guard is in effect", () => { + for (const reason of ["RolloutInProgress", "ScaleDownSettling", "ScaleDownStabilized"]) { + expect(step([ + { type: "ScalingActive", status: "True", reason: "MetricsAvailable" }, + { type: "AbleToScale", status: "True", reason }, + ], 3, 1)).toEqual({ key: "scaler", state: "wait", detail: "deferred", params: { reason } }); + } + }); + it("treats DeferralTimedOut as a normal scale", () => { + expect(step([ + { type: "ScalingActive", status: "True", reason: "DecisionReceived" }, + { type: "AbleToScale", status: "True", reason: "DeferralTimedOut" }, + ], 1, 2)).toEqual({ key: "scaler", state: "wait", detail: "scaling", params: { cur: 1, want: 2 } }); + }); + it("reads an old scaler with ScalingDeferred=False as steady", () => { + expect(step([ { type: "DecisionAvailable", status: "True", reason: "DecisionReceived" }, { type: "ScalingDeferred", status: "False", reason: "NotDeferred" }, - ]) as never); - expect(steps[3]).toEqual({ key: "scaler", state: "ok", detail: "steady", params: { n: 1 } }); + ])).toEqual({ key: "scaler", state: "ok", detail: "steady", params: { n: 1 } }); }); - it("shows a deferred scale step as waiting, with its reason", () => { - const steps = installSteps(status(), {}, scaler([ + it("shows an old ScalingDeferred=True as waiting, with its reason", () => { + expect(step([ { type: "DecisionAvailable", status: "True", reason: "DecisionReceived" }, { type: "ScalingDeferred", status: "True", reason: "RolloutInProgress" }, - ], 1, 2) as never); - expect(steps[3]).toEqual({ key: "scaler", state: "wait", detail: "deferred", params: { reason: "RolloutInProgress" } }); + ], 1, 2)).toEqual({ key: "scaler", state: "wait", detail: "deferred", params: { reason: "RolloutInProgress" } }); }); - it("still calls a missing decision bad", () => { - const steps = installSteps(status(), {}, scaler([ + it("calls an old DecisionAvailable=False a failure", () => { + expect(step([ { type: "DecisionAvailable", status: "False", reason: "DecisionUnavailable" }, { type: "ScalingDeferred", status: "False", reason: "NotDeferred" }, - ]) as never); - expect(steps[3]).toEqual({ key: "scaler", state: "bad", detail: "failing", params: { reason: "DecisionUnavailable" } }); + ])).toEqual({ key: "scaler", state: "bad", detail: "failing", params: { reason: "DecisionUnavailable" } }); + }); + it("ignores a stale ScalingDeferred when AbleToScale is present", () => { + expect(step([ + { type: "ScalingActive", status: "True", reason: "DecisionReceived" }, + { type: "AbleToScale", status: "True", reason: "Synced" }, + { type: "ScalingDeferred", status: "True", reason: "RolloutInProgress" }, + ], 1, 2)).toEqual({ key: "scaler", state: "wait", detail: "scaling", params: { cur: 1, want: 2 } }); + }); + it("ignores a stale DecisionAvailable when ScalingActive is present", () => { + expect(step([ + { type: "ScalingActive", status: "True", reason: "MetricsAvailable" }, + { type: "DecisionAvailable", status: "False", reason: "DecisionUnavailable" }, + { type: "AbleToScale", status: "True", reason: "Synced" }, + ])).toEqual({ key: "scaler", state: "ok", detail: "steady", params: { n: 1 } }); }); }); diff --git a/web/src/modules/inferences/lib.ts b/web/src/modules/inferences/lib.ts index 483c689..0845c1f 100644 --- a/web/src/modules/inferences/lib.ts +++ b/web/src/modules/inferences/lib.ts @@ -159,17 +159,36 @@ function routeStep({ result, spec, status }: Picked): Step { const key = "scaler"; if (!result) return { key, state: "off", detail: "none" }; if (result.error) return { key, state: "wait", detail: "unreadable" }; if (result.missing) return { key, state: "bad", detail: "deleted" }; - // ScalingDeferred is inverted: False is the normal state, True a guard holding a step back. + // AbleToScale replaced ScalingDeferred (True meant held back) and ScalingActive replaced + // DecisionAvailable. The controller leaves a stale condition of the old type in place. const conditions = status?.conditions ?? []; - const failing = conditions.find((c) => c.type !== "ScalingDeferred" && c.status === "False"); - if (failing) return { key, state: "bad", detail: "failing", params: { reason: failing.reason || failing.type } }; - const deferred = conditions.find((c) => c.type === "ScalingDeferred" && c.status === "True"); - if (deferred) return { key, state: "wait", detail: "deferred", params: { reason: deferred.reason || deferred.type } }; + const ofType = (type: string) => conditions.find((c) => c.type === type); + const active = ofType("ScalingActive") ?? ofType("DecisionAvailable"); + const able = ofType("AbleToScale"); + + const failing = able?.status === "False" ? able : active?.status === "False" ? active : undefined; + if (failing) { + const reason = failing.reason || failing.type; + if (failing.message) return { key, state: "bad", detail: "failingMessage", params: { reason, message: failing.message } }; + return { key, state: "bad", detail: "failing", params: { reason } }; + } + + if (able?.status === "True" && able.reason && SCALE_HOLD_REASONS.has(able.reason)) { + return { key, state: "wait", detail: "deferred", params: { reason: able.reason } }; + } + if (!able) { + const deferred = ofType("ScalingDeferred"); + if (deferred?.status === "True") return { key, state: "wait", detail: "deferred", params: { reason: deferred.reason || deferred.type } }; + } + const cur = status?.currentReplicas; const want = status?.desiredReplicas; if (cur === undefined) return { key, state: "wait", detail: "noDecision" }; diff --git a/web/src/modules/inferences/locales/en-US.json b/web/src/modules/inferences/locales/en-US.json index 1fa75a6..30bc09c 100644 --- a/web/src/modules/inferences/locales/en-US.json +++ b/web/src/modules/inferences/locales/en-US.json @@ -197,7 +197,8 @@ "unreadable": "Unreadable", "deleted": "LLMScaler deleted", "failing": "{reason}", - "deferred": "Deferred: {reason}", + "failingMessage": "{reason}: {message}", + "deferred": "Holding: {reason}", "noDecision": "No decision yet", "scaling": "{cur} → {want} replicas", "steady": "{n} replicas, steady" diff --git a/web/src/modules/inferences/locales/zh-CN.json b/web/src/modules/inferences/locales/zh-CN.json index 2854a7a..7b85367 100644 --- a/web/src/modules/inferences/locales/zh-CN.json +++ b/web/src/modules/inferences/locales/zh-CN.json @@ -186,7 +186,8 @@ "unreadable": "读取失败", "deleted": "LLMScaler 已被删除", "failing": "{reason}", - "deferred": "伸缩已推迟:{reason}", + "failingMessage": "{reason}:{message}", + "deferred": "伸缩暂缓:{reason}", "noDecision": "还没有伸缩决策", "scaling": "{cur} → {want} 副本", "steady": "{n} 副本,稳定" From 4a554e9b9c49f30826566eed5b739ea20f1c764f Mon Sep 17 00:00:00 2001 From: huskcass <333684917+huskcass@users.noreply.github.com> Date: Mon, 26 Oct 2026 01:33:11 -0700 Subject: [PATCH 3/4] test(inferences): cover more LLMScaler condition fallbacks - AbleToScale=False with a message and no ScalingActive - an old object with only ScalingDeferred=True - AbleToScale=False next to a stale ScalingDeferred=True Also say in the comment that DecisionAvailable is still written as a deprecated mirror of ScalingActive, not left behind like ScalingDeferred. Co-authored-by: aceforeverd <11715200+aceforeverd@users.noreply.github.com> --- web/src/modules/inferences/lib.test.ts | 16 ++++++++++++++++ web/src/modules/inferences/lib.ts | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/web/src/modules/inferences/lib.test.ts b/web/src/modules/inferences/lib.test.ts index 37cb667..49e2e8a 100644 --- a/web/src/modules/inferences/lib.test.ts +++ b/web/src/modules/inferences/lib.test.ts @@ -270,6 +270,22 @@ describe("installSteps", () => { { type: "ScalingDeferred", status: "False", reason: "NotDeferred" }, ])).toEqual({ key: "scaler", state: "bad", detail: "failing", params: { reason: "DecisionUnavailable" } }); }); + it("fails on AbleToScale=False with its message when ScalingActive is missing", () => { + expect(step([ + { type: "AbleToScale", status: "False", reason: "TargetUnreadable", message: "cannot read deployment demo" }, + ])).toEqual({ key: "scaler", state: "bad", detail: "failingMessage", params: { reason: "TargetUnreadable", message: "cannot read deployment demo" } }); + }); + it("shows an old object with only ScalingDeferred=True as waiting", () => { + expect(step([{ type: "ScalingDeferred", status: "True", reason: "ScaleDownSettling" }], 3, 1)) + .toEqual({ key: "scaler", state: "wait", detail: "deferred", params: { reason: "ScaleDownSettling" } }); + }); + it("fails on AbleToScale=False and ignores a stale ScalingDeferred=True", () => { + expect(step([ + { type: "ScalingActive", status: "True", reason: "DecisionReceived" }, + { type: "AbleToScale", status: "False", reason: "TargetNotFound" }, + { type: "ScalingDeferred", status: "True", reason: "RolloutInProgress" }, + ])).toEqual({ key: "scaler", state: "bad", detail: "failing", params: { reason: "TargetNotFound" } }); + }); it("ignores a stale ScalingDeferred when AbleToScale is present", () => { expect(step([ { type: "ScalingActive", status: "True", reason: "DecisionReceived" }, diff --git a/web/src/modules/inferences/lib.ts b/web/src/modules/inferences/lib.ts index 0845c1f..40232c2 100644 --- a/web/src/modules/inferences/lib.ts +++ b/web/src/modules/inferences/lib.ts @@ -167,8 +167,8 @@ function scalerStep({ result, status }: Picked): if (!result) return { key, state: "off", detail: "none" }; if (result.error) return { key, state: "wait", detail: "unreadable" }; if (result.missing) return { key, state: "bad", detail: "deleted" }; - // AbleToScale replaced ScalingDeferred (True meant held back) and ScalingActive replaced - // DecisionAvailable. The controller leaves a stale condition of the old type in place. + // AbleToScale replaced ScalingDeferred (True meant held back), which the controller leaves + // in place, stale. DecisionAvailable is still written on every sync as a deprecated mirror of ScalingActive. const conditions = status?.conditions ?? []; const ofType = (type: string) => conditions.find((c) => c.type === type); const active = ofType("ScalingActive") ?? ofType("DecisionAvailable"); From dea3127a38178868627c2ca0121b93d0e09466f4 Mon Sep 17 00:00:00 2001 From: huskcass <333684917+huskcass@users.noreply.github.com> Date: Mon, 26 Oct 2026 01:38:53 -0700 Subject: [PATCH 4/4] fix(inferences): drop ScalingDeferred from the LLMScaler step ScalingDeferred only shipped in a release candidate. The released operator to stay compatible with is 0.5.0, which writes DecisionAvailable under the Custom provider and no condition under Prometheus. - no ScalingDeferred handling; a missing AbleToScale means no hold or failure information - tests use the 0.5.0 status shape in place of the ScalingDeferred cases Co-authored-by: aceforeverd <11715200+aceforeverd@users.noreply.github.com> --- web/src/modules/inferences/lib.test.ts | 43 +++++++------------------- web/src/modules/inferences/lib.ts | 7 +---- 2 files changed, 12 insertions(+), 38 deletions(-) diff --git a/web/src/modules/inferences/lib.test.ts b/web/src/modules/inferences/lib.test.ts index 49e2e8a..3142772 100644 --- a/web/src/modules/inferences/lib.test.ts +++ b/web/src/modules/inferences/lib.test.ts @@ -252,48 +252,27 @@ describe("installSteps", () => { { type: "AbleToScale", status: "True", reason: "DeferralTimedOut" }, ], 1, 2)).toEqual({ key: "scaler", state: "wait", detail: "scaling", params: { cur: 1, want: 2 } }); }); - it("reads an old scaler with ScalingDeferred=False as steady", () => { + // Operator 0.5.0 sets DecisionAvailable only under the Custom provider, and no condition under Prometheus. + it("reads a 0.5.0 scaler with a decision as steady", () => { expect(step([ - { type: "DecisionAvailable", status: "True", reason: "DecisionReceived" }, - { type: "ScalingDeferred", status: "False", reason: "NotDeferred" }, - ])).toEqual({ key: "scaler", state: "ok", detail: "steady", params: { n: 1 } }); + { type: "DecisionAvailable", status: "True", reason: "DecisionReceived", message: "decision server recommends 2 replicas" }, + ], 2, 2)).toEqual({ key: "scaler", state: "ok", detail: "steady", params: { n: 2 } }); }); - it("shows an old ScalingDeferred=True as waiting, with its reason", () => { + it("calls a 0.5.0 DecisionAvailable=False a failure, with its message", () => { expect(step([ - { type: "DecisionAvailable", status: "True", reason: "DecisionReceived" }, - { type: "ScalingDeferred", status: "True", reason: "RolloutInProgress" }, - ], 1, 2)).toEqual({ key: "scaler", state: "wait", detail: "deferred", params: { reason: "RolloutInProgress" } }); + { type: "DecisionAvailable", status: "False", reason: "DecisionUnavailable", message: "decision server returned 404" }, + ])).toEqual({ key: "scaler", state: "bad", detail: "failingMessage", params: { reason: "DecisionUnavailable", message: "decision server returned 404" } }); }); - it("calls an old DecisionAvailable=False a failure", () => { - expect(step([ - { type: "DecisionAvailable", status: "False", reason: "DecisionUnavailable" }, - { type: "ScalingDeferred", status: "False", reason: "NotDeferred" }, - ])).toEqual({ key: "scaler", state: "bad", detail: "failing", params: { reason: "DecisionUnavailable" } }); + it("reads a 0.5.0 Prometheus scaler with no conditions from its replicas", () => { + expect(step([], 1, 3)).toEqual({ key: "scaler", state: "wait", detail: "scaling", params: { cur: 1, want: 3 } }); + expect(step([], 3, 3)).toEqual({ key: "scaler", state: "ok", detail: "steady", params: { n: 3 } }); }); it("fails on AbleToScale=False with its message when ScalingActive is missing", () => { expect(step([ { type: "AbleToScale", status: "False", reason: "TargetUnreadable", message: "cannot read deployment demo" }, ])).toEqual({ key: "scaler", state: "bad", detail: "failingMessage", params: { reason: "TargetUnreadable", message: "cannot read deployment demo" } }); }); - it("shows an old object with only ScalingDeferred=True as waiting", () => { - expect(step([{ type: "ScalingDeferred", status: "True", reason: "ScaleDownSettling" }], 3, 1)) - .toEqual({ key: "scaler", state: "wait", detail: "deferred", params: { reason: "ScaleDownSettling" } }); - }); - it("fails on AbleToScale=False and ignores a stale ScalingDeferred=True", () => { - expect(step([ - { type: "ScalingActive", status: "True", reason: "DecisionReceived" }, - { type: "AbleToScale", status: "False", reason: "TargetNotFound" }, - { type: "ScalingDeferred", status: "True", reason: "RolloutInProgress" }, - ])).toEqual({ key: "scaler", state: "bad", detail: "failing", params: { reason: "TargetNotFound" } }); - }); - it("ignores a stale ScalingDeferred when AbleToScale is present", () => { - expect(step([ - { type: "ScalingActive", status: "True", reason: "DecisionReceived" }, - { type: "AbleToScale", status: "True", reason: "Synced" }, - { type: "ScalingDeferred", status: "True", reason: "RolloutInProgress" }, - ], 1, 2)).toEqual({ key: "scaler", state: "wait", detail: "scaling", params: { cur: 1, want: 2 } }); - }); - it("ignores a stale DecisionAvailable when ScalingActive is present", () => { + it("prefers ScalingActive over DecisionAvailable", () => { expect(step([ { type: "ScalingActive", status: "True", reason: "MetricsAvailable" }, { type: "DecisionAvailable", status: "False", reason: "DecisionUnavailable" }, diff --git a/web/src/modules/inferences/lib.ts b/web/src/modules/inferences/lib.ts index 40232c2..bec3598 100644 --- a/web/src/modules/inferences/lib.ts +++ b/web/src/modules/inferences/lib.ts @@ -167,8 +167,7 @@ function scalerStep({ result, status }: Picked): if (!result) return { key, state: "off", detail: "none" }; if (result.error) return { key, state: "wait", detail: "unreadable" }; if (result.missing) return { key, state: "bad", detail: "deleted" }; - // AbleToScale replaced ScalingDeferred (True meant held back), which the controller leaves - // in place, stale. DecisionAvailable is still written on every sync as a deprecated mirror of ScalingActive. + // DecisionAvailable is a deprecated mirror of ScalingActive and the only condition operator 0.5.0 writes. const conditions = status?.conditions ?? []; const ofType = (type: string) => conditions.find((c) => c.type === type); const active = ofType("ScalingActive") ?? ofType("DecisionAvailable"); @@ -184,10 +183,6 @@ function scalerStep({ result, status }: Picked): if (able?.status === "True" && able.reason && SCALE_HOLD_REASONS.has(able.reason)) { return { key, state: "wait", detail: "deferred", params: { reason: able.reason } }; } - if (!able) { - const deferred = ofType("ScalingDeferred"); - if (deferred?.status === "True") return { key, state: "wait", detail: "deferred", params: { reason: deferred.reason || deferred.type } }; - } const cur = status?.currentReplicas; const want = status?.desiredReplicas;