Repository navigation
fix(pg-pool): avoid stale async context on client reuse. #3838
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bizob2828
wants to merge
2
commits into
brianc:master
Choose a base branch
from
bizob2828:async-context-fix
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| 'use strict' | ||
| const Pool = require('../') | ||
| const expect = require('expect.js') | ||
| const { AsyncLocalStorage } = require('async_hooks') | ||
|
|
||
| describe('async context on client reuse', () => { | ||
| it('resets the socket handle async context when handing out an already-connected client', async () => { | ||
| // Force every connect() after the first to reuse the SAME client. | ||
| const pool = new Pool({ max: 1 }) | ||
|
|
||
| try { | ||
| const first = await pool.connect() | ||
| const handle = first.connection.stream._handle | ||
| expect(handle).to.be.ok() | ||
| expect(typeof handle.asyncReset).to.equal('function') | ||
|
|
||
| let resetCalls = 0 | ||
| const originalAsyncReset = handle.asyncReset | ||
| handle.asyncReset = function (...args) { | ||
| resetCalls++ | ||
| return originalAsyncReset.apply(this, args) | ||
| } | ||
|
|
||
| first.release() | ||
|
|
||
| const second = await pool.connect() | ||
| try { | ||
| expect(second).to.equal(first) | ||
| expect(resetCalls).to.equal(1) | ||
| } finally { | ||
| second.release() | ||
| } | ||
| } finally { | ||
| await pool.end() | ||
| } | ||
| }) | ||
|
|
||
| it('clears the legacy kResourceStore context when handing out a reused client', async () => { | ||
| // On Node <24, AsyncLocalStorage stamps a resource with an internal | ||
| // kResourceStore symbol at creation time; this only happens while an | ||
| // ALS context is actually active, so one has to be running here for | ||
| // the symbol to exist at all. | ||
| const als = new AsyncLocalStorage() | ||
| const pool = new Pool({ max: 1 }) | ||
|
|
||
| try { | ||
| const first = await als.run('first-connect', () => pool.connect()) | ||
| const handle = first.connection.stream._handle | ||
| const resourceStoreSymbol = Object.getOwnPropertySymbols(handle).find((symbol) => | ||
| symbol.toString().includes('kResourceStore') | ||
| ) | ||
|
|
||
| if (!resourceStoreSymbol) { | ||
| // Node 24+ tracks async context differently and never stamps this | ||
| // symbol in the first place, so there's nothing to clear. | ||
| first.release() | ||
| return | ||
| } | ||
|
|
||
| expect(handle[resourceStoreSymbol]).to.equal('first-connect') | ||
|
|
||
| first.release() | ||
|
|
||
| const second = await pool.connect() | ||
| try { | ||
| expect(second).to.equal(first) | ||
| expect(handle[resourceStoreSymbol]).to.equal(undefined) | ||
| } finally { | ||
| second.release() | ||
| } | ||
| } finally { | ||
| await pool.end() | ||
| } | ||
| }) | ||
|
|
||
| it('does not leak the connecting context into a later query on a reused client', (done) => { | ||
| const als = new AsyncLocalStorage() | ||
| const pool = new Pool({ max: 1 }) | ||
|
|
||
| als.run('first-context', () => { | ||
| pool.query('SELECT 1', (err) => { | ||
| if (err) { | ||
| return pool.end(() => done(err)) | ||
| } | ||
|
|
||
| als.run('second-context', () => { | ||
| pool.query('SELECT 1', (err2) => { | ||
| if (err2) { | ||
| return pool.end(() => done(err2)) | ||
| } | ||
|
|
||
| let assertionError = null | ||
| try { | ||
| expect(als.getStore()).to.equal('second-context') | ||
| } catch (e) { | ||
| assertionError = e | ||
| } | ||
| pool.end(() => done(assertionError)) | ||
| }) | ||
| }) | ||
| }) | ||
| }) | ||
| }) | ||
| }) | ||
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this do anything that isn’t already covered by the other two tests? Don’t want to reach into Node internals unnecessarily, even for test code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a test for Node < 24 and to cover this code. Basically in Node 24 AsyncLocalStorage now relies on AsyncContextFrame, see
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the link. I won’t get the chance to look into it properly anytime soon, but that really seems like the kind of code that should not be added.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand. That code is necessary to handle the issue in node < 24. looking at your engines stanza, you support 16+. I can try to provide another minimal repro to demonstrate it better. FWIW there are several other issues with retaining objects during this pool connection reuse in versions less than 24 but I had to limit this PR as the others touched the pg client and some other package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reaching into Node implementation details, especially in this way, is rarely the best or only way to solve a problem. In the case – which seems like it should be unlikely, again with the qualification that I haven’t looked into it – that it truly is the only way to solve the problem, it might be better left as a workaround to be implemented by users of pg on Node <24 who also use
AsyncLocalStorage.Updating symbol-keyed properties by string representation is really questionable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fair. Please still dig into it though. I'll check/explore alternate options or as you said, just document the workaround. Anyone that uses any observability tool will be implicitly using als. But the agents could instrument this if it is indeed an actual issue. Most, if not all of these changes mirror what HttpAgent is doing with sockets to avoid this exact issue.