A custom socket without readyState is never closed: an expired connection leaves the pool, and sql.end() never resolves
Maintainers usually reply within 7 days
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- javascript, node.js, postgresql
Research direction
Start with the connection shutdown logic in src/connection.js around the linked terminate() and end() lines, then inspect the custom socket tests. Reproduce with a plain Duplex stream and verify that ending the connection closes the stream and lets the pool open another socket. Done when sql.end() resolves and queries continue after max_lifetime expires.
Written by the indexing model from the issue text.
Description
The custom socket example resolves with the channel ssh2's forwardOut yields, a Duplex with no readyState. A connection never closes such a socket: terminate() sends Terminate and ends the socket only when its readyState is 'open',
and end() then waits for a close that never comes:
So:
- a connection whose
max_lifetimeexpires — 30 to 60 minutes by default — never closes and never makes room for another, so once every connection has expired, queries wait forever; sql.end()never resolves, andsql.end({ timeout })resolves with each socket still open.
Reproduction. With postgres 3.4.9, node repro.mjs (and bun repro.mjs) prints sql.end() still pending after 2 s; a duplex stream stands in for the server:
import { Duplex } from 'node:stream'
import postgres from 'postgres'
// Stands in for the server the way ssh2's forwardOut channel would: a duplex stream that
// answers the startup message and simple queries, and ends once the client ends it.
const message = (type, body) => {
const header = Buffer.alloc(5)
header.write(type)
header.writeInt32BE(body.length + 4, 1)
return Buffer.concat([header, body])
}
const ready = message('Z', Buffer.from('I'))
function server() {
let started = false
const stream = new Duplex({
read() {},
write(chunk, _, callback) {
const reply = !started
? Buffer.concat([message('R', Buffer.alloc(4)), ready])
: chunk[0] === 0x51 // Q
? Buffer.concat([message('C', Buffer.from('SELECT 0\0')), ready])
: null
started = true
reply && setImmediate(() => stream.push(reply))
callback()
},
final(callback) {
stream.push(null)
callback()
}
})
return stream
}
let step = 'select 1'
const guard = setTimeout(() => {
console.log(step + ' still pending after 2 s')
process.exit(1)
}, 2000)
const sql = postgres({ max: 1, fetch_types: false, socket: server })
await sql.unsafe('select 1').simple()
step = 'sql.end()'
await sql.end()
clearTimeout(guard)
console.log('sql.end() resolved')
With max: 1, max_lifetime: 1, a query sent once the lifetime has expired stays pending the same way, and the pool never asks for a second socket.
Fix. Reading the stream's own state when a socket has no readyState closes the connection in both cases, and the pool opens the next one:
- socket.readyState === 'open' && socket.end(b().X().end())
+ if (socket.readyState ? socket.readyState === 'open' : socket.writable)
+ socket.end(b().X().end())
net.Socket and the Deno and Cloudflare polyfills all set readyState, so they keep today's behaviour. I can open a PR with this and a test beside Custom socket that hands the connection a plain Duplex.
- Dominant language
- JavaScript
- Stars
- 8.7k
- Forks
- 389
- PR merge metrics
- No merged PRs in 30d
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from porsager/postgres
-
sql.end() never resolves when a reserved connection is released after end() was calledPossibly taken @whoekage claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 7 days
-
Cancelled query leaves stale row index; next query on the connection returns leading empty slotsPossibly taken @pislm claimed this 5 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
Maintainers usually reply within 7 days
-
Reconnect after a close with a pending write never sends StartupMessage: stale nextWriteTimer/chunk make every statement fail CONNECT_TIMEOUTPossibly taken @Vengron claimed this 1 day ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 7 days
-
Connection reconnect rejects the next query with a stale ErrorResponse from before the socket closedOpen
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
Maintainers usually reply within 7 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 7 days
All issues in porsager/postgres
Similar issues
-
BUG
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
Maintainers usually reply within 1 day
-
type/bug
Difficulty 1/5 Under an hour Newbie friendliness 75/100
flarum/framework#5231 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
apache/rocketmq-dashboard#6896 ·
Maintainers usually reply within 3 days
-
bug good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Maintainers usually reply within 1 day
-
[workshop-sync] workshop/side-quest-17-06-output-injection.md: stale add_comment.cjs line referencesOpendocumentation
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
githubnext/gh-aw-workshop#4679 ·
Maintainers usually reply within 1 day