@steadybrew/postgresjs 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1121,6 +1121,58 @@ There are no guarantees about queries executing in order unless using a transact
1121
1121
 
1122
1122
  Since this library automatically creates prepared statements, it also has a default max lifetime for connections to prevent memory bloat on the database itself. This is a random interval for each connection between 45 and 90 minutes. This allows multiple connections to independently come up and down without affecting the service.
1123
1123
 
1124
+ ### Pool stats
1125
+
1126
+ `sql.stats()` returns a snapshot of the pool as a plain object, ready to log, serialise from a health endpoint or map to metrics gauges:
1127
+
1128
+ ```js
1129
+ sql.stats()
1130
+ // { max: 10, totalCount: 7, idleCount: 2, waitingCount: 3,
1131
+ // activeCount: 4, availableCount: 5,
1132
+ // busyCount: 3, reservedCount: 1, connectingCount: 1, closingCount: 0,
1133
+ // executingCount: 4, listenCount: 1, subscribeCount: 0 }
1134
+ ```
1135
+
1136
+ | Field | Meaning |
1137
+ |---|---|
1138
+ | `max` | The `max` option |
1139
+ | `totalCount` | Connections that exist: connecting, idle or in use |
1140
+ | `idleCount` | Connections with a live session that are not in use. A connection released while its reserved handle still has queries in flight is idle with `executingCount` above 0 |
1141
+ | `waitingCount` | Issued queries and `reserve()` or `begin()` calls that are not running yet: those in the pool queue, those held by a connection that is still opening, those pipelined behind a connection's running query, and those a reserved handle or transaction has queued behind its own connection |
1142
+ | `activeCount` | `busyCount + reservedCount`, connections in use |
1143
+ | `availableCount` | `idleCount` plus the slots the pool can still open (`max - totalCount - closingCount`): connections a query can get without waiting behind other work |
1144
+ | `busyCount` | Pool connections running queries, including connections at the pipeline limit |
1145
+ | `reservedCount` | Connections held by `reserve()` or `begin()` |
1146
+ | `connectingCount` | Connections opening a session |
1147
+ | `closingCount` | Connections draining or closing. They are not in `totalCount`, but still hold a pool slot until closed |
1148
+ | `executingCount` | Queries the server is running right now, at most one per connection, on pool and reserved connections alike. It includes the queries the driver runs while a connection starts, and a `COPY` for its whole duration |
1149
+ | `listenCount` | Connections held by the internal `sql.listen()` pool (0 or 1) |
1150
+ | `subscribeCount` | Connections held by the internal `sql.subscribe()` pool (0 or 1) |
1151
+
1152
+ `totalCount` and `idleCount` have the same meaning as in node-postgres. `waitingCount` is node-postgres's `waitingCount` extended to queries pipelined on connections and queued in leases, and it includes queries sent with `sql` directly, as `pool.query()` does there. Once the pool is at `max`, Postgres.js pipelines new queries onto busy connections, so waiting work can sit on a connection rather than in the pool queue; `waitingCount` counts it too, so `executingCount + waitingCount` covers all issued work that has not finished. Every snapshot satisfies `totalCount === idleCount + busyCount + reservedCount + connectingCount` and `totalCount + closingCount <= max`, the remainder being slots the pool has not opened.
1153
+
1154
+ `listenCount` and `subscribeCount` are counted outside `max`, and the other fields only describe the `max` pool connections. A reserved connection's `stats()` returns the same snapshot, but `sql` inside `sql.begin()` and savepoints has no `stats()`, so call it on the main `sql`.
1155
+
1156
+ The pool is saturated when `waitingCount > 0 && availableCount === 0`: every connection is in use and more work is waiting. Saturation alone is not a reason to fail a health check, because a single pipelined query on a fully used pool, including a transaction pipelining its own statements, is enough to meet it. A health endpoint should compare `waitingCount` against a limit the application chooses:
1157
+
1158
+ ```js
1159
+ import http from 'node:http'
1160
+
1161
+ const MAX_WAITING = 100
1162
+
1163
+ http.createServer((req, res) => {
1164
+ const stats = sql.stats()
1165
+ const overloaded = stats.availableCount === 0 && stats.waitingCount > MAX_WAITING
1166
+
1167
+ res.writeHead(overloaded ? 503 : 200, { 'content-type': 'application/json' })
1168
+ res.end(JSON.stringify(stats))
1169
+ }).listen(8080)
1170
+ ```
1171
+
1172
+ How much waiting work is acceptable depends on the query durations and the latency the application can tolerate, so the limit has no universal value.
1173
+
1174
+ Change events and a running total of finished queries are planned together with the diagnostics events, so for now take snapshots on a timer or on request.
1175
+
1124
1176
  ### Connection timeout
