Repository navigation
Description
What happens
When an application's config.yaml lists jsResource before graphqlSchema, its resources.js cannot see its own schema tables at module top level on the first boot of a fresh data root. tables.CachedThing is undefined, so tables.CachedThing.sourcedFrom(Source, { expiration: 2, eviction: 2 }) throws Cannot read properties of undefined (reading 'sourcedFrom'). No source is attached, and GET /CachedThing/<id> returns 404.
After a restart on the same data root, the table already exists in the persisted catalog when components load. The same application then works: sourcedFrom is applied, the worker reports expirationMS/evictionMS 2000/2000, all 20 GETs return 200, and the cached rows expire and are removed at about 5 s.
The application behaves differently on its first boot than on every later boot.
| Owner | Build | First boot, fresh root | After restart |
|---|---|---|---|
isolated app (threads: {count: 2, maxIsolated: 1}, dedicated worker) |
PR head 51a5a77 (×3 RocksDB, ×1 LMDB) |
typeof tables.CachedThing undefined; 20/20 GETs 404 |
20/20 GETs 200; rows removed at 5.0–5.1 s |
| isolated app | main 87078c2dc |
same: undefined, 20/20 GETs 404 |
20/20 GETs 200; rows removed at 5.1 s |
shared owner, threads.count: 2 |
PR head 51a5a77 |
undefined on the worker sampled; 20/20 GETs 404 |
works |
shared owner, threads.count: 2 |
main 87078c2dc |
undefined on worker 0; GETs 6×200, 14×404 (per-worker split) |
works |
On a shared owner the result depends on timing. A worker whose resources.js evaluates after another worker's graphqlSchema has created the table can see the table. A worker that evaluates earlier cannot. So the workers disagree until a restart.
Repro
- Harper
51a5a77c1ff8e8847a6f1708f1fc1ebcdeb2c4ca(PR Clear a removed table TTL everywhere, reject invalid TTLs where they are declared, and scan catalog-loaded tables without a write #4250 head) and87078c2dc5a2217d5ae1b4da834a737c466de840(main, the merge base). - Spec: QA scenario QA-5430 (
qa5430-iso-ttl.test.tswith fixtureqa5430-iso-ttl/sourced, kept in the QA archive; candidate P-6847 promotes the TTL part to an integration test). Run fromintegrationTests/qa-scratch/:QA5430_ARM=A npm run test:integration -- "integrationTests/qa-scratch/qa5430-iso-ttl.test.ts" # isolated QA5430_ARM=A QA5430_OWNER=shared npm run test:integration -- "integrationTests/qa-scratch/qa5430-iso-ttl.test.ts" # shared, threads 2 - Fixture
config.yaml:rest: true/jsResource: {files: resources.js}/graphqlSchema: {files: schema.graphql}. Schema:type CachedThing @table @export { id: ID @primaryKey value: String }. resources.jscallstables.CachedThing.sourcedFrom(...)at top level inside a try/catch, and an/Info/resource reportstypeof tables.CachedThingat load along with the caught error.
Mechanism
components/componentLoader.ts:1014iteratesfor (const componentName in config), which isconfig.yamlinsertion order.components/componentLoader.ts:1192:await sequentiallyHandleApplication(scope, extensionModule)runs inside that loop, so each plugin's load finishes before the next one starts.- So
jsResourceevaluatesresources.jsbefore thegraphqlSchemahandler declaresCachedThing. On a fresh root nothing else has defined the table yet. - On restart, the table is defined from the persisted catalog before components load. I observed this but did not trace the catalog-load path.
- Both lines are the same on main
87078c2dc. This is not a regression from Clear a removed table TTL everywhere, reject invalid TTLs where they are declared, and scan catalog-loaded tables without a write #4250. - This is the same ordering class as roles.yaml cannot grant an operation the same component registers: roles plugin loads before jsResource #2203, where the
rolesplugin loads beforejsResource. There, config order decides whether a plugin can see what a later plugin registers. components/DEFAULT_CONFIG.tslistsgraphqlSchemabeforejsResource, so only a custom order hits this.
Existing work checked
HarperFast/harperandHarperFast/harper-pro, issues and PRs, all states. Searches: "jsResource graphqlSchema order", "jsResource before graphqlSchema", "tables undefined jsResource", "plugin order config.yaml", "tables undefined first deploy", "table undefined until restart", "load order", "jsResource", "plugin order", "component order", "tables is undefined", "order of plugins". Nothing tracks this. Related:- roles.yaml cannot grant an operation the same component registers: roles plugin loads before jsResource — harper#2203 (open): the same loader loop, a different plugin pair, and no first-boot-versus-restart split.
- Component load race fails per-worker (200/500 split-brain at threads>1) and is invisible to get_status — harper#1951 (open): the same per-worker split shape, from a different cause.
- Docs at
origin/main8bc20658e96b(reference,learn,fabric,release-notes): nothing says thatconfig.yamlplugin order decides whetherresources.jscan see schema tables, or that a fresh install differs from a restart.
Options
- Declare a component's schema tables before evaluating any of its
jsResourcemodules (a graphqlSchema pre-pass in the loader). First boot would then match restart. - Or document the order requirement, and log a warning when a component lists
jsResourcebeforegraphqlSchema.
What this does not prove
- The shared-owner run was RocksDB only. LMDB was run for the isolated owner.
- Not tested: a
resources.jswith no try/catch. Does the top-level throw fail the component and show inget_status, or only log? - Not tested: the
deploy_componentpath. The fixture was staged beforestartHarper. - Not tested: a new node joining a cluster, and whether a later redeploy repairs it without a restart.
- Whether this order is a supported configuration at all is a product decision. The behaviour is measured; the contract is undocumented.
Found by PR QA on harper#4250 (QA finding F-1245); pre-existing on main.
🤖 Filed by Claude Opus 5.5
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity