test: de-flake the concurrency/blob-store specs - #59
Merged
Conversation
The @openbucket/nestjs unit suite was skipped by release-nestjs.yml because it was known-flaky under full-suite parallelism. Fix the three root causes so the suite is deterministic (60/60 full-suite runs green): 1. s3/concurrency.spec.ts — un-skip the two same-target concurrency invariants. They were quarantined (a8f185b) before their prerequisite landed: per- (bucket,key) write serialization (ObjectWriterService.withKeyLock, F6) shipped in c87ef90 but the tests were never re-enabled. With the lock in place the concurrent same-key PUT and same-partNumber UploadPart cases are deterministic. 2. common/middleware/request-id.middleware.spec.ts — the only supertest spec that built its Express app in beforeEach and called request(app) per test, standing up + tearing down a fresh ephemeral server for every request. Under CPU saturation that churn intermittently surfaced as a client-side "socket hang up". Build one persistent server in beforeAll (as every other supertest spec does). 3. admin/backup/scheduled-backup.service.spec.ts — the run-now test waited a fixed setTimeout(50ms) for a fire-and-forget cycle, then afterEach fs.rm raced the still-writing cycle -> ENOTEMPTY. Await the shared in-flight promise via runSnapshotCycle('manual') instead of sleeping. All changes are test-only; no production code changed. release-nestjs.yml can now drop its unit-suite skip (left for a follow-up). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ProjectBay
added a commit
that referenced
this pull request
Jul 9, 2026
The release workflow used to skip the unit suite because a few specs were flaky; those were de-flaked (#59), so the publish gate now runs `nx test nestjs` — the tag you publish must pass its own tests. Co-authored-by: Daniel Bersenkowitsch <accounts@projectbay.io> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
release-nestjs.ymldeliberately skips the@openbucket/nestjsunit suite because it was "known-flaky (race, not parallelism)". That is a 1.0 blocker: the release we tag 1.0 should pass its own full suite. This PR finds, diagnoses, and fixes the flakiness at the root so the suite is deterministic and the release gate can stop skipping it.The flakiness only reproduces under full-suite CPU saturation (many Jest workers). Looping the whole suite before this PR failed ~2/12 runs; after, 60/60 full-suite runs are green (two rounds of 30).
Root causes & fixes (all test-only — no production code changed)
1.
s3/concurrency.spec.ts— two invariants were quarantined (it.skip) but their prerequisite fix has since landedThe two same-target concurrency cases were skipped in Jun 2026 (commit
a8f185b) because the write path could tear under a same-key/same-part race. The hardening they explicitly waited for — per-(bucket,key)write serialization (ObjectWriterService.withKeyLock, F6) — landed afterwards inc87ef90(Jul 2026), but the tests were never re-enabled.withKeyLocknow serializes writers of the same(bucket,key)through a keyed async mutex, so the loser's rollback can no longer unlink the winner's committed blob. Row, blob bytes, and ETag agree on one winner.randomUUID-suffixed tmp file (noO_EXCLcollision) andrename(2)s onto the shared<n>.part; on POSIX (Linux CI, macOS dev) that is atomic last-wins, so the final part is one whole writer's payload.Fix: un-skip both cases and refresh the stale quarantine comment. They now pass 30/30 in isolation and in the full suite.
2.
common/middleware/request-id.middleware.spec.ts— "socket hang up" under loadThis was the only supertest spec that built its Express app in
beforeEachand calledrequest(app)per test, which makes supertest stand up and tear down a brand-new ephemeral server for every request (4× per run). Under full-suite CPU saturation that rapid listen/connect/close churn intermittently surfaced as a client-sidesocket hang up— the failing case varied run to run (case 1, case 4…), the tell-tale of a transport-level flake rather than a logic bug.Fix: adopt the pattern every other supertest spec in the lib already uses — build the app and one persistent listening server once in
beforeAll, reuserequest(server), close it inafterAll. The middleware is stateless, so each request still mints a fresh id; every assertion is unchanged.3.
admin/backup/scheduled-backup.service.spec.ts—ENOTEMPTYduring cleanupThe "run-now joins an in-flight cycle" test fires
runNowOrJoin()(a fire-and-forget async snapshot cycle) and then waited a fixed wall-clocksetTimeout(50ms)before asserting. Under load the cycle outlived the sleep and was still writing files into the per-test temp dir whenafterEach'sfs.rm(dir, { recursive })ran — thermdirthen hit a dir that a concurrent write had just repopulated →ENOTEMPTY: directory not empty.Fix: replace the sleep with a deterministic join —
await svc.runSnapshotCycle('manual')returns the same liveinFlightpromise (never starts a second cycle), so we wait for the fire-and-forget cycle to fully finish writing before cleanup. No timing assumption; thewriteSnapshot-called-once assertion is unchanged.Evidence — 25×+ green
npx nx lint nestjs→ 0 errors.npx nx test nestjs→ all suites pass (2 previously-skipped concurrency cases now run).Follow-up
With the suite now deterministic,
release-nestjs.ymlcan drop the "publish gate deliberately does NOT re-run the unit suite" carve-out and gate the release on the fullnx test nestjs. Intentionally left out of this PR to keep it test-only and reviewable.🤖 Generated with Claude Code