1125
1177
 
1126
1178
  By default, connections will not close until `.end()` is called. However, it may be useful to have them close automatically when:
@@ -224,6 +224,9 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
224
224
  },
225
225
  expired: () => idleTimer.expired() || lifeTimer.expired(),
226
226
  owner: null,
227
+ get active() { return query ? 1 : 0 },
228
+ get pipelined() { return sent.length },
229
+ get acquiring() { return acquisition && !acquisition.owner.cancelled ? 1 : 0 },
227
230
  count: 0,
228
231
  id
229
232
  }
@@ -529,9 +532,9 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
529
532
  && !q.cursorFn
530
533
  && sent.length < max_pipeline
531
534
  } catch (error) {
532
- sent.length === 0 && write(Sync)
533
- errored(error)
534
- return true
535
+ sent.remove(q) || write(Sync)
536
+ queryError(q, error)
537
+ return sent.length < max_pipeline
535
538
  }
536
539
  }
537
540
 
package/cf/src/index.js CHANGED
@@ -82,6 +82,7 @@ function Postgres(a, b) {
82
82
  PostgresError,
83
83
  options,
84
84
  reserve,
85
+ stats,
85
86
  listen,
86
87
  begin,
87
88
  close,
@@ -151,6 +152,56 @@ function Postgres(a, b) {
151
152
  }
152
153
  }
153
154
 
