Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/pg/lib/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ class Connection extends EventEmitter {
}

sync() {
this._ending = true
// Sync is the extended-query protocol barrier, not a disconnect.
// Only end()/Terminate (and connect-timeout teardown) should set _ending.
this._send(syncBuffer)
}

Expand Down
62 changes: 62 additions & 0 deletions packages/pg/test/integration/gh-issues/3772-probe-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use strict'

const helper = require('./../test-helper')
const assert = require('assert')

const suite = new helper.Suite()

suite.test('probe: real PostgreSQL backend termination after Sync', function (done) {
const client = new helper.Client({ pipeline: true })
const killer = new helper.Client()
const events = []
let finished = false
let timer

const finish = function (err) {
if (finished) return
finished = true
clearTimeout(timer)
killer.end(() => client.end(() => done(err)))
}

client.on('error', (err) => {
events.push(`client.error:${err.code || err.message}`)
console.log('3772 probe events:', events)
if (!finished) finish()
})

client.on('end', () => {
events.push('client.end')
console.log('3772 probe events:', events)
if (!finished) finish()
})

client.connect((err) => {
if (err) return done(err)

client.query('SELECT pg_backend_pid() AS pid', (err, result) => {
if (err) return done(err)
const pid = result.rows[0].pid

// Ensure the parameterized query uses the extended protocol and sends Sync.
client.query({
text: 'SELECT $1::int AS value',
values: [1],
})

killer.connect((err) => {
if (err) return done(err)

killer.query('SELECT pg_terminate_backend($1)', [pid], (err, result) => {
if (err) return done(err)
assert.equal(result.rows[0].pg_terminate_backend, true)
})
})
})
})

timer = setTimeout(() => {
console.log('3772 probe timeout events:', events)
finish(new Error(`timed out waiting for connection termination; events=${events.join(',')}`))
}, 2000)
})
56 changes: 56 additions & 0 deletions packages/pg/test/integration/gh-issues/3772-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use strict'

const helper = require('./../test-helper')
const assert = require('assert')

const suite = new helper.Suite()

suite.test('real PostgreSQL backend termination after Sync surfaces connection failure', function (done) {
const client = new helper.Client({ pipeline: true })
const killer = new helper.Client()
let finished = false
let timer

const finish = function (err) {
if (finished) return
finished = true
clearTimeout(timer)
killer.end(() => client.end(() => done(err)))
}

client.on('error', function (err) {
assert(err instanceof Error)
finish()
})

client.on('end', function () {
if (!finished) finish()
})

client.connect(function (err) {
if (err) return done(err)

client.query('SELECT pg_backend_pid() AS pid', function (err, result) {
if (err) return done(err)
const pid = result.rows[0].pid

// Force the extended-query protocol, which sends Sync after the query.
client.query({
text: 'SELECT $1::int AS value',
values: [1],
})

killer.connect(function (err) {
if (err) return done(err)
killer.query('SELECT pg_terminate_backend($1)', [pid], function (err, result) {
if (err) return done(err)
assert.equal(result.rows[0].pg_terminate_backend, true)
})
})
})
})

timer = setTimeout(function () {
finish(new Error('timed out waiting for PostgreSQL connection termination'))
}, 2000)
})
22 changes: 22 additions & 0 deletions packages/pg/test/unit/connection/error-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@ suite.test('connection emits ECONNRESET errors during normal operation', functio
con.stream.emit('error', e)
})

suite.test('connection emits ECONNRESET errors after Sync (Sync is not disconnect)', function (done) {
const con = new Connection({ stream: new MemoryStream() })
con.connect()
// Extended-query Sync used to incorrectly set _ending and swallow resets (#3769)
con.sync()
assert.equal(con._ending, false)
assert.emits(con, 'error', function (err) {
assert.equal(err.code, 'ECONNRESET')
done()
})
const e = new Error('Connection Reset')
e.code = 'ECONNRESET'
con.stream.emit('error', e)
})

suite.test('connection does not set _ending when calling sync()', function () {
const con = new Connection({ stream: new MemoryStream() })
con.connect()
con.sync()
assert.equal(con._ending, false)
})

suite.test('connection does not emit ECONNRESET errors during disconnect', function (done) {
const con = new Connection({ stream: new MemoryStream() })
con.connect()
Expand Down