Skip to content

fix(sim): keep the autoscaler controlling when a knob is not a number - #64

Merged
xevrion merged 1 commit into
xevrion:mainfrom
kevin9327:fix/autoscaler-knobs
Sep 7, 2026
Merged

fix(sim): keep the autoscaler controlling when a knob is not a number#64
xevrion merged 1 commit into
xevrion:mainfrom
kevin9327:fix/autoscaler-knobs

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

What

The autoscaler's fallbacks carry this note:

Fallbacks for the optional config knobs, so an unset field is never NaN.

?? delivers that for an UNSET field. It does nothing for a field that is set to something which is
not a number, and none of the six knobs is among the nine config numbers isTopology checks. NaN
then survives every reading, because they are all written from comparisons and NaN fails all of them:

Math.max(1, Math.floor(cfg.minCapacity ?? DEFAULT_MIN_INSTANCES))   // NaN
clamp01(cfg.targetUtil ?? DEFAULT_TARGET_UTIL)                      // NaN
Math.max(0.01, cfg.scaleStepPct ?? DEFAULT_STEP_PCT)                // NaN
Math.max(0, cfg.warmupMs ?? DEFAULT_WARMUP_MS)                      // NaN

The controller does not crash. It stops controlling, which looks exactly like a design that did not
need to scale.

Measured

One client at 400 rps into a service with capacity: 2, serviceMs: 40, one instance, with an
autoscaler on a control edge. Thirty simulated seconds, so several cooldowns:

                    instances   completed
baseline                   11        6756
targetUtil: NaN             1        1520   controller never acts
scaleStepPct: NaN           1        1520   targetInstances is NaN
warmupMs: NaN               1        1520   the warm-up never lands
minCapacity: NaN           19        6753   bound gone, overshoots
maxCapacity: NaN           19        6753   bound gone, overshoots
cooldownMs: NaN             9        7588   decides every tick

Three of the six cost 77% of the design's throughput. The reader sees a fleet that never grows under
load, which is the exact opposite of the lesson the component exists to teach — and setpoint and
targetInstances reach the panel as NaN in two of them.

Where it comes from

Not the inspector: every one of these is a bounded slider or number input. isTopology checks
capacity, serviceMs, serviceCv, queueLimit, hitRate, errorRate, timeoutMs, retries
and rps, and none of the controller's knobs, so a shared link, a .breakscale file and a restored
session carry them through untouched.

Fix

One knob(value, fallback) helper that treats a value which is not a finite number the way an absent
one is already treated, applied to all six reads. That is what the existing note already promises;
?? was simply the wrong operator to promise it with.

Falling back to the module's own DEFAULT_* is the only available answer: when the field itself is
NaN there is no earlier value to recover.

Tests

src/sim/behaviour-control.autoscaler.test.ts, new. The reading that matters is not the knob, it is
whether the fleet still grows:

  • a baseline that genuinely needs to scale, anchoring the rest
  • each of the six knobs set to NaN still scales past five instances and still serves over 4000
  • targetUtil, minCapacity and scaleStepPct set to NaN are byte-identical to the baseline,
    because for those three the documented fallback is also what the node carries — the sharpest
    available statement that the guard changed nothing real
  • setpoint and targetInstances are numbers
  • a design naming all six knobs explicitly is untouched

Against main, eight of the thirteen fail:

× still scales when targetUtil is NaN          expected 1 to be greater than 5
× still scales when scaleStepPct is NaN
× still scales when warmupMs is NaN
× is identical to the baseline when minCapacity is NaN    expected 18 to be 11
× publishes a setpoint that is a number        expected true to be false

How I tested

Windows 11, Bun 1.3.14. bun run test is 942 passed across 40 files, up from 929 by the thirteen new
tests, with nothing else moving. bun run typecheck and bun run format:check are clean, and
bun run lint reports the same pre-existing warnings as main.

Last of the family alongside #58, #60, #61, #62 and #63. This one is the only one where nothing
throws and nothing hangs — the simulation runs perfectly and teaches the wrong thing — which is why
I went looking at throughput rather than at exceptions.

@kevin9327
kevin9327 requested a review from xevrion as a code owner September 6, 2026 11:55
@vercel

vercel Bot commented Sep 6, 2026

Copy link
Copy Markdown

@kevin9327 is attempting to deploy a commit to the whoarrryou's projects Team on Vercel.

A member of the Team first needs to authorize it.

@xevrion

xevrion commented Sep 7, 2026

Copy link
Copy Markdown
Owner

The knob() helper is a nicer shape than repeating the guard at each call site. LGTM, thanks!

@xevrion
xevrion merged commit ea11c5b into xevrion:main Sep 7, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants