fix(migration): always close leader-election broadcastChannel#8780
Open
pubkey wants to merge 3 commits into
Open
fix(migration): always close leader-election broadcastChannel#8780pubkey wants to merge 3 commits into
pubkey wants to merge 3 commits into
Conversation
The schema migration acquires leadership through a per-collection BroadcastChannel/leaderElector but only closed it on the success path. When startMigration threw (a failing migrationStrategy, an error while setting up the storage instances, or a non-conflict error while removing the old collection meta doc) the channel stayed open, so the tab remained leader forever and blocked other tabs from ever running the migration. Wrap the migration body in try/finally so the channel is closed on every code path, and register the onClose cleanup up-front (instead of inside migrateStorage) so it also runs when an error happens before the replication is set up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYki1roCApd2AaTxZ8ZJxj
The test attached the .catch handler to migratePromise() only after an intervening `await waitUntil(...)`. When the migration rejected before the handler was attached (timing-dependent, hit on the CI runners), the test harness treated it as an unhandledRejection and exited with code 5. Catch the rejection synchronously at the call site and assert that the broadcastChannel is released (set back to undefined, which the fix only does after close() has run). This removes the capture-while-open race and keeps the test a valid guard for the leak. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYki1roCApd2AaTxZ8ZJxj
Contributor
✅ Verify Test Reproduction: Tests FAILED without the fix (expected)This confirms the changed tests correctly reproduce the bug that the source changes fix. This workflow runs the changed tests without the source fix to verify they reproduce the bug. Show output |
async-test-util waitUntil() has no default timeout and polls forever. In the reproduction run (test executed without the source fix) the channel never becomes undefined, so the test hung until the browser CI runner was disconnected after ~26 minutes instead of failing cleanly. Pass an explicit 5s timeout so a regression fails fast with a clear "reached timeout" error. With the fix the channel is released in well under that bound. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYki1roCApd2AaTxZ8ZJxj
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.
This PR contains:
Describe the problem you have without this PR
During a schema migration,
RxMigrationState.startMigration()creates a per-collectionBroadcastChanneland acquires leadership through aleaderElector(src/plugins/migration-schema/rx-migration-state.ts, at theawait leaderElector.awaitLeadership()line). The channel was only closed on the success path.If the migration threw anywhere after leadership was acquired, the channel stayed open and the tab remained leader forever, blocking other tabs from ever running the migration. The leaking paths were:
tryblock.oldCollectionMeta,createStorageInstance(),getConnectedStorageInstances(),countAllDocuments(), and the initialupdateStatus()all ran before thetry. Any rejection propagated out with the channel still open (andstartMigrationis called fire-and-forget with a swallowed catch, so the failure was silent).catchblock. On a migration failure it closedoldStorageInstanceand set statusERROR, but never touchedbroadcastChannel.Worse, the
onClosesafety net that would release the channel on database/collection close was registered insidemigrateStorage(), so on path 1 (an error beforemigrateStoragewas ever reached) it was never registered, leaking the leader even beyond database close.Fix
try/finallyso thebroadcastChannelis closed on every code path (success, migration error, pre-migration error, meta-removal error). The body was extracted into a privaterunMigration()method to keep the diff readable.onClosecleanup up-front instartMigration()(right afterstarted = true) instead of insidemigrateStorage(), so it also runs when an error happens before the replication is set up. This also removes the previous duplicate registration (migrateStorage()is called once per connected storage plus once for the main collection).BroadcastChannel.close()is idempotent, so thefinallyclose and the existingcancel()close paths do not conflict.Test
Added
#7827 should close the broadcastChannel/leader-election when the migration errorstotest/unit/migration-schema.test.ts. It runs a multiInstance migration whosemigrationStrategythrows, captures the createdbroadcastChannel, and asserts that after the failed migration the channel is closed (isClosed === true) and released (broadcastChannel === undefined).Verified the test fails without the fix (20s timeout, channel never closes) and passes with it. All 50 tests in
migration-schema.test.tspass;lintandcheck-typesare clean.Todos
🤖 Generated with Claude Code
Generated by Claude Code