Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions web/src/modules/inferences/lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,94 @@ 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; message?: string }[], cur = 1, want = 1) => ({
result: { ref: { apiVersion: "v1", kind: "LLMScaler", name: "r" } } as ObjectResult,
status: { currentReplicas: cur, desiredReplicas: want, conditions },
});
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 } });
});
// 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", message: "decision server recommends 2 replicas" },
], 2, 2)).toEqual({ key: "scaler", state: "ok", detail: "steady", params: { n: 2 } });
});
it("calls a 0.5.0 DecisionAvailable=False a failure, with its message", () => {
expect(step([
{ 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("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("prefers ScalingActive over DecisionAvailable", () => {
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 } });
});
});

describe("age", () => {
Expand Down
22 changes: 20 additions & 2 deletions web/src/modules/inferences/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,31 @@ function routeStep({ result, spec, status }: Picked<ModelRouteSpec, ModelRouteSt
return status?.ready ? { key, state: "ok", detail: "ready", params } : { key, state: "wait", detail: "notReady", params };
}

// AbleToScale=True reasons that hold a step. Synced and DeferralTimedOut are a normal scale.
const SCALE_HOLD_REASONS = new Set(["RolloutInProgress", "ScaleDownSettling", "ScaleDownStabilized"]);

function scalerStep({ result, status }: Picked<LLMScalerSpec, LLMScalerStatus>): 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" };
const failing = status?.conditions?.find((c) => c.status === "False");
if (failing) return { key, state: "bad", detail: "failing", params: { reason: failing.reason || failing.type } };
// 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");
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 } };
}

const cur = status?.currentReplicas;
const want = status?.desiredReplicas;
if (cur === undefined) return { key, state: "wait", detail: "noDecision" };
Expand Down
2 changes: 2 additions & 0 deletions web/src/modules/inferences/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@
"unreadable": "Unreadable",
"deleted": "LLMScaler deleted",
"failing": "{reason}",
"failingMessage": "{reason}: {message}",
"deferred": "Holding: {reason}",
"noDecision": "No decision yet",
"scaling": "{cur} → {want} replicas",
"steady": "{n} replicas, steady"
Expand Down
2 changes: 2 additions & 0 deletions web/src/modules/inferences/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@
"unreadable": "读取失败",
"deleted": "LLMScaler 已被删除",
"failing": "{reason}",
"failingMessage": "{reason}:{message}",
"deferred": "伸缩暂缓:{reason}",
"noDecision": "还没有伸缩决策",
"scaling": "{cur} → {want} 副本",
"steady": "{n} 副本,稳定"
Expand Down
Loading