@steadybrew/postgresjs 4.2.0 → 4.4.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 +70 -2
- package/cf/src/connection.js +63 -21
- package/cf/src/errors.js +8 -0
- package/cf/src/index.js +100 -33
- package/cf/src/subscribe.js +122 -47
- package/cjs/src/connection.js +63 -21
- package/cjs/src/errors.js +8 -0
- package/cjs/src/index.js +100 -33
- package/cjs/src/subscribe.js +122 -47
- package/package.json +1 -1
- package/src/connection.js +63 -21
- package/src/errors.js +8 -0
- package/src/index.js +100 -33
- package/src/subscribe.js +122 -47
- package/types/index.d.ts +41 -9
package/README.md
CHANGED
|
@@ -592,6 +592,8 @@ for await (const chunk of readableStream) {
|
|
|
592
592
|
|
|
593
593
|
> **NOTE** This is a low-level API which does not provide any type safety. To make this work, you must match your [`copy query` parameters](https://www.postgresql.org/docs/14/sql-copy.html) correctly to your [Node.js stream read or write](https://nodejs.org/api/stream.html) code. Ensure [Node.js stream backpressure](https://nodejs.org/en/learn/modules/backpressuring-in-streams) is handled correctly to avoid memory exhaustion.
|
|
594
594
|
|
|
595
|
+
A `COPY` holds its connection until it finishes. Postgres.js cannot tell that a simple-protocol query is a `COPY` until the server answers, so it sends nothing behind a simple query whose text contains the word `copy` (`sql.unsafe()` or `sql.file()` without parameters, `.simple()`, `.readable()`, `.writable()`). Queries sent through `sql` meanwhile run on another connection or wait for one. With `max: 1`, or when every connection is running a `COPY`, a query awaited before the `COPY` is ended therefore waits forever. Statements on a `sql.reserve()` or `sql.begin()` handle reject with `COPY_IN_PROGRESS` while its `COPY` runs.
|
|
596
|
+
|
|
595
597
|
### Canceling Queries in Progress
|
|
596
598
|
|
|
597
599
|
Postgres.js supports, [canceling queries in progress](https://www.postgresql.org/docs/7.1/protocol-protocol.html#AEN39000). It works by opening a new connection with a protocol level startup message to cancel the current query running on a specific connection. That means there is no guarantee that the query will be canceled, and due to the possible race conditions it might even result in canceling another query. This is fine for long running queries, but in the case of high load and fast queries it might be better to simply ignore results instead of canceling.
|
|
@@ -869,7 +871,7 @@ const sql = postgres({
|
|
|
869
871
|
When you call `.listen`, a dedicated connection will be created to ensure that you receive notifications instantly. This connection will be used for any further calls to `.listen`. The connection will automatically reconnect according to a backoff reconnection pattern to not overload the database server.
|
|
870
872
|
|
|
871
873
|
### Listen `await sql.listen(channel, onnotify, [onlisten]) -> { state }`
|
|
872
|
-
`.listen` takes the channel name, a function to handle each notify, and an optional function to run every time listen is registered and ready (happens on initial connect and reconnects). It returns a promise which resolves once the `LISTEN` query to Postgres completes, or if there is already a listener active.
|
|
874
|
+
`.listen` takes the channel name, a function to handle each notify, and an optional function to run every time listen is registered and ready (happens on initial connect and reconnects). It returns a promise which resolves once the `LISTEN` query to Postgres completes, or if there is already a listener active. If either function throws, the error is rethrown as an uncaught exception, and the other listeners and the returned promise are not affected.
|
|
873
875
|
|
|
874
876
|
```js
|
|
875
877
|
await sql.listen('news', payload => {
|
|
@@ -922,10 +924,17 @@ const { unsubscribe } = await sql.subscribe(
|
|
|
922
924
|
},
|
|
923
925
|
() => {
|
|
924
926
|
// Callback on initial connect and potential reconnects
|
|
927
|
+
},
|
|
928
|
+
(error) => {
|
|
929
|
+
// Callback on connection errors and changes that could not be decoded
|
|
925
930
|
}
|
|
926
931
|
)
|
|
927
932
|
```
|
|
928
933
|
|
|
934
|
+
If any of these callbacks throws, the error is rethrown as an uncaught exception, and other subscribers and the replication stream are not affected.
|
|
935
|
+
|
|
936
|
+
`onerror` is called with errors of the replication connection, after which the subscription reconnects on a new replication slot and calls `onsubscribe` again (these are also logged with `console.error`, and changes committed while it was disconnected are not delivered; if the reconnect itself fails, it is retried with backoff, its error is logged with `console.error` and passed to `onerror`, and it never ends the process), and with errors thrown while a change is decoded by a [transform](#data-transformation) or a custom type parser. Such a change is skipped, and the replication stream and later changes carry on. A decode error goes only to the subscriptions that would have received the change, matched by operation and table; a subscription filtered by key gets every skipped change of its table, as the key is read from the decoded row, even if the table has no primary key. If a column name transform throws, each change of that table is skipped and reported until the server describes the table again, for example after `ALTER TABLE`. If one of the matching subscriptions has no `onerror`, the error is rethrown once as an uncaught exception, which ends the process under Node's default handling. `onerror` stays registered across reconnects until `unsubscribe()`.
|
|
937
|
+
|
|
929
938
|
### Subscribe pattern
|
|
930
939
|
|
|
931
940
|
You can subscribe to specific operations, tables, or even rows with primary keys.
|
|
@@ -1051,6 +1060,8 @@ const sql = postgres('postgres://username:password@host:port/database', {
|
|
|
1051
1060
|
})
|
|
1052
1061
|
```
|
|
1053
1062
|
|
|
1063
|
+
The `onnotice`, `onparameter`, `onnotify`, `onclose`, `onidle` and `debug` callbacks passed in the options are called synchronously from inside the driver. If one throws, the error is rethrown as an uncaught exception and the connection and its queries are not affected; under Node's default handling that ends the process.
|
|
1064
|
+
|
|
1054
1065
|
Note that `max_lifetime = 60 * (30 + Math.random() * 30)` by default. This resolves to an interval between 30 and 60 minutes to optimize for the benefits of prepared statements **and** working nicely with Linux's OOM killer.
|
|
1055
1066
|
|
|
1056
1067
|
### Dynamic passwords
|
|
@@ -1121,6 +1132,58 @@ There are no guarantees about queries executing in order unless using a transact
|
|
|
1121
1132
|
|
|
1122
1133
|
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
1134
|
|
|
1135
|
+
### Pool stats
|
|
1136
|
+
|
|
1137
|
+
`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:
|
|
1138
|
+
|
|
1139
|
+
```js
|
|
1140
|
+
sql.stats()
|
|
1141
|
+
// { max: 10, totalCount: 7, idleCount: 2, waitingCount: 3,
|
|
1142
|
+
// activeCount: 4, availableCount: 5,
|
|
1143
|
+
// busyCount: 3, reservedCount: 1, connectingCount: 1, closingCount: 0,
|
|
1144
|
+
// executingCount: 4, listenCount: 1, subscribeCount: 0 }
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
| Field | Meaning |
|
|
1148
|
+
|---|---|
|
|
1149
|
+
| `max` | The `max` option |
|
|
1150
|
+
| `totalCount` | Connections that exist: connecting, idle or in use |
|
|
1151
|
+
| `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 |
|
|
1152
|
+
| `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 |
|
|
1153
|
+
| `activeCount` | `busyCount + reservedCount`, connections in use |
|
|
1154
|
+
| `availableCount` | `idleCount` plus the slots the pool can still open (`max - totalCount - closingCount`): connections a query can get without waiting behind other work |
|
|
1155
|
+
| `busyCount` | Pool connections running queries, including connections at the pipeline limit |
|
|
1156
|
+
| `reservedCount` | Connections held by `reserve()` or `begin()` |
|
|
1157
|
+
| `connectingCount` | Connections opening a session |
|
|
1158
|
+
| `closingCount` | Connections draining or closing. They are not in `totalCount`, but still hold a pool slot until closed |
|
|
1159
|
+
| `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 |
|
|
1160
|
+
| `listenCount` | Connections held by the internal `sql.listen()` pool (0 or 1) |
|
|
1161
|
+
| `subscribeCount` | Connections held by the internal `sql.subscribe()` pool (0 or 1) |
|
|
1162
|
+
|
|
1163
|
+
`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.
|
|
1164
|
+
|
|
1165
|
+
`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`.
|
|
1166
|
+
|
|
1167
|
+
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:
|
|
1168
|
+
|
|
1169
|
+
```js
|
|
1170
|
+
import http from 'node:http'
|
|
1171
|
+
|
|
1172
|
+
const MAX_WAITING = 100
|
|
1173
|
+
|
|
1174
|
+
http.createServer((req, res) => {
|
|
1175
|
+
const stats = sql.stats()
|
|
1176
|
+
const overloaded = stats.availableCount === 0 && stats.waitingCount > MAX_WAITING
|
|
1177
|
+
|
|
1178
|
+
res.writeHead(overloaded ? 503 : 200, { 'content-type': 'application/json' })
|
|
1179
|
+
res.end(JSON.stringify(stats))
|
|
1180
|
+
}).listen(8080)
|
|
1181
|
+
```
|
|
1182
|
+
|
|
1183
|
+
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.
|
|
1184
|
+
|
|
1185
|
+
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.
|
|
1186
|
+
|
|
1124
1187
|
### Connection timeout
|
|
1125
1188
|
|
|
1126
1189
|
By default, connections will not close until `.end()` is called. However, it may be useful to have them close automatically when:
|
|
@@ -1405,7 +1468,12 @@ If the only attempt times out, the error is `CONNECT_TIMEOUT`. Once earlier atte
|
|
|
1405
1468
|
##### COPY_IN_PROGRESS
|
|
1406
1469
|
> You cannot execute queries during copy
|
|
1407
1470
|
|
|
1408
|
-
This error is thrown
|
|
1471
|
+
This error is thrown for a statement on a `sql.reserve()` or `sql.begin()` handle while its copy operation (writable / readable) runs, including statements sent before the server started it. Queries through `sql` wait instead.
|
|
1472
|
+
|
|
1473
|
+
##### TRANSACTION_ROLLED_BACK
|
|
1474
|
+
> The server rolled the transaction back instead of committing it
|
|
1475
|
+
|
|
1476
|
+
This error is thrown by `sql.begin()` when the server answers the final `COMMIT` (or `PREPARE TRANSACTION`) with `ROLLBACK`, because the transaction was aborted by a statement whose error the callback did not surface, such as a failing query that was not awaited. Nothing was committed. When such a query's error was already recorded, it is the error's `cause`.
|
|
1409
1477
|
|
|
1410
1478
|
## TypeScript support
|
|
1411
1479
|
|
package/cf/src/connection.js
CHANGED
|
@@ -7,7 +7,7 @@ import Stream from 'node:stream'
|
|
|
7
7
|
import { performance } from '../polyfills.js'
|
|
8
8
|
|
|
9
9
|
import { stringify, handleValue, addArrayType } from './types.js'
|
|
10
|
-
import { Errors } from './errors.js'
|
|
10
|
+
import { Errors, callback } from './errors.js'
|
|
11
11
|
import Result from './result.js'
|
|
12
12
|
import Queue from './queue.js'
|
|
13
13
|
import clampedTimeout from './timeout.js'
|
|
@@ -15,7 +15,7 @@ import { SSLRequest, tlsConfig, cancelRequest } from './transport.js'
|
|
|
15
15
|
import { Query, CLOSE } from './query.js'
|
|
16
16
|
import b from './bytes.js'
|
|
17
17
|
|
|
18
|
-
export
|
|
18
|
+
export { Connection }
|
|
19
19
|
|
|
20
20
|
let uid = 1
|
|
21
21
|
|
|
@@ -28,6 +28,8 @@ const Sync = b().S().end()
|
|
|
28
28
|
const Phase = { Closed: 0, Backoff: 1, Connecting: 2, Initializing: 3, Ready: 4, Draining: 5, Closing: 6 }
|
|
29
29
|
const phaseNames = Object.keys(Phase)
|
|
30
30
|
|
|
31
|
+
const endsAbortedState = /^\s*(rollback|abort)\b/i
|
|
32
|
+
|
|
31
33
|
const retryRoutines = new Set([
|
|
32
34
|
'FetchPreparedStatement',
|
|
33
35
|
'RevalidateCachedQuery',
|
|
@@ -112,6 +114,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
112
114
|
, chunk = null
|
|
113
115
|
, nonce = null
|
|
114
116
|
, query = null
|
|
117
|
+
, barrier = null
|
|
115
118
|
, final = null
|
|
116
119
|
|
|
117
120
|
const check = globalThis[Symbol.for('postgres.js:check')]
|
|
@@ -224,6 +227,11 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
224
227
|
},
|
|
225
228
|
expired: () => idleTimer.expired() || lifeTimer.expired(),
|
|
226
229
|
owner: null,
|
|
230
|
+
get active() { return query ? 1 : 0 },
|
|
231
|
+
get pipelined() { return sent.length },
|
|
232
|
+
get accepting() { return accepting() },
|
|
233
|
+
get copying() { return stream !== null },
|
|
234
|
+
get acquiring() { return acquisition && !acquisition.owner.cancelled ? 1 : 0 },
|
|
227
235
|
count: 0,
|
|
228
236
|
id
|
|
229
237
|
}
|
|
@@ -267,8 +275,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
267
275
|
return !query && sent.length === 0
|
|
268
276
|
}
|
|
269
277
|
|
|
278
|
+
function accepting() {
|
|
279
|
+
return !query || (!barrier && sent.length < max_pipeline)
|
|
280
|
+
}
|
|
281
|
+
|
|
270
282
|
function backoffMs() {
|
|
271
|
-
return (
|
|
283
|
+
return backoffSeconds(backoff, options.shared.retries) * 1000
|
|
272
284
|
}
|
|
273
285
|
|
|
274
286
|
function standbyPass() {
|
|
@@ -445,7 +457,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
445
457
|
queryError(pending, err)
|
|
446
458
|
pending.cancelled && pending.cancelled.resolve()
|
|
447
459
|
}
|
|
448
|
-
query = results = errorResponse = chunk = nextWriteTimer = null
|
|
460
|
+
query = results = errorResponse = chunk = nextWriteTimer = barrier = null
|
|
449
461
|
result = new Result()
|
|
450
462
|
rows = 0
|
|
451
463
|
nonce = serverSignature = null
|
|
@@ -509,8 +521,10 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
509
521
|
}
|
|
510
522
|
|
|
511
523
|
function submit(q) {
|
|
512
|
-
if (stream)
|
|
513
|
-
|
|
524
|
+
if (stream) {
|
|
525
|
+
queryError(q, Errors.generic('COPY_IN_PROGRESS', 'You cannot execute queries during copy'))
|
|
526
|
+
return true
|
|
527
|
+
}
|
|
514
528
|
|
|
515
529
|
if (q.cancelled)
|
|
516
530
|
return true
|
|
@@ -524,17 +538,21 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
524
538
|
: (query = q, query.active = true)
|
|
525
539
|
|
|
526
540
|
build(q)
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
541
|
+
const written = write(toBuffer(q))
|
|
542
|
+
if (q.describeFirst || q.cursorFn || mayCopy(q))
|
|
543
|
+
barrier = q
|
|
544
|
+
return written && accepting()
|
|
531
545
|
} catch (error) {
|
|
532
|
-
sent.
|
|
533
|
-
|
|
534
|
-
return
|
|
546
|
+
sent.remove(q) || write(Sync)
|
|
547
|
+
queryError(q, error)
|
|
548
|
+
return accepting()
|
|
535
549
|
}
|
|
536
550
|
}
|
|
537
551
|
|
|
552
|
+
function mayCopy(q) {
|
|
553
|
+
return q.options.simple && /\bcopy\b/i.test(q.string)
|
|
554
|
+
}
|
|
555
|
+
|
|
538
556
|
function toBuffer(q) {
|
|
539
557
|
if (q.parameters.length >= 65534)
|
|
540
558
|
throw Errors.generic('MAX_PARAMETERS_EXCEEDED', 'Max number of parameters (65534) exceeded')
|
|
@@ -593,7 +611,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
593
611
|
? statements[q.signature]
|
|
594
612
|
: { string, types, name: q.prepare ? statementId + statementCount++ : '' }
|
|
595
613
|
|
|
596
|
-
typeof options.debug === 'function' && options.debug
|
|
614
|
+
typeof options.debug === 'function' && callback(options.debug, id, string, parameters, types)
|
|
597
615
|
}
|
|
598
616
|
|
|
599
617
|
function write(x, fn) {
|
|
@@ -764,18 +782,21 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
764
782
|
backendParameters[k] = v
|
|
765
783
|
if (options.parameters[k] !== v) {
|
|
766
784
|
options.parameters[k] = v
|
|
767
|
-
onparameter
|
|
785
|
+
callback(onparameter, k, v)
|
|
768
786
|
}
|
|
769
787
|
}
|
|
770
788
|
|
|
771
|
-
function ReadyForQuery() {
|
|
789
|
+
function ReadyForQuery(x) {
|
|
790
|
+
query === barrier && (barrier = null)
|
|
772
791
|
if (query) {
|
|
773
792
|
if (errorResponse) {
|
|
774
793
|
if (query.initialization)
|
|
775
794
|
return dispatch('initFailed', errorResponse)
|
|
795
|
+
query.prepared && retryRoutines.has(errorResponse.routine) && delete statements[query.signature]
|
|
776
796
|
query.retried
|
|
777
797
|
? errored(query.retried)
|
|
778
|
-
: query.prepared && retryRoutines.has(errorResponse.routine) && query.owner === connection.owner
|
|
798
|
+
: query.prepared && retryRoutines.has(errorResponse.routine) && query.owner === connection.owner && !barrier
|
|
799
|
+
&& (x[5] !== 69 || endsAbortedState.test(query.string))
|
|
779
800
|
? retry(query, errorResponse)
|
|
780
801
|
: errored(errorResponse)
|
|
781
802
|
} else {
|
|
@@ -1058,7 +1079,6 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
1058
1079
|
}
|
|
1059
1080
|
|
|
1060
1081
|
function retry(q, error) {
|
|
1061
|
-
delete statements[q.signature]
|
|
1062
1082
|
q.retried = error
|
|
1063
1083
|
execute(q)
|
|
1064
1084
|
}
|
|
@@ -1069,7 +1089,8 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
1069
1089
|
|
|
1070
1090
|
let index = 9
|
|
1071
1091
|
while (x[index++] !== 0);
|
|
1072
|
-
|
|
1092
|
+
callback(
|
|
1093
|
+
onnotify,
|
|
1073
1094
|
x.toString('utf8', 9, index - 1),
|
|
1074
1095
|
x.toString('utf8', index, x.length - 1)
|
|
1075
1096
|
)
|
|
@@ -1115,6 +1136,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
1115
1136
|
}
|
|
1116
1137
|
})
|
|
1117
1138
|
query.resolve(stream)
|
|
1139
|
+
connection.owner && connection.owner.next()
|
|
1118
1140
|
}
|
|
1119
1141
|
|
|
1120
1142
|
function CopyOutResponse() {
|
|
@@ -1122,6 +1144,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
1122
1144
|
read() { socket.resume() }
|
|
1123
1145
|
})
|
|
1124
1146
|
query.resolve(stream)
|
|
1147
|
+
connection.owner && connection.owner.next()
|
|
1125
1148
|
}
|
|
1126
1149
|
|
|
1127
1150
|
/* c8 ignore next 3 */
|
|
@@ -1144,6 +1167,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
1144
1167
|
}
|
|
1145
1168
|
})
|
|
1146
1169
|
query.resolve(stream)
|
|
1170
|
+
connection.owner && connection.owner.next()
|
|
1147
1171
|
}
|
|
1148
1172
|
|
|
1149
1173
|
function CopyData(x) {
|
|
@@ -1156,10 +1180,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
1156
1180
|
}
|
|
1157
1181
|
|
|
1158
1182
|
function NoticeResponse(x) {
|
|
1183
|
+
if (onnotice === false)
|
|
1184
|
+
return
|
|
1185
|
+
|
|
1159
1186
|
onnotice
|
|
1160
|
-
? onnotice
|
|
1187
|
+
? callback(onnotice, parseError(x))
|
|
1161
1188
|
: console.log(parseError(x)) // eslint-disable-line
|
|
1162
|
-
|
|
1163
1189
|
}
|
|
1164
1190
|
|
|
1165
1191
|
/* c8 ignore next 3 */
|
|
@@ -1310,3 +1336,19 @@ function timer(fn, seconds) {
|
|
|
1310
1336
|
fn.apply(null, args)
|
|
1311
1337
|
}
|
|
1312
1338
|
}
|
|
1339
|
+
|
|
1340
|
+
export function defaultBackoff(retries) {
|
|
1341
|
+
return (0.5 + Math.random() / 2) * Math.min(3 ** retries / 100, 20)
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
export function backoffSeconds(backoff, retries) {
|
|
1345
|
+
if (typeof backoff !== 'function')
|
|
1346
|
+
return backoff
|
|
1347
|
+
|
|
1348
|
+
try {
|
|
1349
|
+
return backoff(retries)
|
|
1350
|
+
} catch (e) {
|
|
1351
|
+
callback(() => { throw e })
|
|
1352
|
+
return defaultBackoff(retries)
|
|
1353
|
+
}
|
|
1354
|
+
}
|
package/cf/src/errors.js
CHANGED
package/cf/src/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Parameter,
|
|
10
10
|
Identifier,
|
|
11
11
|
Builder,
|
|
12
|
+
escapeIdentifier,
|
|
12
13
|
toPascal,
|
|
13
14
|
pascal,
|
|
14
15
|
toCamel,
|
|
@@ -20,12 +21,12 @@ import {
|
|
|
20
21
|
fromKebab
|
|
21
22
|
} from './types.js'
|
|
22
23
|
|
|
23
|
-
import Connection from './connection.js'
|
|
24
|
+
import { Connection, defaultBackoff } from './connection.js'
|
|
24
25
|
import { cancelRequest } from './transport.js'
|
|
25
26
|
import clampedTimeout from './timeout.js'
|
|
26
27
|
import { Query, CLOSE } from './query.js'
|
|
27
28
|
import Queue from './queue.js'
|
|
28
|
-
import { Errors, PostgresError } from './errors.js'
|
|
29
|
+
import { Errors, PostgresError, callback } from './errors.js'
|
|
29
30
|
import Subscribe from './subscribe.js'
|
|
30
31
|
import largeObject from './large.js'
|
|
31
32
|
|
|
@@ -82,6 +83,7 @@ function Postgres(a, b) {
|
|
|
82
83
|
PostgresError,
|
|
83
84
|
options,
|
|
84
85
|
reserve,
|
|
86
|
+
stats,
|
|
85
87
|
listen,
|
|
86
88
|
begin,
|
|
87
89
|
close,
|
|
@@ -151,6 +153,56 @@ function Postgres(a, b) {
|
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
|
|
156
|
+
function stats() {
|
|
157
|
+
let idleCount = 0
|
|
158
|
+
, busyCount = 0
|
|
159
|
+
, reservedCount = 0
|
|
160
|
+
, connectingCount = 0
|
|
161
|
+
, closingCount = 0
|
|
162
|
+
, executingCount = 0
|
|
163
|
+
, waitingCount = queries.length
|
|
164
|
+
|
|
165
|
+
for (const c of connections) {
|
|
166
|
+
executingCount += c.active
|
|
167
|
+
waitingCount += c.pipelined + c.acquiring + (c.owner ? c.owner.queue.length : 0)
|
|
168
|
+
c.queue === open
|
|
169
|
+
? idleCount++
|
|
170
|
+
: c.queue === connecting
|
|
171
|
+
? connectingCount++
|
|
172
|
+
: c.queue === ended
|
|
173
|
+
? closingCount++
|
|
174
|
+
: c.queue === reserved
|
|
175
|
+
? reservedCount++
|
|
176
|
+
: (c.queue === busy || c.queue === full) && (c.owner ? reservedCount++ : busyCount++)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const totalCount = idleCount + busyCount + reservedCount + connectingCount
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
max: options.max,
|
|
183
|
+
totalCount,
|
|
184
|
+
idleCount,
|
|
185
|
+
waitingCount,
|
|
186
|
+
activeCount: busyCount + reservedCount,
|
|
187
|
+
availableCount: idleCount + options.max - totalCount - closingCount,
|
|
188
|
+
busyCount,
|
|
189
|
+
reservedCount,
|
|
190
|
+
connectingCount,
|
|
191
|
+
closingCount,
|
|
192
|
+
executingCount,
|
|
193
|
+
listenCount: pooled(listen.sql),
|
|
194
|
+
subscribeCount: pooled(subscribe.sql)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function pooled(sql) {
|
|
199
|
+
if (!sql)
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
const s = sql.stats()
|
|
203
|
+
return s.totalCount + s.closingCount
|
|
204
|
+
}
|
|
205
|
+
|
|
154
206
|
async function listen(name, fn, onlisten) {
|
|
155
207
|
const listener = { fn, onlisten }
|
|
156
208
|
|
|
@@ -168,7 +220,7 @@ function Postgres(a, b) {
|
|
|
168
220
|
})
|
|
169
221
|
},
|
|
170
222
|
onnotify(c, x) {
|
|
171
|
-
c in listen.channels && listen.channels[c].listeners.forEach(l => l.fn
|
|
223
|
+
c in listen.channels && listen.channels[c].listeners.forEach(l => callback(l.fn, x))
|
|
172
224
|
}
|
|
173
225
|
}))
|
|
174
226
|
|
|
@@ -178,7 +230,7 @@ function Postgres(a, b) {
|
|
|
178
230
|
if (exists) {
|
|
179
231
|
channels[name].listeners.push(listener)
|
|
180
232
|
const result = await channels[name].result
|
|
181
|
-
listener.onlisten
|
|
233
|
+
callback(listener.onlisten)
|
|
182
234
|
return { state: result.state, unlisten }
|
|
183
235
|
}
|
|
184
236
|
|
|
@@ -186,7 +238,7 @@ function Postgres(a, b) {
|
|
|
186
238
|
sql.unsafe('"' + name.replace(/"/g, '""') + '"')
|
|
187
239
|
}`, listeners: [listener] }
|
|
188
240
|
const result = await channels[name].result
|
|
189
|
-
listener.onlisten
|
|
241
|
+
callback(listener.onlisten)
|
|
190
242
|
return { state: result.state, unlisten }
|
|
191
243
|
|
|
192
244
|
async function unlisten() {
|
|
@@ -244,9 +296,9 @@ function Postgres(a, b) {
|
|
|
244
296
|
function send(l, q, expected = Lease.Active) {
|
|
245
297
|
const c = l.connection
|
|
246
298
|
l.state === expected
|
|
247
|
-
? c.queue
|
|
248
|
-
?
|
|
249
|
-
:
|
|
299
|
+
? c.copying || (c.queue !== full && c.accepting)
|
|
300
|
+
? c.execute(q) || move(c, full)
|
|
301
|
+
: (move(c, full), q.queue = l.queue, l.queue.push(q))
|
|
250
302
|
: q.reject(leaseError(l.state))
|
|
251
303
|
}
|
|
252
304
|
|
|
@@ -276,6 +328,7 @@ function Postgres(a, b) {
|
|
|
276
328
|
const l = await lease(false)
|
|
277
329
|
const sql = Sql(q => send(l, q))
|
|
278
330
|
sql.release = () => settle(l, Lease.Released)
|
|
331
|
+
sql.stats = stats
|
|
279
332
|
return sql
|
|
280
333
|
}
|
|
281
334
|
|
|
@@ -301,14 +354,16 @@ function Postgres(a, b) {
|
|
|
301
354
|
}
|
|
302
355
|
|
|
303
356
|
async function finish(statement) {
|
|
357
|
+
let result
|
|
304
358
|
l.state = Lease.Settling
|
|
305
359
|
try {
|
|
306
|
-
await statement(Sql(q => send(l, q, Lease.Settling)))
|
|
360
|
+
result = await statement(Sql(q => send(l, q, Lease.Settling)))
|
|
307
361
|
} catch (e) {
|
|
308
362
|
settle(l, Lease.Discarded)
|
|
309
363
|
throw e
|
|
310
364
|
}
|
|
311
365
|
settle(l, Lease.Released)
|
|
366
|
+
return result
|
|
312
367
|
}
|
|
313
368
|
|
|
314
369
|
async function scope(fn, name) {
|
|
@@ -323,7 +378,7 @@ function Postgres(a, b) {
|
|
|
323
378
|
let uncaughtError
|
|
324
379
|
, result
|
|
325
380
|
|
|
326
|
-
name && await sql
|
|
381
|
+
name && await sql.unsafe('savepoint ' + escapeIdentifier(name))
|
|
327
382
|
try {
|
|
328
383
|
result = await new Promise((resolve, reject) => {
|
|
329
384
|
const x = fn(sql)
|
|
@@ -334,8 +389,8 @@ function Postgres(a, b) {
|
|
|
334
389
|
throw uncaughtError
|
|
335
390
|
} catch (e) {
|
|
336
391
|
l.state === Lease.Active && await (name
|
|
337
|
-
? sql
|
|
338
|
-
: finish(final => final
|
|
392
|
+
? sql.unsafe('rollback to ' + escapeIdentifier(name))
|
|
393
|
+
: finish(final => final.unsafe('rollback'))
|
|
339
394
|
)
|
|
340
395
|
throw e instanceof PostgresError && e.code === '25P02' && uncaughtError || e
|
|
341
396
|
}
|
|
@@ -343,10 +398,18 @@ function Postgres(a, b) {
|
|
|
343
398
|
if (l.state !== Lease.Active)
|
|
344
399
|
throw l.error || leaseError(l.state)
|
|
345
400
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
401
|
+
if (!name) {
|
|
402
|
+
const done = await finish(final => prepare
|
|
403
|
+
? final.unsafe('prepare transaction ' + prepare)
|
|
404
|
+
: final.unsafe('commit')
|
|
405
|
+
)
|
|
406
|
+
if (done.command === 'ROLLBACK') {
|
|
407
|
+
throw Object.assign(
|
|
408
|
+
Errors.generic('TRANSACTION_ROLLED_BACK', 'The server rolled the transaction back instead of committing it'),
|
|
409
|
+
uncaughtError && { cause: uncaughtError }
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
350
413
|
|
|
351
414
|
return result
|
|
352
415
|
|
|
@@ -375,14 +438,6 @@ function Postgres(a, b) {
|
|
|
375
438
|
return c
|
|
376
439
|
}
|
|
377
440
|
|
|
378
|
-
function callback(fn, ...args) {
|
|
379
|
-
try {
|
|
380
|
-
fn && fn(...args)
|
|
381
|
-
} catch (e) {
|
|
382
|
-
queueMicrotask(() => { throw e })
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
441
|
function json(x) {
|
|
387
442
|
return new Parameter(x, 3802)
|
|
388
443
|
}
|
|
@@ -405,11 +460,24 @@ function Postgres(a, b) {
|
|
|
405
460
|
if (closed.length)
|
|
406
461
|
return connect(closed.shift(), query)
|
|
407
462
|
|
|
408
|
-
|
|
409
|
-
|
|
463
|
+
const c = takeBusy()
|
|
464
|
+
c
|
|
465
|
+
? go(c, query)
|
|
410
466
|
: queries.push(query)
|
|
411
467
|
}
|
|
412
468
|
|
|
469
|
+
function takeBusy() {
|
|
470
|
+
while (busy.length) {
|
|
471
|
+
const c = busy.shift()
|
|
472
|
+
if (c.accepting)
|
|
473
|
+
return c
|
|
474
|
+
|
|
475
|
+
move(c, full)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return null
|
|
479
|
+
}
|
|
480
|
+
|
|
413
481
|
function takeOpen() {
|
|
414
482
|
while (open.length) {
|
|
415
483
|
const c = open.shift()
|
|
@@ -436,7 +504,7 @@ function Postgres(a, b) {
|
|
|
436
504
|
if (query.state) {
|
|
437
505
|
query.cancelled = { resolve, reject }
|
|
438
506
|
} else {
|
|
439
|
-
queries.remove(query)
|
|
507
|
+
(query.queue || queries).remove(query)
|
|
440
508
|
query.cancelled = true
|
|
441
509
|
query.reject(Errors.generic('57014', 'canceling statement due to user request'))
|
|
442
510
|
resolve()
|
|
@@ -494,6 +562,9 @@ function Postgres(a, b) {
|
|
|
494
562
|
if (initial && initial.reserve)
|
|
495
563
|
return initial.reserve(c)
|
|
496
564
|
|
|
565
|
+
if (!initial && !c.accepting)
|
|
566
|
+
return move(c, full)
|
|
567
|
+
|
|
497
568
|
if (!initial && !queries.length)
|
|
498
569
|
return move(c, open)
|
|
499
570
|
|
|
@@ -532,7 +603,7 @@ function Postgres(a, b) {
|
|
|
532
603
|
function onclose(c, e) {
|
|
533
604
|
move(c, closed)
|
|
534
605
|
c.owner && settle(c.owner, Lease.Closed, e)
|
|
535
|
-
options.onclose
|
|
606
|
+
callback(options.onclose, c.id)
|
|
536
607
|
if (ending)
|
|
537
608
|
return closedWhileEnding(c, e)
|
|
538
609
|
|
|
@@ -587,7 +658,7 @@ function parseOptions(a, b) {
|
|
|
587
658
|
connect_timeout : 30,
|
|
588
659
|
max_lifetime : max_lifetime,
|
|
589
660
|
max_pipeline : 100,
|
|
590
|
-
backoff :
|
|
661
|
+
backoff : defaultBackoff,
|
|
591
662
|
keep_alive : 60,
|
|
592
663
|
prepare : true,
|
|
593
664
|
debug : false,
|
|
@@ -646,10 +717,6 @@ function tsa(o, url, env) {
|
|
|
646
717
|
throw new Error('target_session_attrs ' + x + ' is not supported')
|
|
647
718
|
}
|
|
648
719
|
|
|
649
|
-
function backoff(retries) {
|
|
650
|
-
return (0.5 + Math.random() / 2) * Math.min(3 ** retries / 100, 20)
|
|
651
|
-
}
|
|
652
|
-
|
|
653
720
|
function max_lifetime() {
|
|
654
721
|
return 60 * (30 + Math.random() * 30)
|
|
655
722
|
}
|