fix(admin): dispose factory-form ManagedRuntime per call in run/runExit - #122
Open
detail-app[bot] wants to merge 1 commit into
Open
detail-app[bot] wants to merge 1 commit into
detail-app[bot] wants to merge 1 commit into
Conversation
|
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.
Detail bug report: View on Detail
Closes #103
Bug
run()andrunExit()inpackages/admin/src/lib/functions/run.tsaccept aRuntime<R>that may be either aManagedRuntimeinstance or a() => ManagedRuntimefactory. The factory branch built a freshManagedRuntimeper call but never calleddispose()on it, so the runtime's top-level scope was never closed and any layer-levelEffect.acquireReleasefinalizers never ran — violatingManagedRuntime's contract.This was introduced in
4e322b1, which removed an unconditionalawait runner.dispose()to stop tearing down a shared instance-form runtime after every call (correct), but also stripped disposal from the factory branch whererun()/runExit()are the sole owner of the freshly-built runtime. The packagedAdmin.layerstack has no layer-level finalizers, so the leak was unobservable internally; it manifests for consumers who use the factory form with a layer containingacquireReleasefinalizers.Fix
Dispose only in the factory branch, preserving the instance-form behavior (whose lifecycle is owned by the caller, e.g.
FunctionsRuntime.make's SIGINT/SIGTERM handler). Bothrun()andrunExit()now wrap the factory-built runtime intry { ... } finally { await runner.dispose(); }, so disposal happens on success, typed failure, and defect paths. Updated the stale JSDoc to describe the actual semantics.Testing
Added
packages/admin/src/lib/functions/run.spec.ts, a focused regression suite (7 tests) for the previously-untestedrun.ts, guarding both the bug and its inverse:run()disposes after success and after a defect (finalizer runs).runExit()disposes after success and after aFailureexit.run()is not disposed; only an explicitdispose()releases it.onRequestEffectandonCallEffectdispose the factory-form runtime after handling a request/call (asserts the wrapper boundary).Routine checks all pass: vitest (full admin suite, 77/77),
@effect-firebase/admin:build, eslint, and prettier. The new spec typechecks with no new errors; the full@effect-firebase/admin:typechecknx target fails at the baseline for an unrelated, pre-existing reason (effect-firebase:typecheck— TS6307 in the upstreameffect-firebasespec config), reproduced with the change stashed and not part of CI'slint test buildchain.nx affected -t lint test buildsucceeds.End-to-end verification (not versioned): temporarily swapped the example backend's entry for a factory-form
onRequestEffecthandler over a layer with anacquireReleasefinalizer logging acquire/release, then ran the real Firebase Functions emulator (firebase emulators:start). This required unblocking the environment (installing OpenJDK; binding the emulator to IPv4127.0.0.1since the container lacks IPv6::1; using--project demo-...to skip a 401 adminSdkConfig call; generatingdist/package.jsonviacopy-workspace-modules; and waiting for the deferred "Loaded functions definitions" marker). The live request returned200 "ok"with the[release]finalizer logging exactly once — confirmingrun()disposed the per-call runtime. The same harness reportedreleaseRan: falsewith the fix stashed (the bug) andtrueafter restoring it. A matching@google-cloud/functions-frameworkE2E confirmed the same. All temporary overrides were removed and the example backend rebuilt to its committed state.Automatic Fixes PRs can be configured here.