155
+ function stats() {
156
+ let idleCount = 0
157
+ , busyCount = 0
158
+ , reservedCount = 0
159
+ , connectingCount = 0
160
+ , closingCount = 0
161
+ , executingCount = 0
162
+ , waitingCount = queries.length
163
+
164
+ for (const c of connections) {
165
+ executingCount += c.active
166
+ waitingCount += c.pipelined + c.acquiring + (c.owner ? c.owner.queue.length : 0)
167
+ c.queue === open
168
+ ? idleCount++
169
+ : c.queue === connecting
170
+ ? connectingCount++
171
+ : c.queue === ended
172
+ ? closingCount++
173
+ : c.queue === reserved
174
+ ? reservedCount++
175
+ : (c.queue === busy || c.queue === full) && (c.owner ? reservedCount++ : busyCount++)
176
+ }
177
+
178
+ const totalCount = idleCount + busyCount + reservedCount + connectingCount
179
+
180
+ return {
181
+ max: options.max,
182
+ totalCount,
183
+ idleCount,
184
+ waitingCount,
185
+ activeCount: busyCount + reservedCount,
186
+ availableCount: idleCount + options.max - totalCount - closingCount,
187
+ busyCount,
188
+ reservedCount,
189
+ connectingCount,
190
+ closingCount,
191
+ executingCount,
192
+ listenCount: pooled(listen.sql),
193
+ subscribeCount: pooled(subscribe.sql)
194
+ }
195
+ }
196
+
197
+ function pooled(sql) {
198
+ if (!sql)
199
+ return 0
200
+
201
+ const s = sql.stats()
202
+ return s.totalCount + s.closingCount
203
+ }
204
+
154
205
  async function listen(name, fn, onlisten) {
155
206
  const listener = { fn, onlisten }
156
207
 
@@ -245,7 +296,7 @@ function Postgres(a, b) {
245
296
  const c = l.connection
246
297
  l.state === expected
247
298
  ? c.queue === full
248
- ? l.queue.push(q)
299
+ ? (q.queue = l.queue, l.queue.push(q))
249
300
  : c.execute(q) || move(c, full)
250
301
  : q.reject(leaseError(l.state))
251
302
  }
@@ -276,6 +327,7 @@ function Postgres(a, b) {
276
327
  const l = await lease(false)
277
328
  const sql = Sql(q => send(l, q))
278
329
  sql.release = () => settle(l, Lease.Released)
330
+ sql.stats = stats
279
331
  return sql
280
332
  }
281
333
 
@@ -436,7 +488,7 @@ function Postgres(a, b) {
436
488
  if (query.state) {
437
489
  query.cancelled = { resolve, reject }
438
490
  } else {
439
- queries.remove(query)
491
+ (query.queue || queries).remove(query)
440
492
  query.cancelled = true
441
493
  query.reject(Errors.generic('57014', 'canceling statement due to user request'))
442
494
  resolve()
@@ -222,6 +222,9 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
222
222
  },
223
223
  expired: () => idleTimer.expired() || lifeTimer.expired(),
224
224
  owner: null,
225
+ get active() { return query ? 1 : 0 },
226
+ get pipelined() { return sent.length },
227
+ get acquiring() { return acquisition && !acquisition.owner.cancelled ? 1 : 0 },
225
228
  count: 0,
226
229
  id
227
230
  }
@@ -527,9 +530,9 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
527
530
  && !q.cursorFn
528
531
  && sent.length < max_pipeline
529
532
  } catch (error) {
530
- sent.length === 0 && write(Sync)
531
- errored(error)
532
- return true
533
+ sent.remove(q) || write(Sync)
534
+ queryError(q, error)
535
+ return sent.length < max_pipeline
533
536
  }
534
537
  }
535
538
 
package/cjs/src/index.js CHANGED
@@ -81,6 +81,7 @@ function Postgres(a, b) {
81
81
  PostgresError,
82
82
  options,
83
83
  reserve,
84
+ stats,
84
85
  listen,
85
86
  begin,
86
87
  close,
@@ -150,6 +151,56 @@ function Postgres(a, b) {
150
151
  }
151
152
  }
152
153
 
