Skip to content

test(metadata-fs): external-write-resync drives the sweep at its seam, 24 s to 3.3 s (#23182 row 4) - #23262

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-23182-metadata-fs-resync-test-budget
Oct 13, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-23182-metadata-fs-resync-test-budget

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #23182 (row 4)
Clause-②: no

This PR covers row 4 of #23182 only: @objectstack/metadata-fs, test/external-write-resync.test.ts, 24.3 s in run 38217670691. It changes one test file and no production file. #23182 stays open for rows 1, 3, 5 and 6.

Where the 24 s went

Per case at BASE 6f4c518b01 (vitest, ms): create/update/delete 6042–6169, exactly-once 6082–6124, unreadable directory 7030–7048, ENOENT 5012–5018, close 6.

All of it was idle waiting, on two things:

  • The sweep's 2 s cadence. The first case waited for three sweeps in a row: create, then update, then delete. The cadence is RESYNC_INTERVAL_MS, a private constant in src/repository.ts that no option sets. Making it injectable would widen FileSystemRepositoryOptions, a published production surface, so I did not take that route.
  • Three fixed 5 s quiet windows (QUIET_WINDOW_MS), in the exactly-once, unreadable-directory and ENOENT cases.

What changed (test only)

  • One recovery still waits for the clock. In the first case, the scheduled sweep recovers the lost create with nobody calling anything. The deadline is RECOVERY_WAIT_MS (15 s, unchanged). The case now also requires the chain to re-arm after that sweep: resyncTimer must be set again. This keeps the half of the pin that says the backstop runs by itself and keeps running.
  • Every other sweep is called directly. The update and delete recoveries, and every quiet window, await the private resync(), the same body the timer runs. A quiet window is now QUIET_SWEEPS = 3 sweeps, where the old window was sized at "comfortably more than two sweeps". self-write-suppression.test.ts and external-delete-requires-absence.test.ts already test this package this way. The watcher stays blinded exactly as before, so a recovered event can only come from a sweep.
  • Two cases are stricter. In the unreadable-directory case, the first sweep that meets the fault must report it. In the ENOENT case, every driven sweep must hit the injected ENOENT (enoentServed). Before this change, the ENOENT case passed even if no sweep ran.
  • The fixed 200 ms sleep after put() in the unreadable-directory case is gone. put() is awaited and sets the head in the same continuation.
  • RECOVERY_WAIT_MS and CASE_TIMEOUT_MS are unchanged. No case was deleted, skipped or moved.

Wall time

Vitest module line, in ms, on a shared 4-core box under os-verify-lock:

BASE 6f4c518b01 AFTER 43fef097df
file alone 24228 / 24364 (a verbose run: tests 24.18 s) 3331 / 3303 / 3356
package suite, --maxWorkers=2 24226 / 24229 3387 / 3330
package suite, --maxWorkers=8 (2x oversubscribed) NOT MEASURED 3348
  • Per case AFTER: about 2055 ms for create/update/delete (the one scheduled sweep), about 1150 ms for exactly-once (the live watcher's 1 s poll), and under 110 ms for each of the other three.
  • Package suite: 11 files and 74 tests pass, before and after. The suite's summed test time fell from 34.6 s to 13.8 s.
  • CI after this change: NOT MEASURED here. This PR's own Test Core log will carry the module line.
  • origin/main (afbf960c26, which touches nothing in this package) was merged before the AFTER readings.

Ablation: every case can still fail

Each leg changed src/repository.ts through scripts/ablation-replace.mjs: the anchor count went from 1 to 0, and the marker was on disk. Each leg then ran the new file and a temporary copy of the BASE file in one vitest run. Afterwards, the file was restored to the HEAD blob f76f6dee4f48, with git diff HEAD empty. The test imports ../src/index.js, so no dist is involved.

leg mutation red cases, this PR red cases, BASE
A1 resync() returns at once 1, 3, 4 1, 3
A2 scheduleResync() never arms 1 1, 3
A3 the chain never re-arms 1 (re-arm assertion) 1 (update)
A4 the sweep forgets heads before each file 1, 2 1, 2
A5 the fault latch is removed 3 (3 reports) 3 (3 reports)
A6 an unreadable type is not recorded none none
A8 A6, plus the delete pass's existsSync re-check removed 3 (2 deletes) 3 (2 deletes)
A7 ENOENT is reported as a fault 4 4
  • A2 is the one place where this PR reds fewer cases. At BASE, the unreadable-directory case also needed the scheduler. Now only case 1 does, so the scheduler half of the pin rests on case 1's scheduled create and its re-arm assertion.
  • A6 stays green on both trees. The delete pass re-checks the disk under the per-key lock, which is a second guard. A8 removes both guards and turns case 3 red on both trees.
  • A1 reds case 4 here but not at BASE, because the old ENOENT case passed without any sweep.

Gates, lint and typecheck (at 43fef097df)

  • Gates: dispatch-gates --commands --repo objectstack-ai/objectstack gives 49 commands, identical to the dispatch list. All 49 ran. --ran with exit codes reads "49 derived, 49 run, 0 NOT-MEASURED, 0 UNRUN".
    • check:dual-build-cjs-loads and check:lean-entry-closure first exited 3 (PREREQUISITE NOT MET, no dist). Both were re-run green after turbo run build (75 tasks, 73 from cache).
    • check:sourcemap-no-sources-content was also re-run after the build: 70 packages, 554 maps.
  • Typecheck: pnpm --filter @objectstack/metadata-fs typecheck exits 0. tsc --listFiles on tsconfig.test.json names the test file once.
  • Lint (narrowed to the one file): eslint --no-inline-config --format json reports 1 file, 0 errors and 0 warnings.
    • --print-config returns a config for the file, so it is linted.
    • Its parserOptions are ecmaVersion and sourceType only. Without type-aware linting, the diff cannot change any other file's result.
  • Bytes: check:nul-bytes exits 0, and a control-byte scan of the file finds 0 lines.

No changeset

This diff changes only a test. I built the package with pnpm --filter @objectstack/metadata-fs build and searched its files (dist, README.md and CHANGELOG.md). The test-only names QUIET_SWEEPS, reportedByFirstSweep, enoentServed and blindWatcher have 0 hits. The production controls RESYNC_INTERVAL_MS and reportResyncFault hit 2 and 4 files. skip-changeset is labelled.

Acceptance notes

  • This row's pin is the next refresh PR's list no longer naming this file.
  • Outside the module line: a single-file run of this package spends 12.5 to 15.2 s in vitest's import phase (the import figure in the summary line), and no module line reports it. This is an observation only and is not filed. Carrier: none.

Generated by Claude Code

…, 24 s to ~3 s

The file spent ~24 s of wall almost entirely idle: three sequential waits on
the 2 s reconciliation cadence in the first case and three fixed 5 s quiet
windows. One recovery, the lost create, still waits for the scheduled sweep
with nobody calling anything, and the case now also requires the chain to
re-arm behind it. Every other recovery and quiet window awaits resync()
itself, so a quiet window is a count of sweeps that ran. The ENOENT case
now requires each driven sweep to have met the injected fault.

Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6d7f4df3c91c0d4e7d0a40c3b5dc796290e90a2d → packageMentionDocs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants