diff --git a/CHANGELOG.md b/CHANGELOG.md index 26e368ff9..5347e3557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ For richer information consult the commit log on github with referenced pull req We do not include break-fix version release in this file. +### pg@8.7.0 + +- Add optional config to [pool](https://github.com/brianc/node-postgres/pull/2568) to allow process to exit if pool is idle. + +### pg-cursor@2.7.0 + +- Convert to [es6 class](https://github.com/brianc/node-postgres/pull/2553) +- Add support for promises [to cursor methods](https://github.com/brianc/node-postgres/pull/2554) + ### pg@8.6.0 - Better [SASL](https://github.com/brianc/node-postgres/pull/2436) error messages & more validation on bad configuration. diff --git a/packages/pg-cursor/index.js b/packages/pg-cursor/index.js index 8e8552be8..ddfb2b4ca 100644 --- a/packages/pg-cursor/index.js +++ b/packages/pg-cursor/index.js @@ -17,6 +17,7 @@ class Cursor extends EventEmitter { this._queue = [] this.state = 'initialized' this._result = new Result(this._conf.rowMode, this._conf.types) + this._Promise = this._conf.Promise || global.Promise this._cb = null this._rows = null this._portal = null @@ -198,38 +199,52 @@ class Cursor extends EventEmitter { } close(cb) { + let promise + + if (!cb) { + promise = new this._Promise((resolve, reject) => { + cb = (err) => (err ? reject(err) : resolve()) + }) + } + if (!this.connection || this.state === 'done') { - if (cb) { - return setImmediate(cb) - } else { - return - } + setImmediate(cb) + return promise } this._closePortal() this.state = 'done' - if (cb) { - this.connection.once('readyForQuery', function () { - cb() - }) - } + this.connection.once('readyForQuery', function () { + cb() + }) + + // Return the promise (or undefined) + return promise } read(rows, cb) { - if (this.state === 'idle' || this.state === 'submitted') { - return this._getRows(rows, cb) - } - if (this.state === 'busy' || this.state === 'initialized') { - return this._queue.push([rows, cb]) - } - if (this.state === 'error') { - return setImmediate(() => cb(this._error)) + let promise + + if (!cb) { + promise = new this._Promise((resolve, reject) => { + cb = (err, rows) => (err ? reject(err) : resolve(rows)) + }) } - if (this.state === 'done') { - return setImmediate(() => cb(null, [])) + + if (this.state === 'idle' || this.state === 'submitted') { + this._getRows(rows, cb) + } else if (this.state === 'busy' || this.state === 'initialized') { + this._queue.push([rows, cb]) + } else if (this.state === 'error') { + setImmediate(() => cb(this._error)) + } else if (this.state === 'done') { + setImmediate(() => cb(null, [])) } else { throw new Error('Unknown state: ' + this.state) } + + // Return the promise (or undefined) + return promise } } diff --git a/packages/pg-cursor/package.json b/packages/pg-cursor/package.json index 5607ea955..b85000aba 100644 --- a/packages/pg-cursor/package.json +++ b/packages/pg-cursor/package.json @@ -1,6 +1,6 @@ { "name": "pg-cursor", - "version": "2.6.0", + "version": "2.7.1", "description": "Query cursor extension for node-postgres", "main": "index.js", "directories": { @@ -18,7 +18,7 @@ "license": "MIT", "devDependencies": { "mocha": "^7.1.2", - "pg": "^8.6.0" + "pg": "^8.7.1" }, "peerDependencies": { "pg": "^8" diff --git a/packages/pg-cursor/test/close.js b/packages/pg-cursor/test/close.js index e63512abd..b34161a17 100644 --- a/packages/pg-cursor/test/close.js +++ b/packages/pg-cursor/test/close.js @@ -23,6 +23,17 @@ describe('close', function () { }) }) + it('can close a finished cursor a promise', function (done) { + const cursor = new Cursor(text) + this.client.query(cursor) + cursor.read(100, (err) => { + assert.ifError(err) + cursor.close().then(() => { + this.client.query('SELECT NOW()', done) + }) + }) + }) + it('closes cursor early', function (done) { const cursor = new Cursor(text) this.client.query(cursor) diff --git a/packages/pg-cursor/test/promises.js b/packages/pg-cursor/test/promises.js new file mode 100644 index 000000000..1635a1a8b --- /dev/null +++ b/packages/pg-cursor/test/promises.js @@ -0,0 +1,51 @@ +const assert = require('assert') +const Cursor = require('../') +const pg = require('pg') + +const text = 'SELECT generate_series as num FROM generate_series(0, 5)' + +describe('cursor using promises', function () { + beforeEach(function (done) { + const client = (this.client = new pg.Client()) + client.connect(done) + + this.pgCursor = function (text, values) { + return client.query(new Cursor(text, values || [])) + } + }) + + afterEach(function () { + this.client.end() + }) + + it('resolve with result', async function () { + const cursor = this.pgCursor(text) + const res = await cursor.read(6) + assert.strictEqual(res.length, 6) + }) + + it('reject with error', function (done) { + const cursor = this.pgCursor('select asdfasdf') + cursor.read(1).catch((err) => { + assert(err) + done() + }) + }) + + it('read multiple times', async function () { + const cursor = this.pgCursor(text) + let res + + res = await cursor.read(2) + assert.strictEqual(res.length, 2) + + res = await cursor.read(3) + assert.strictEqual(res.length, 3) + + res = await cursor.read(1) + assert.strictEqual(res.length, 1) + + res = await cursor.read(1) + assert.strictEqual(res.length, 0) + }) +}) diff --git a/packages/pg-pool/index.js b/packages/pg-pool/index.js index ef08f2965..1766c0030 100644 --- a/packages/pg-pool/index.js +++ b/packages/pg-pool/index.js @@ -83,6 +83,7 @@ class Pool extends EventEmitter { this.options.max = this.options.max || this.options.poolSize || 10 this.options.maxUses = this.options.maxUses || Infinity + this.options.allowExitOnIdle = this.options.allowExitOnIdle || false this.log = this.options.log || function () {} this.Client = this.options.Client || Client || require('pg').Client this.Promise = this.options.Promise || global.Promise @@ -136,6 +137,7 @@ class Pool extends EventEmitter { const idleItem = this._idle.pop() clearTimeout(idleItem.timeoutId) const client = idleItem.client + client.ref && client.ref() const idleListener = idleItem.idleListener return this._acquireClient(client, pendingItem, idleListener, false) @@ -323,6 +325,15 @@ class Pool extends EventEmitter { this.log('remove idle client') this._remove(client) }, this.options.idleTimeoutMillis) + + if (this.options.allowExitOnIdle) { + // allow Node to exit if this is all that's left + tid.unref() + } + } + + if (this.options.allowExitOnIdle) { + client.unref() } this._idle.push(new IdleItem(client, idleListener, tid)) diff --git a/packages/pg-pool/package.json b/packages/pg-pool/package.json index b92e7df90..d479ae55f 100644 --- a/packages/pg-pool/package.json +++ b/packages/pg-pool/package.json @@ -1,6 +1,6 @@ { "name": "pg-pool", - "version": "3.3.0", + "version": "3.4.1", "description": "Connection pool for node-postgres", "main": "index.js", "directories": { diff --git a/packages/pg-pool/test/idle-timeout-exit.js b/packages/pg-pool/test/idle-timeout-exit.js new file mode 100644 index 000000000..1292634a8 --- /dev/null +++ b/packages/pg-pool/test/idle-timeout-exit.js @@ -0,0 +1,16 @@ +// This test is meant to be spawned from idle-timeout.js +if (module === require.main) { + const allowExitOnIdle = process.env.ALLOW_EXIT_ON_IDLE === '1' + const Pool = require('../index') + + const pool = new Pool({ idleTimeoutMillis: 200, ...(allowExitOnIdle ? { allowExitOnIdle: true } : {}) }) + pool.query('SELECT NOW()', (err, res) => console.log('completed first')) + pool.on('remove', () => { + console.log('removed') + done() + }) + + setTimeout(() => { + pool.query('SELECT * from generate_series(0, 1000)', (err, res) => console.log('completed second')) + }, 50) +} diff --git a/packages/pg-pool/test/idle-timeout.js b/packages/pg-pool/test/idle-timeout.js index fd9fba4a4..0bb097565 100644 --- a/packages/pg-pool/test/idle-timeout.js +++ b/packages/pg-pool/test/idle-timeout.js @@ -4,6 +4,8 @@ const expect = require('expect.js') const describe = require('mocha').describe const it = require('mocha').it +const { fork } = require('child_process') +const path = require('path') const Pool = require('../') @@ -84,4 +86,33 @@ describe('idle timeout', () => { return pool.end() }) ) + + it('unrefs the connections and timeouts so the program can exit when idle when the allowExitOnIdle option is set', function (done) { + const child = fork(path.join(__dirname, 'idle-timeout-exit.js'), [], { + silent: true, + env: { ...process.env, ALLOW_EXIT_ON_IDLE: '1' }, + }) + let result = '' + child.stdout.setEncoding('utf8') + child.stdout.on('data', (chunk) => (result += chunk)) + child.on('error', (err) => done(err)) + child.on('close', () => { + expect(result).to.equal('completed first\ncompleted second\n') + done() + }) + }) + + it('keeps old behavior when allowExitOnIdle option is not set', function (done) { + const child = fork(path.join(__dirname, 'idle-timeout-exit.js'), [], { + silent: true, + }) + let result = '' + child.stdout.setEncoding('utf8') + child.stdout.on('data', (chunk) => (result += chunk)) + child.on('error', (err) => done(err)) + child.on('close', () => { + expect(result).to.equal('completed first\ncompleted second\nremoved\n') + done() + }) + }) }) diff --git a/packages/pg-query-stream/package.json b/packages/pg-query-stream/package.json index d01b18d86..5f332e8cd 100644 --- a/packages/pg-query-stream/package.json +++ b/packages/pg-query-stream/package.json @@ -1,6 +1,6 @@ { "name": "pg-query-stream", - "version": "4.1.0", + "version": "4.2.1", "description": "Postgres query result returned as readable stream", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -37,13 +37,13 @@ "concat-stream": "~1.0.1", "eslint-plugin-promise": "^3.5.0", "mocha": "^7.1.2", - "pg": "^8.6.0", + "pg": "^8.7.1", "stream-spec": "~0.3.5", "stream-tester": "0.0.5", "ts-node": "^8.5.4", "typescript": "^4.0.3" }, "dependencies": { - "pg-cursor": "^2.6.0" + "pg-cursor": "^2.7.1" } } diff --git a/packages/pg/lib/client.js b/packages/pg/lib/client.js index 1e1e83374..589aa9f84 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -577,6 +577,14 @@ class Client extends EventEmitter { return result } + ref() { + this.connection.ref() + } + + unref() { + this.connection.unref() + } + end(cb) { this._ending = true diff --git a/packages/pg/lib/connection.js b/packages/pg/lib/connection.js index 7d45de2b7..ebb2f099d 100644 --- a/packages/pg/lib/connection.js +++ b/packages/pg/lib/connection.js @@ -177,6 +177,14 @@ class Connection extends EventEmitter { this._send(syncBuffer) } + ref() { + this.stream.ref() + } + + unref() { + this.stream.unref() + } + end() { // 0x58 = 'X' this._ending = true diff --git a/packages/pg/lib/native/client.js b/packages/pg/lib/native/client.js index 6cf800d0e..d1faeb3d8 100644 --- a/packages/pg/lib/native/client.js +++ b/packages/pg/lib/native/client.js @@ -285,6 +285,9 @@ Client.prototype.cancel = function (query) { } } +Client.prototype.ref = function () {} +Client.prototype.unref = function () {} + Client.prototype.setTypeParser = function (oid, format, parseFn) { return this._types.setTypeParser(oid, format, parseFn) } diff --git a/packages/pg/package.json b/packages/pg/package.json index af71629f3..930a7d928 100644 --- a/packages/pg/package.json +++ b/packages/pg/package.json @@ -1,6 +1,6 @@ { "name": "pg", - "version": "8.6.0", + "version": "8.7.1", "description": "PostgreSQL client - pure javascript & libpq with the same API", "keywords": [ "database", @@ -23,7 +23,7 @@ "buffer-writer": "2.0.0", "packet-reader": "1.0.0", "pg-connection-string": "^2.5.0", - "pg-pool": "^3.3.0", + "pg-pool": "^3.4.1", "pg-protocol": "^1.5.0", "pg-types": "^2.1.0", "pgpass": "1.x" diff --git a/packages/pg/test/integration/client/connection-timeout-tests.js b/packages/pg/test/integration/client/connection-timeout-tests.js index 843fa95bb..7a3ee4447 100644 --- a/packages/pg/test/integration/client/connection-timeout-tests.js +++ b/packages/pg/test/integration/client/connection-timeout-tests.js @@ -7,13 +7,13 @@ const suite = new helper.Suite() const options = { host: 'localhost', - port: 54321, + port: Math.floor(Math.random() * 2000) + 2000, connectionTimeoutMillis: 2000, user: 'not', database: 'existing', } -const serverWithConnectionTimeout = (timeout, callback) => { +const serverWithConnectionTimeout = (port, timeout, callback) => { const sockets = new Set() const server = net.createServer((socket) => { @@ -47,11 +47,11 @@ const serverWithConnectionTimeout = (timeout, callback) => { } } - server.listen(options.port, options.host, () => callback(closeServer)) + server.listen(port, options.host, () => callback(closeServer)) } suite.test('successful connection', (done) => { - serverWithConnectionTimeout(0, (closeServer) => { + serverWithConnectionTimeout(options.port, 0, (closeServer) => { const timeoutId = setTimeout(() => { throw new Error('Client should have connected successfully but it did not.') }, 3000) @@ -67,12 +67,13 @@ suite.test('successful connection', (done) => { }) suite.test('expired connection timeout', (done) => { - serverWithConnectionTimeout(options.connectionTimeoutMillis * 2, (closeServer) => { + const opts = { ...options, port: 54322 } + serverWithConnectionTimeout(opts.port, opts.connectionTimeoutMillis * 2, (closeServer) => { const timeoutId = setTimeout(() => { throw new Error('Client should have emitted an error but it did not.') }, 3000) - const client = new helper.Client(options) + const client = new helper.Client(opts) client .connect() .then(() => client.end()) diff --git a/packages/pg/test/integration/client/network-partition-tests.js b/packages/pg/test/integration/client/network-partition-tests.js index 993396401..2ac100dff 100644 --- a/packages/pg/test/integration/client/network-partition-tests.js +++ b/packages/pg/test/integration/client/network-partition-tests.js @@ -11,6 +11,7 @@ var Server = function (response) { this.response = response } +let port = 54321 Server.prototype.start = function (cb) { // this is our fake postgres server // it responds with our specified response immediatley after receiving every buffer @@ -39,7 +40,7 @@ Server.prototype.start = function (cb) { }.bind(this) ) - var port = 54321 + port = port + 1 var options = { host: 'localhost', diff --git a/yarn.lock b/yarn.lock index e579f984e..bc5330a1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3066,9 +3066,9 @@ growl@1.10.5: integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== handlebars@^4.0.1, handlebars@^4.7.6: - version "4.7.6" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" - integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== + version "4.7.7" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== dependencies: minimist "^1.2.5" neo-async "^2.6.0" @@ -5647,9 +5647,9 @@ sshpk@^1.7.0: tweetnacl "~0.14.0" ssri@^6.0.0, ssri@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" - integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== + version "6.0.2" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" + integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q== dependencies: figgy-pudding "^3.5.1" @@ -5868,9 +5868,9 @@ table@^5.2.3: string-width "^3.0.0" tar@^4.4.10, tar@^4.4.12, tar@^4.4.8: - version "4.4.13" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" - integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== + version "4.4.15" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.15.tgz#3caced4f39ebd46ddda4d6203d48493a919697f8" + integrity sha512-ItbufpujXkry7bHH9NpQyTXPbJ72iTlXgkBAYsAjDXk3Ds8t/3NfO5P4xZGy7u+sYuQUbimgzswX4uQIEeNVOA== dependencies: chownr "^1.1.1" fs-minipass "^1.2.5" @@ -6125,9 +6125,9 @@ typescript@^4.0.3: integrity sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg== uglify-js@^3.1.4: - version "3.11.1" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.11.1.tgz#32d274fea8aac333293044afd7f81409d5040d38" - integrity sha512-OApPSuJcxcnewwjSGGfWOjx3oix5XpmrK9Z2j0fTRlHGoZ49IU6kExfZTM0++fCArOOCet+vIfWwFHbvWqwp6g== + version "3.13.5" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.5.tgz#5d71d6dbba64cf441f32929b1efce7365bb4f113" + integrity sha512-xtB8yEqIkn7zmOyS2zUNBsYCBRhDkvlNxMMY2smuJ/qA8NCHeQvKCF3i9Z4k8FJH4+PJvZRtMrPynfZ75+CSZw== uid-number@0.0.6: version "0.0.6" @@ -6388,9 +6388,9 @@ xtend@^4.0.0, xtend@~4.0.1: integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + version "4.0.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" + integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: version "3.1.1"