154
+ function stats() {
155
+ let idleCount = 0
156
+ , busyCount = 0
157
+ , reservedCount = 0
158
+ , connectingCount = 0
159
+ , closingCount = 0
160
+ , executingCount = 0
161
+ , waitingCount = queries.length
162
+
163
+ for (const c of connections) {
164
+ executingCount += c.active
165
+ waitingCount += c.pipelined + c.acquiring + (c.owner ? c.owner.queue.length : 0)
166
+ c.queue === open
167
+ ? idleCount++
168
+ : c.queue === connecting
169
+ ? connectingCount++
170
+ : c.queue === ended
171
+ ? closingCount++
172
+ : c.queue === reserved
173
+ ? reservedCount++
174
+ : (c.queue === busy || c.queue === full) && (c.owner ? reservedCount++ : busyCount++)
175
+ }
176
+
177
+ const totalCount = idleCount + busyCount + reservedCount + connectingCount
178
+
179
+ return {
180
+ max: options.max,
181
+ totalCount,
182
+ idleCount,
183
+ waitingCount,
184
+ activeCount: busyCount + reservedCount,
185
+ availableCount: idleCount + options.max - totalCount - closingCount,
186
+ busyCount,
187
+ reservedCount,
188
+ connectingCount,
189
+ closingCount,
190
+ executingCount,
191
+ listenCount: pooled(listen.sql),
192
+ subscribeCount: pooled(subscribe.sql)
193
+ }
194
+ }
195
+
196
+ function pooled(sql) {
197
+ if (!sql)
198
+ return 0
199
+
200
+ const s = sql.stats()
201
+ return s.totalCount + s.closingCount
202
+ }
203
+
153
204
  async function listen(name, fn, onlisten) {
154
205
  const listener = { fn, onlisten }
155
206
 
@@ -244,7 +295,7 @@ function Postgres(a, b) {
244
295
  const c = l.connection
245
296
  l.state === expected
246
297
  ? c.queue === full
247
- ? l.queue.push(q)
298
+ ? (q.queue = l.queue, l.queue.push(q))
248
299
  : c.execute(q) || move(c, full)
249
300
  : q.reject(leaseError(l.state))
250
301
  }
@@ -275,6 +326,7 @@ function Postgres(a, b) {
275
326
  const l = await lease(false)
276
327
  const sql = Sql(q => send(l, q))
277
328
  sql.release = () => settle(l, Lease.Released)
329
+ sql.stats = stats
278
330
  return sql
279
331
  }
280
332
 
@@ -435,7 +487,7 @@ function Postgres(a, b) {
435
487
  if (query.state) {
436
488
  query.cancelled = { resolve, reject }
437
489
  } else {
438
- queries.remove(query)
490
+ (query.queue || queries).remove(query)
439
491
  query.cancelled = true
440
492
  query.reject(Errors.generic('57014', 'canceling statement due to user request'))
441
493
  resolve()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steadybrew/postgresjs",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "PostgreSQL client for Node.js and Deno, independently maintained by Steady Brew",
5
5
  "type": "module",
6
6
  "module": "src/index.js",
package/src/connection.js CHANGED
@@ -222,6 +222,9 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
222
222
  },
223
223
  expired: () => idleTimer.expired() || lifeTimer.expired(),
224
224
  owner: null,
225
+ get active() { return query ? 1 : 0 },
226
+ get pipelined() { return sent.length },
227
+ get acquiring() { return acquisition && !acquisition.owner.cancelled ? 1 : 0 },
225
228
  count: 0,
226
229
  id
227
230
  }
@@ -527,9 +530,9 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
527
530
  && !q.cursorFn
528
531
  && sent.length < max_pipeline
529
532
  } catch (error) {
530
- sent.length === 0 && write(Sync)
531
- errored(error)
532
- return true
533
+ sent.remove(q) || write(Sync)
534
+ queryError(q, error)
535
+ return sent.length < max_pipeline
533
536
  }
534
537
  }
535
538
 
package/src/index.js CHANGED
@@ -81,6 +81,7 @@ function Postgres(a, b) {
81
81
  PostgresError,
82
82
  options,
83
83
  reserve,
84
+ stats,
84
85
  listen,
85
86
  begin,
86
87
  close,
@@ -150,6 +151,56 @@ function Postgres(a, b) {
150
151
  }
151
152
  }
152
153
 
