Skip to content

fix(admin): dispose factory-form ManagedRuntime per call in run/runExit - #122

Open
detail-app[bot] wants to merge 1 commit into
mainfrom
detail/bug-fix/fix-admin-dispose-factory-form-managedruntime-per-936ea6
Open

detail-app[bot] wants to merge 1 commit into
mainfrom
detail/bug-fix/fix-admin-dispose-factory-form-managedruntime-per-936ea6

Conversation

@detail-app

@detail-app detail-app Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Detail bug report: View on Detail

Closes #103

Bug

run() and runExit() in packages/admin/src/lib/functions/run.ts accept a Runtime<R> that may be either a ManagedRuntime instance or a () => ManagedRuntime factory. The factory branch built a fresh ManagedRuntime per call but never called dispose() on it, so the runtime's top-level scope was never closed and any layer-level Effect.acquireRelease finalizers never ran — violating ManagedRuntime's contract.

This was introduced in 4e322b1, which removed an unconditional await runner.dispose() to stop tearing down a shared instance-form runtime after every call (correct), but also stripped disposal from the factory branch where run()/runExit() are the sole owner of the freshly-built runtime. The packaged Admin.layer stack has no layer-level finalizers, so the leak was unobservable internally; it manifests for consumers who use the factory form with a layer containing acquireRelease finalizers.

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). Both run() and runExit() now wrap the factory-built runtime in try { ... } 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-untested run.ts, guarding both the bug and its inverse:

  • Factory-form run() disposes after success and after a defect (finalizer runs).
  • Factory-form runExit() disposes after success and after a Failure exit.
  • Instance-form run() is not disposed; only an explicit dispose() releases it.
  • onRequestEffect and onCallEffect dispose 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:typecheck nx target fails at the baseline for an unrelated, pre-existing reason (effect-firebase:typecheck — TS6307 in the upstream effect-firebase spec config), reproduced with the change stashed and not part of CI's lint test build chain. nx affected -t lint test build succeeds.

End-to-end verification (not versioned): temporarily swapped the example backend's entry for a factory-form onRequestEffect handler over a layer with an acquireRelease finalizer logging acquire/release, then ran the real Firebase Functions emulator (firebase emulators:start). This required unblocking the environment (installing OpenJDK; binding the emulator to IPv4 127.0.0.1 since the container lacks IPv6 ::1; using --project demo-... to skip a 401 adminSdkConfig call; generating dist/package.json via copy-workspace-modules; and waiting for the deferred "Loaded functions definitions" marker). The live request returned 200 "ok" with the [release] finalizer logging exactly once — confirming run() disposed the per-call runtime. The same harness reported releaseRan: false with the fix stashed (the bug) and true after restoring it. A matching @google-cloud/functions-framework E2E confirmed the same. All temporary overrides were removed and the example backend rebuilt to its committed state.


Automatic Fixes PRs can be configured here.

@detail-app
detail-app Bot requested a review from fwal as a code owner September 19, 2026 03:41
@github-actions github-actions Bot added 🐛 fix Something is broken or doesn't work properly 📦 admin labels Sep 19, 2026
@greptile-apps

greptile-apps Bot commented Sep 19, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge and correctly restores cleanup for factory-owned runtimes without disposing shared instances.

Summary

This PR corrects ownership of factory-created managed runtimes at the shared Cloud Functions execution boundary.

  • Factory-form runtimes are disposed in finally blocks after run and runExit.
  • Caller-owned runtime instances remain alive until explicitly disposed by their owner.
  • Regression tests cover successful execution, failures and defects, instance reuse, and HTTP/callable wrapper integration.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Cloud Functions wrapper] --> B{Runtime form}
  B -->|Factory| C[Create ManagedRuntime per invocation]
  C --> D[Run Effect]
  D --> E[Dispose in finally]
  E --> F[Run layer finalizers]
  B -->|Instance| G[Run Effect on shared runtime]
  G --> H[Leave lifecycle to caller]
Loading

Reviews (1) · Last reviewed commit: "fix(admin): dispose factory-form Managed..."

@fwal fwal added this to the 1.0 milestone Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📦 admin 🐛 fix Something is broken or doesn't work properly

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Detail Bug] Admin functions: function-form ManagedRuntime created per call is never disposed (layer finalizers never run)

1 participant