Skip to content

fix(graphile-cache): prevent double-disposal of PostGraphile instances during shutdown#918

Open
pyramation wants to merge 2 commits intomainfrom
devin/1774643134-fix-postgraphile-double-release
Open

fix(graphile-cache): prevent double-disposal of PostGraphile instances during shutdown#918
pyramation wants to merge 2 commits intomainfrom
devin/1774643134-fix-postgraphile-double-release

Conversation

@pyramation
Copy link
Copy Markdown
Contributor

@pyramation pyramation commented Mar 27, 2026

Summary

Fixes the PostGraphile instance has been released error during test teardown (and any closeAllCaches call) by replacing the disposedKeys string-keyed Set with a WeakSet<GraphileCacheEntry> keyed on entry object references.

Root cause: disposeEntry() added the cache key to disposedKeys as a double-disposal guard, but then deleted it in the finally block after disposal completed. When closeAllCaches() subsequently called graphileCache.clear(), the LRU dispose callback re-invoked disposeEntry() for the same entry — but the guard key was already gone, so it attempted pgl.release() on an already-released instance.

Fix: Track disposal by entry object reference (WeakSet) instead of key string. This eliminates both the race (no finally cleanup needed) and a theoretical key-reuse edge case where different entry objects could share the same cache key string.

Updates since last revision

Added a regression test suite in graphile/graphile-cache/__tests__/graphile-cache.test.ts that:

  • Mocks pgl.release() to throw on double-call (matching real PostGraphile behavior)
  • Verifies closeAllCaches() calls release() exactly once per entry, not twice
  • Verifies single LRU eviction also disposes exactly once
  • Covers the empty-cache edge case

CI server-test logs were also checked — the Error disposing PostGraphile message no longer appears in teardown output.

Review & Testing Checklist for Human

  • Trace the closeAllCaches flow to confirm the race is resolved: disposeEntry marks the entry in the WeakSetPromise.allSettled completes → graphileCache.clear() triggers LRU dispose callback → disposeEntry sees entry in WeakSet and no-ops
  • Verify removal of the finally block is safe — the WeakSet needs no manual cleanup (entries are GC'd when unreferenced), unlike the old Set<string> which leaked stale keys without the finally. Note: if disposeEntry errors partway through, the entry stays in the WeakSet, preventing a retry — confirm this is acceptable (a failed release would likely fail again anyway)
  • Run the server-test suite (cd graphql/server-test && pnpm test) and confirm the Error disposing PostGraphile log no longer appears in output

Notes

  • The error was caught and logged (non-fatal), so CI was green even before the fix — the real signal is absence of the error log during teardown
  • The regression test mocks pg-cache and @pgpmjs/logger to run in isolation; the full integration path is covered by the existing server-test CI job
  • Two files changed: graphile-cache.ts (fix, net −1 line) and new graphile-cache.test.ts (regression test)

Link to Devin session: https://app.devin.ai/sessions/c36c53a01f67404eb07113d24a4b7c67
Requested by: @pyramation

…s during shutdown

Replace key-based disposedKeys Set with entry-based WeakSet to track
disposed PostGraphile instances. The previous approach used a string key
guard with a finally block that deleted the key after disposal completed.
This caused a race condition in closeAllCaches():

1. closeAllCaches() manually calls disposeEntry() which adds key to
   disposedKeys, releases pgl, then deletes key in finally block
2. graphileCache.clear() triggers LRU dispose callback for same entry
3. Key is no longer in disposedKeys, so disposeEntry() tries to
   release the already-released PostGraphile instance
4. PostGraphile throws 'instance has been released'

Using a WeakSet keyed on the entry object reference:
- Prevents double-disposal without needing cleanup in finally block
- Avoids key-reuse edge cases (same cache key, different entry object)
- Allows GC to clean up naturally when entries are no longer referenced
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Adds unit tests that verify:
- pgl.release() is called exactly once per entry during closeAllCaches()
- closeAllCaches() does not throw when LRU dispose callback fires after
  manual disposal (the original bug scenario)
- Empty cache is handled gracefully
- Single LRU eviction disposes exactly once
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