154
+ function stats() {
155
+ let idleCount = 0
156
+ , busyCount = 0
157
+ , reservedCount = 0
158
+ , connectingCount = 0
159
+ , closingCount = 0
160
+ , executingCount = 0
161
+ , waitingCount = queries.length
162
+
163
+ for (const c of connections) {
164
+ executingCount += c.active
165
+ waitingCount += c.pipelined + c.acquiring + (c.owner ? c.owner.queue.length : 0)
166
+ c.queue === open
167
+ ? idleCount++
168
+ : c.queue === connecting
169
+ ? connectingCount++
170
+ : c.queue === ended
171
+ ? closingCount++
172
+ : c.queue === reserved
173
+ ? reservedCount++
174
+ : (c.queue === busy || c.queue === full) && (c.owner ? reservedCount++ : busyCount++)
175
+ }
176
+
177
+ const totalCount = idleCount + busyCount + reservedCount + connectingCount
178
+
179
+ return {
180
+ max: options.max,
181
+ totalCount,
182
+ idleCount,
183
+ waitingCount,
184
+ activeCount: busyCount + reservedCount,
185
+ availableCount: idleCount + options.max - totalCount - closingCount,
186
+ busyCount,
187
+ reservedCount,
188
+ connectingCount,
189
+ closingCount,
190
+ executingCount,
191
+ listenCount: pooled(listen.sql),
192
+ subscribeCount: pooled(subscribe.sql)
193
+ }
194
+ }
195
+
196
+ function pooled(sql) {
197
+ if (!sql)
198
+ return 0
199
+
200
+ const s = sql.stats()
201
+ return s.totalCount + s.closingCount
202
+ }
203
+
153
204
  async function listen(name, fn, onlisten) {
154
205
  const listener = { fn, onlisten }
155
206
 
@@ -244,7 +295,7 @@ function Postgres(a, b) {
244
295
  const c = l.connection
245
296
  l.state === expected
246
297
  ? c.queue === full
247
- ? l.queue.push(q)
298
+ ? (q.queue = l.queue, l.queue.push(q))
248
299
  : c.execute(q) || move(c, full)
249
300
  : q.reject(leaseError(l.state))
250
301
  }
@@ -275,6 +326,7 @@ function Postgres(a, b) {
275
326
  const l = await lease(false)
276
327
  const sql = Sql(q => send(l, q))
277
328
  sql.release = () => settle(l, Lease.Released)
329
+ sql.stats = stats
278
330
  return sql
279
331
  }
280
332
 
@@ -435,7 +487,7 @@ function Postgres(a, b) {
435
487
  if (query.state) {
436
488
  query.cancelled = { resolve, reject }
437
489
  } else {
438
- queries.remove(query)
490
+ (query.queue || queries).remove(query)
439
491
  query.cancelled = true
440
492
  query.reject(Errors.generic('57014', 'canceling statement due to user request'))
441
493
  resolve()
package/types/index.d.ts CHANGED
@@ -704,6 +704,35 @@ declare namespace postgres {
704
704
  json(value: JSONValue): Parameter;
705
705
  }
706
706
 
707
+ interface PoolStats {
708
+ /** The configured maximum number of pool connections */
709
+ max: number;
710
+ /** Pool connections that exist: connecting, idle or in use */
711
+ totalCount: number;
712
+ /** Pool connections with a live session that are not in use */
713
+ idleCount: number;
714
+ /** Issued queries and reserve or begin calls that are not running yet, whether queued in the pool, held by an opening connection, pipelined or queued on a reserved connection */
715
+ waitingCount: number;
716
+ /** Pool connections in use: busyCount plus reservedCount */
717
+ activeCount: number;
718
+ /** Connections a query can get without waiting behind other work: idleCount plus the slots the pool can still open */
719
+ availableCount: number;
720
+ /** Pool connections running queries, including those at the pipeline limit */
721
+ busyCount: number;
722
+ /** Pool connections held by reserve() or begin() */
723
+ reservedCount: number;
724
+ /** Pool connections opening a session */
725
+ connectingCount: number;
726
+ /** Pool connections draining or closing, not included in totalCount */
727
+ closingCount: number;
728
+ /** Queries the server is running right now, at most one per connection, startup queries and COPY included */
729
+ executingCount: number;
730
+ /** Connections held by the internal listen pool, outside max */
731
+ listenCount: number;
732
+ /** Connections held by the internal subscribe pool, outside max */
733
+ subscribeCount: number;
734
+ }
735
+
707
736
  interface Sql<TTypes extends Record<string, unknown> = {}> extends ISql<TTypes> {
708
737
  CLOSE: {};
709
738
  END: this['CLOSE'];
@@ -724,6 +753,9 @@ declare namespace postgres {
724
753
  begin<T>(options: string, cb: (sql: TransactionSql<TTypes>) => T | Promise<T>): Promise<UnwrapPromiseArray<T>>;
725
754
 
726
755
  reserve(): Promise<ReservedSql<TTypes>>
756
+
757
+ /** Returns a snapshot of the pool's connection and query counts */
758
+ stats(): PoolStats;
727
759
  }
728
760
 
729
761
  interface TransactionSql<TTypes extends Record<string, unknown> = {}> extends ISql<TTypes> {