test(integration): de-flake computed indexed properties REST block - #1721
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a client-side readiness poll (waitForComputedResolver) in the computed indexed properties integration test to ensure that the JS-computed resolver is fully initialized across all HTTP workers before executing tests. It also updates documentation comments explaining the execution context of the main thread versus worker threads. The review feedback recommends wrapping the polling requests in a try-catch block to handle transient network errors gracefully and prevent unexpected test failures due to unhandled promise rejections.
|
Reviewed; no blockers found. |
The 'Computed indexed properties' suite exercises jsTotalPrice, a '@computed @indexed' attribute whose resolver is registered at runtime by resources.js (setComputedAttribute) as per-worker in-memory state (Table.ts userResolvers), not at schema-load time. The /Product/ route (from the @export'd table) starts serving as soon as the schema loads, which can be before resources.js finishes on a worker. Harper runs many http workers (default cpus-1) and load-balances via SO_REUSEPORT, so a request can hit a still-cold worker: - a PUT on a cold worker indexes jsTotalPrice as undefined (frozen at write time), so the ?jsTotalPrice=119 filter misses the record; and - a GET ?select(jsTotalPrice) on a cold worker recomputes null. Either surfaces as an intermittent failure in the REST block (seen on Integration Tests 5/6, Node v26). Fix: gate the suite (before all reads/writes) on the jsTotalPrice resolver being live on every worker — poll on-read bursts (fresh connections spread across workers via Connection: close) until several consecutive bursts see zero cold responses. The route probe alone only proves the table is reachable on one worker. Stays multi-worker (functional test, not a single-thread special case). Verified on a 19-worker box by deferring the resolver registration 4s: the ungated write+filter fails deterministically (3/3), the gated version passes (3/3); the unmodified suite passes across repeated runs. Also corrects the search_by_value comment: jsTotalPrice is null there because the operations API runs on the main thread, where resources.js never registered the resolver — not because of a write-time race. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
828c842 to
dce7084
Compare
Ethan-Arrowood
left a comment
There was a problem hiding this comment.
Great root-cause writeup — much better than pinning the suite to one worker.
sent with Claude Fable 5
What
De-flakes the
Computed indexed propertiesREST block inintegrationTests/apiTests/computed-indexed-properties.test.mjs(seen failing intermittently on Integration Tests 5/6, Node v26).Root cause
jsTotalPriceis a@computed @indexedattribute whose resolver is registered at runtime by the component'sresources.js(setComputedAttribute) as per-worker in-memory state (resources/Table.tsuserResolvers) — unlike the@computed(from: …)expression attributes, whose resolvers are set at schema-load time.The
/Product/route (from the@export'd table) starts serving as soon as the schema loads, which can be beforeresources.jsfinishes on a given http worker. Harper runs many http workers (defaultcpus-1) and load-balances viaSO_REUSEPORT, so a request can hit a still-cold worker. Two failure modes:@indexedvalue with a missing resolver →jsTotalPriceis indexed asundefined, frozen at write time (never self-heals on read), so?jsTotalPrice=119misses the record; and?select(jsTotalPrice)served by a cold worker recomputesnull.The existing route probe only proves the table is reachable on one worker — not that the resolver is registered everywhere.
Fix (test-only)
Add
waitForComputedResolver, a client-side readiness gate (in the spirit ofrestartHttpWorkers) that seeds a throwaway probe record and reads the on-demand computed value in bursts — each burst opens fresh connections (Connection: close) so requests spread across workers via SO_REUSEPORT — until several consecutive bursts see zero cold responses, i.e. every worker has runresources.js. The suite stays multi-worker (this is a functional test, not a single-thread special case).Also corrects the
search_by_valuecomment:jsTotalPriceisnullthere because the operations API runs on the main thread, whereresources.jsnever registered the resolver — not because of a write-time race, as the old comment claimed.Verification
On a 19-worker box, forcing the race by deferring the resolver registration 4s (
setTimeout(() => setComputedAttribute(...), 4000)):Cross-model review
Reviewed via the cross-model-review skill (Codex leg + Harper-domain adjudication; Gemini leg skipped — test-only change, nothing left after the integration-test strip). Findings addressed in this PR:
=119filter assertions. The probe previously computed to119(same as the real assertions filter on), so a missed cleanup could reintroduce a flake in a different test. Fixed: probe now usestaxRate: 0→ computes to100(PROBE_COMPUTED), which can never satisfy?jsTotalPrice=119/?totalPrice=119; cleanup moved into afinally..expect(204)) so a seed failure fails fast with an accurate message instead of a misattributed 60s "resolver not live" timeout.readsPerBurstnow scales with host core count (max(60, cpus*8)) so a straggler worker is reliably hit even on high-core hosts.No blockers. Underlying product hardening tracked in #1631 (out of scope here).
Related
undefinedindexing when a JS-computed resolver isn't yet live at write time; main-thread ops-API parity): Computed @indexed attribute silently indexes undefined when its JS resolver isn't yet registered (per-worker readiness race) #1631