Skip to content

fix(gridfs): close remaining upload races and stream append safely - #16

Open
rockyshi1993 wants to merge 4 commits into
mainfrom
fix/gridfs-remaining-issues
Open

rockyshi1993 wants to merge 4 commits into
mainfrom
fix/gridfs-remaining-issues

Conversation

@rockyshi1993

@rockyshi1993 rockyshi1993 commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Single PR for the remaining GridFS issues, based on current main (693f214, which already includes merged MZ-17 / PR #15). PR #15 is left untouched.

  • Base: 693f21410c1c5db166cc1d836f9863e4b6c674e7
  • Head: 7f8bf89cb9d5af0b8d50d0d4d42e79661ad5f9cf
  • Commits: 43850aa (functional + tests + docs), 7f8bf89 (package-budget raise only, explained below)

File changes by item

1. P1 beginResumableUpload precheck race

  • src/capabilities/gridfs/resumable.ts — after the session insert, re-check files. If a file is visible, drop the new pending session (best effort) and throw GRIDFS_FILE_EXISTS. Never deleteMany chunks of a visible file.
  • Tests: test/unit/gridfs/gridfs.test.ts, test/integration/gridfs/gridfs-remaining-issues.test.ts
  • Deterministic evidence (must fail on main, pass on this branch): see Test results.

2. P2 finalize stuck in finalizing

  • src/capabilities/gridfs/resumable.ts — after this worker's pending→finalizing CAS, GRIDFS_FILE_EXISTS / GRIDFS_INTEGRITY_MISMATCH from committedFile or a post-insert duplicate conflict CAS-restore the session to pending. Already-finalizing retry path and ambiguous driver errors still hold finalizing.
  • Tests: unit + gridfs-remaining-issues.test.ts (abort via public API works; visible chunks are retained).

3. P2 renameFile finally masking the real result

  • src/capabilities/gridfs/operations.ts — aligned with upload / uploadStream: warn on release failure, return the successful rename or rethrow the original error.
  • Tests: test/unit/gridfs/gridfs.test.ts

4. P2 gridfs-recover.cjs stale pre-claim visibility / unchecked verifyVisibleFile

  • scripts/maintenance/gridfs-recover.cjs — apply re-reads files after the exclusive claim; visibility changes fail closed. If a required visible file cannot be verified (verifyVisibleFile returns null), fail closed and leave recovering.
  • Tests: test/integration/gridfs/gridfs-stale-recovery.test.ts
  • Docs: docs/en/gridfs-recovery.md, docs/zh/gridfs-recovery.md

5. MZ-02 design A — incremental appendChunks(Readable)

  • New internal src/capabilities/gridfs/stream-chunks.ts (not a public export)
  • src/capabilities/gridfs/resumable.ts — CAS-claim activeAppend first; Buffer/Uint8Array still plan-then-write; Readable writes complete chunks as they arrive; residual + current read() buffer only; trailing partial / oversize rejected during consumption; replay / busy / no deletion of others' chunks unchanged.
  • Tests: test/unit/gridfs/stream-chunks.test.ts (peak memory does not scale with input size, backpressure, partial/oversize, onChunk error), plus unit and integration append coverage.

6. MZ-06 docs-only

  • docs/en/gridfs.md, docs/zh/gridfs.md — keep “no visible wrong file before verification”; disclose full buffering of upload(Readable, { sha256 }); large files should use resumable + a modest-highWaterMark Readable. No new API.

7. MZ-07 docs-only

  • Same gridfs docs: callers must persist uploadId / length / sha256. After a vanished session, getFile + re-hash actual chunks; never trust metadata.sha256 alone. No new public API.

8. MZ-03 inspect-only operator script

  • New scripts/maintenance/gridfs-inspect-orphans.cjs — paginated walk of { files_id, n }, unified files+uploads re-check, report only (visible_file / owned / candidate_orphan). No apply, no delete, no age-based deletion, not on the public bucket API.
  • Tests: test/integration/gridfs/gridfs-inspect-orphans.test.ts
  • Documented in en/zh recovery runbooks.

9. MZ-09 / 13 / 14 — docs only

  • docs/en/support-matrix.md, docs/zh/support-matrix.md — GridFS memory-server coverage is 7.0.37 and 8.0.26; Search / Vector Search against Atlas, atlas-local, or mongot remain UNVERIFIED.

Test runner wiring

  • test/run-tests.cjs, scripts/validation/run-memory-server-matrix.cjs — new suites added (gates strengthened, not weakened).

Package budgets (separate commit)

  • config/package-budget.json — ceilings raised 890000→896000 CJS and 887000→893000 ESM. Local build after the functional commit was 892639 / 889922, which exceeded the previous ceilings. No dependency change. Headroom ~3 KiB.

Design decisions

  • Begin prechecks stay as a fast path; the session insert is the id claim; the post-insert files re-check is the fail-closed gate.
  • Finalize rollback is only for closed, definitive conflicts after this worker won pending→finalizing. Ambiguous insert errors and an already-finalizing in-flight owner stay fenced.
  • Streaming append does not add maxAppendBytes or any new public option. Peak-memory instrumentation is an internal callback on the unexported helper.
  • Inspect-only first: no apply mode in this PR.
  • MZ-06 / MZ-07 stay contract-compatible: no new public API, no weakening of “no visible wrong file before verification.”

Test commands, versions, and results

Local box: Node v24.22.1, mongodb-memory-server 10.4.3. CI Node 18/20/22 was not waited on and Node 20 was not run locally (UNVERIFIED here).

P1 evidence (deterministic hook; fail on main, pass on branch)

Script: evidence checkout path beginResumableUpload-precheck-race-deterministic.cjs (MongoDB 7.0.37, Driver 6.21.0).

Tree SHA Exit Result
main worktree 693f214 2 passed: false, errorCode: null, fileVisible: true, chunkCount: 0, downloadedLength: 0, corruption: true (visible file, chunks deleted, download() returns empty Buffer)
this branch 7f8bf89 0 passed: true, errorCode: "GRIDFS_FILE_EXISTS", chunkCount: 1, downloadedLength: 4, sessionStatus: null, corruption: false

Gates

Check Result
npm run lint 0 errors (206 pre-existing warnings)
npm run type-check (build + tsc --noEmit + tsd including gridfs-usage) pass
npm run build pass
npm run check:docs-examples pass (112/112)
npm run check:doc-claims pass
npm run check:package-budgets failed at 890000/887000 (892639/889922); pass after the separate budget commit (896000/893000)

GridFS suites (new tests included), 73/73 pass in each of four combinations

Commands (serial):

node scripts/compile-tests.cjs
node --test --test-concurrency=1 \
  .generated/test-dist/test/unit/gridfs/gridfs.test.js \
  .generated/test-dist/test/unit/gridfs/stream-chunks.test.js \
  .generated/test-dist/test/integration/gridfs/gridfs.test.js \
  .generated/test-dist/test/integration/gridfs/gridfs-scan-regressions.test.js \
  .generated/test-dist/test/integration/gridfs/gridfs-ownership-regressions.test.js \
  .generated/test-dist/test/integration/gridfs/gridfs-stale-recovery.test.js \
  .generated/test-dist/test/integration/gridfs/gridfs-remaining-issues.test.js \
  .generated/test-dist/test/integration/gridfs/gridfs-inspect-orphans.test.js
Driver MongoDB (memory-server) Result
6.21.0 7.0.37 73 pass / 0 fail
6.21.0 8.0.26 73 pass / 0 fail
7.8.0 (temporary npm install mongodb@7.8.0 --no-save) 7.0.37 73 pass / 0 fail
7.8.0 8.0.26 73 pass / 0 fail

Driver restored to 6.21.0 afterwards. package-lock.json was not committed.

Risks

  • Streaming appendChunks can persist a complete prefix and then reject a trailing partial; callers must retry from the reported nextChunk. Documented. Buffer payloads are still fully planned before any insert (existing tests unchanged).
  • Finalize rollback uses { _id, status: 'finalizing' } (same CAS shape as the existing validation rollback). Only the pending→finalizing winner reaches this path.
  • Inspect script is report-only; operators must not treat candidate_orphan as a delete authorization.
  • Budget ceiling increase is reviewed and isolated; CI bundle size may differ slightly from the local 892639 / 889922 figures.
  • Node 18/20/22 CI and Atlas / mongot Search (MZ-09/13) are not claimed as verified by this local run.

CI is not being waited on; run links will be recorded once triggered.


Note

High Risk
Touches GridFS ownership, chunk lifecycle, and crash-recovery paths where mistakes can corrupt or delete user data; streaming append can leave partial chunks before failing.

Overview
This PR hardens GridFS resumable upload and operator tooling after several race and stuck-state bugs.

Resumable upload: beginResumableUpload now re-checks files after the session insert and fails closed with GRIDFS_FILE_EXISTS without deleting chunks of a concurrently visible file. appendChunks gains incremental Readable consumption via internal stream-chunks.ts (bounded memory, backpressure, partial/trailing reject with retriable prefix). finalizeResumableUpload rolls finalizing back to pending on definitive GRIDFS_FILE_EXISTS / GRIDFS_INTEGRITY_MISMATCH so abortResumableUpload still works. rename no longer masks the real outcome when reservation release fails.

Operations: gridfs-recover.cjs apply mode re-reads file visibility after the exclusive claim and fails closed if visibility changed or verifyVisibleFile cannot confirm a required visible file. New inspect-only gridfs-inspect-orphans.cjs paginates chunks and classifies visible_file / owned / candidate_orphan without delete or apply.

Docs (en/zh) and support-matrix notes align with streaming semantics and verification expectations. Package bundle ceilings rise to 896000/893000; new unit/integration suites are wired into test runners.

Reviewed by Cursor Bugbot for commit af16030. Bugbot is set up for automated code reviews on this repo. Configure here.

Re-check files after beginResumableUpload inserts a session and never
delete chunks of a file that became visible in that window. Roll a
finalizing session back to pending after GRIDFS_FILE_EXISTS or
GRIDFS_INTEGRITY_MISMATCH so public abort works. Align renameFile
release-owner handling with upload so a cleanup failure cannot mask
the real result. Re-read visibility after a recovery claim and fail
closed when verifyVisibleFile cannot confirm a required visible file.

Consume Readable appends incrementally (design A): claim first, honor
backpressure, buffer only the residual plus the in-flight chunk, and
reject trailing partial or oversize input during consumption. Add an
inspect-only, paginated orphan reporter with no apply or delete path.
Clarify buffering, large-file resumable guidance, and the
caller-persisted verification contract in the English and Chinese
docs. Record Search/Vector Search Atlas deployments as UNVERIFIED.
The remaining-issue change set exceeded the 890000 / 887000 ceilings
(892639 CJS / 889922 ESM in the local 2026-11-12 build). The growth is
from the incremental Readable append consumer, the beginResumableUpload
post-insert files re-check, finalize pending rollback, and rename
release-owner alignment; dependencies are unchanged. Raise to 896000 /
893000 to keep about 3 KiB of reviewed headroom.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7f8bf89. Configure here.

Comment thread src/capabilities/gridfs/resumable.ts Resolved
…e docs

R1-01: consumeReadableChunks now handles already-destroyed streams and
premature close (destroy without error). The read promise rejects in
finite time, settles once, waits for an in-flight onChunk write before
settling, and removes its listeners on every path so activeAppend can be
released.

R1-03: gridfs-inspect-orphans streams NDJSON findings, caps each run
with maxFindings (default 1000), and exposes an exclusive
resumeAfter.filesId cursor. Summary counters accumulate incrementally.
The tool remains inspect-only.

R1-04: English and Chinese GridFS docs no longer say a failed append
returns nextChunk. Clients resume from the last successfully
acknowledged position, or from GRIDFS_RESUME_MISMATCH.expectedNextChunk
when the stored prefix has moved.
After the R1 documentation corrections, the English search index measures
1800511 bytes, 511 over the 1800000 ceiling. CI run 43196147300 already
failed at 1800085 on the previous PR head; this round added the failed
append resume contract (R1-04) and inspect memory-bound run notes
(R1-03) on the GridFS and recovery pages. The Chinese index is 1672743
and the combined total 3473254 remains under 3500000. The growth is from
GridFS remaining-issue documentation, not from new website dependencies.
The maintainer explicitly authorized raising this ceiling instead of
trimming documentation. Raise maximumSearchIndexBytesPerLocale from
1800000 to 1804000 to keep about 3.4 KiB of reviewed headroom.

This branch has not been deployed

No deployments
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.

1 participant