Skip to content

fix(plugins): bound the setup-field match, not the worker's birth (#607) - #609

Merged
Weegy merged 1 commit into
mainfrom
fix/607-pattern-budget-window
Aug 4, 2026
Merged

fix(plugins): bound the setup-field match, not the worker's birth (#607)#609
Weegy merged 1 commit into
mainfrom
fix/607-pattern-budget-window

Conversation

@Weegy

@Weegy Weegy commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Closes #607.

The defect

The setup-field pattern check runs every match in a worker thread under a wall-clock budget, because a regex cannot be interrupted on the thread running it. That mechanism is right and stays. What was wrong is the window being measured: the budget included worker startup and module evaluation, neither of which is regex work.

Two user-visible consequences, both timing-dependent — which is why they read as intermittent "my correct value was rejected" reports:

  1. A valid value could be rejected fail-closed because the host was busy.
  2. A healthy pattern was written into getPatternProblems() and then reported to the operator as "this field declares a format check that could not be applied" — permanently, for the life of the process.

Both are the failure class the customer test report was about: the UI stating something that is not true.

Root cause

ensureWorker() resolved readiness on the worker's 'online' event. 'online' fires when the thread starts, not when the worker module has been evaluated and its message listener exists. Everything in that gap was charged to the match budget:

path measured
cold start, first call ~838 ms
under node --import tsx (how the suite runs) 84-212 ms every call, because the worker re-runs tsx's loader
warm call, trivial pattern 4-36 ms of pure IPC — up to 71% of a 50 ms budget

Changes

1. The worker handshakes. It posts { ready: true } after installing its message handler, and readiness waits for that. The budget timer now starts after the handshake, so it bounds regex execution plus one IPC round trip.

2. The worker stays ref'd until it handshakes. This one bit me and is worth calling out: unref'ing on 'online' (which an intermediate revision of this branch did) is a deadlock — nothing else holds the event loop open while awaiting the handshake, so on an otherwise-idle loop node settles and the promise never resolves. It surfaced as Promise resolution is still pending but the event loop has already resolved across the whole file on node 22, while node 26 happened to hide it. Both versions are now verified.

3. Budget 50 ms → 250 ms. The budget is not what protects the event loop — the worker boundary is. A runaway regex burns a thread nothing is waiting on, so this number only bounds how long one admin-only setup write waits before giving up. With a measured IPC floor of 4-36 ms, 50 ms was rejecting valid values with no regex work to speak of.

4. An overrun no longer blames the pattern on its own. The write still fails closed on the first overrun — that is unchanged and deliberate. But the durable verdict now needs PATTERN_OVERRUN_STRIKES (3) consecutive overruns, and any completed match resets the count. A genuinely hostile pattern overruns every time and trips it within three writes; a healthy one recovers.

5. A dead worker is retried once, on a fresh thread. A budget expiry is never retried, so a hostile pattern still costs exactly one budget. This only stops one caller's terminate() landing on another caller's worker from being reported as "your value is invalid".

6. warmPatternWorker() at startup, so the first operator to save a credential does not pay thread creation inside their request. Fire-and-forget; the worker is still created on demand.

Tests

New coverage in middleware/test/setupFieldPatternValidation.test.ts:

  • a trivial match on a deliberately cold worker is accepted and records no problem — this is the case that overran on every run under tsx before the fix;
  • three consecutive cold starts never reject a valid value;
  • warmPatternWorker is idempotent;
  • one overrun fails the write but records no problem;
  • three consecutive overruns do record one;
  • a completed match resets the strike count.

The slow/fast pair is calibrated by measurement rather than assumed: ^[a-z]+[a-z]+[a-z]+[a-z]+$ takes ~1.6 s on a 500-character subject and ~0 ms on abcd, so the overrun is not a race and the reset test exercises the same pattern (strikes are keyed by context and pattern).

A measurement note that cost a round, recorded in the test file so the next person does not repeat it: V8 caches compiled regexes by source, so timing a subject after another subject has already run the same source reports the warm number. An earlier revision of these tests picked a subject that measured 0.011 ms that way — and 2152 ms on a first call in a fresh process.

Verification

  • middleware full suite on node 22.22.3: 5502 pass, 0 fail (--test-concurrency=4)
  • target file green on both node 22.22.3 and node 26.3.0: 76/76
  • tsc --noEmit clean, eslint clean on the changed files

prettier --check flags both changed files, but it flags them identically on origin/main — pre-existing, so reformatting here would bury the change in an unrelated diff.

Not in scope

middleware/test/ is still not typechecked by npm run typecheck (#573) — the LSP surfaces a pre-existing error in this file at an untouched line.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

The pattern match budget measured a window that included worker startup
and module evaluation, so a valid credential could be rejected because
the host was busy and a healthy pattern could be permanently reported to
the operator as uncheckable.

- The worker now handshakes when its message handler is installed, and
  readiness waits for that instead of the 'online' event. 'online' fires
  when the thread starts, well before the module has been evaluated;
  under tsx that gap is the loader re-running (84-212 ms measured), and
  on a cold start it was ~838 ms. All of it was charged to the budget.
- The worker stays ref'd until it handshakes. Unref'ing on 'online'
  deadlocks: nothing holds the event loop open while awaiting the
  handshake, which surfaced as "Promise resolution is still pending but
  the event loop has already resolved" across the suite on node 22.
- Budget raised 50 ms -> 250 ms. The worker boundary is what protects
  the event loop; this number only bounds how long one admin-only write
  waits. The measured IPC floor alone was 4-36 ms, so 50 ms rejected
  valid values with no regex work to speak of.
- A budget overrun no longer records a pattern problem on its own. The
  write still fails closed immediately, but the durable "this field is
  unchecked" verdict now needs three consecutive overruns; any completed
  match resets the count.
- A dead worker is retried once on a fresh thread. A budget expiry is
  never retried, so a hostile pattern still costs exactly one budget.
- warmPatternWorker() is called at startup so the first operator to save
  a credential does not pay thread creation inside their request.

Tests pin the cold-start path, the strike counter and its reset, using a
pattern calibrated by measurement (500-char subject ~1.6 s vs 'abcd'
~0 ms on the same source). Verified on node 22.22.3 and 26.3.0.
@Weegy
Weegy merged commit 1e72b00 into main Aug 4, 2026
9 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.

Setup-field pattern budget measures worker boot + IPC, so a valid value can be rejected and a healthy pattern mislabelled

1 participant