@amritk/nish-x86_64-linux 0.21.0 → 0.23.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/INSTALL.md +30 -0
- package/bin/nish +0 -0
- package/docs/AI.md +50 -0
- package/package.json +1 -1
- package/runtime/browser.d.ts +1 -9
- package/runtime/browser.mjs +2 -13
- package/runtime/core/arrays.d.ts +26 -8
- package/runtime/core/arrays.mjs +31 -3
- package/runtime/core/host.d.ts +10 -0
- package/runtime/core/host.mjs +14 -0
- package/runtime/core/numbers.d.ts +14 -1
- package/runtime/core/numbers.mjs +30 -3
- package/runtime/core/result.d.ts +4 -0
- package/runtime/core/result.mjs +13 -0
- package/runtime/core/strings.d.ts +2 -2
- package/runtime/nish.d.ts +2 -2
- package/runtime/node.mjs +3 -0
- package/runtime/runtime-net.c +12 -1
- package/scripts/build.sh +58 -14
- package/std/README.md +3 -3
- package/std/cli.ts +17 -1
- package/std/crypto/bcrypt.ts +869 -72
- package/std/json.ts +4 -7
- package/std/net/http1-server.ts +199 -34
- package/std/net/http1.ts +40 -21
- package/std/net/postgres.ts +1339 -55
- package/std/net/prefork.ts +23 -3
- package/std/random.ts +66 -4
package/std/net/postgres.ts
CHANGED
|
@@ -63,7 +63,12 @@
|
|
|
63
63
|
* length in two reused arrays, so a row costs no allocation. `int`, `bool`,
|
|
64
64
|
* `isNull` and `PgText.timestamp` decode a column in place; `PgArrayWalker`
|
|
65
65
|
* walks a one-dimensional `text[]` in place. `columnText(c)` is the one that
|
|
66
|
-
* allocates, a string.
|
|
66
|
+
* allocates, a string. An array parameter is a text array literal too:
|
|
67
|
+
* `paramIntArray` writes an `int8[]` from an `i64[]` window, and `arrayStart`,
|
|
68
|
+
* `arrayElement`, `arrayInt`, `arrayNull` and `arrayEnd` build an array one
|
|
69
|
+
* element at a time into the Bind, so a list needs no string built first. A
|
|
70
|
+
* program whose element was refused for room can drop the whole bind,
|
|
71
|
+
* unsent, with `bindCancel`.
|
|
67
72
|
*
|
|
68
73
|
* **Authentication.** SCRAM-SHA-256 (RFC 5802, RFC 7677) or none (`trust`).
|
|
69
74
|
* The password arrives as a `Secret<u8[]>`; the connection keeps a copy in a
|
|
@@ -90,6 +95,35 @@
|
|
|
90
95
|
* connection with their own `failure` code. The constructor panics on a cap
|
|
91
96
|
* out of range, as on a missing user or a short nonce.
|
|
92
97
|
*
|
|
98
|
+
* **Pools.** `PgPool` holds up to `size` connections to one server and a FIFO
|
|
99
|
+
* queue of up to `queueSize` requests, still sans-IO: `wanted()` names a slot
|
|
100
|
+
* that needs a socket, `connect(slot, nonce)` starts its connection, and the
|
|
101
|
+
* program moves its bytes as above through `connection(slot)`. A request is a
|
|
102
|
+
* ticket from `request()`; `dispatch()` gives the oldest to the ready
|
|
103
|
+
* connection with the fewest groups in flight, the program builds its group and
|
|
104
|
+
* ends it with `send()`, and `next(slot)` reads that connection's events with
|
|
105
|
+
* `ticket` naming the request. Statements given to `register` are prepared on
|
|
106
|
+
* each connection ahead of its first request. A connection that fails reports
|
|
107
|
+
* `PG_LOST` for each request it took, then `PG_FAILED`; the program closes the
|
|
108
|
+
* socket and the slot is built again on its next use. `tests/postgres/carrier.ts`
|
|
109
|
+
* is a whole carrier: one `pollWait` loop holding the pool's sockets beside an
|
|
110
|
+
* HTTP listener.
|
|
111
|
+
*
|
|
112
|
+
* **Batching.** `PgBatcher` turns many reads of one row shape by key — a user
|
|
113
|
+
* by id, an article by slug — into one execution of a statement over
|
|
114
|
+
* `unnest($1) with ordinality`. `addInt` or `addText` queues a request's key
|
|
115
|
+
* and tag; `take(conn)` binds the oldest waiting, each distinct key once, up
|
|
116
|
+
* to `maxRequests` and `maxBytes`, and answers a run's handle; `row`, `end`
|
|
117
|
+
* and `fail` take that execution's events (a handle already answered makes
|
|
118
|
+
* them a no-op counted in `stray`, so a server's late event cannot panic),
|
|
119
|
+
* routing each row by its ordinal (checked first: a server's ordinal is
|
|
120
|
+
* refused before it routes anything), and `next(run)` answers
|
|
121
|
+
* `PG_BATCH_ROW`, then `PG_BATCH_DONE`, `PG_BATCH_MISSING` or
|
|
122
|
+
* `PG_BATCH_FAILED`, for each request. It reads no clock, and the batcher
|
|
123
|
+
* itself allocates only in its constructor (the connection's output buffer a
|
|
124
|
+
* take writes into may still grow, up to its `maxOutput`); when to take is
|
|
125
|
+
* the program's.
|
|
126
|
+
*
|
|
93
127
|
* Written from PostgreSQL's "Frontend/Backend Protocol" chapter (version 18)
|
|
94
128
|
* and RFC 5802 and RFC 7677; not ported from another implementation.
|
|
95
129
|
*/
|
|
@@ -296,6 +330,35 @@ class PgPassword {
|
|
|
296
330
|
}
|
|
297
331
|
}
|
|
298
332
|
|
|
333
|
+
/** Panics on a `PgConfig` that is a mistake in the program: no user, a cap out of range, a NUL in a startup parameter. */
|
|
334
|
+
const pgCheckConfig = (config: PgConfig): void => {
|
|
335
|
+
if (config.user.length === 0) {
|
|
336
|
+
panic("nish/net/postgres: PgConfig.user is required")
|
|
337
|
+
}
|
|
338
|
+
if (
|
|
339
|
+
config.maxMessage < 1024 ||
|
|
340
|
+
config.maxMessage > PG_MAX_CAP ||
|
|
341
|
+
config.maxOutput < 1024 ||
|
|
342
|
+
config.maxOutput > PG_MAX_CAP ||
|
|
343
|
+
config.maxInFlight < 1 ||
|
|
344
|
+
config.maxColumns < 1 ||
|
|
345
|
+
config.maxColumns > 65535 ||
|
|
346
|
+
config.minIterations < 1 ||
|
|
347
|
+
config.maxIterations < config.minIterations ||
|
|
348
|
+
config.maxIterations > PG_MAX_ITERATIONS
|
|
349
|
+
) {
|
|
350
|
+
panic("nish/net/postgres: a PgConfig cap out of range")
|
|
351
|
+
}
|
|
352
|
+
if (
|
|
353
|
+
pgHasNul(config.user) ||
|
|
354
|
+
pgHasNul(config.database) ||
|
|
355
|
+
pgHasNul(config.applicationName) ||
|
|
356
|
+
pgHasNul(config.options)
|
|
357
|
+
) {
|
|
358
|
+
panic("nish/net/postgres: a startup parameter holds a NUL byte")
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
299
362
|
/** The bytes of `text`. */
|
|
300
363
|
const pgBytesOf = (text: string): u8[] => {
|
|
301
364
|
const out: u8[] = []
|
|
@@ -306,6 +369,17 @@ const pgBytesOf = (text: string): u8[] => {
|
|
|
306
369
|
return out
|
|
307
370
|
}
|
|
308
371
|
|
|
372
|
+
/** How many bytes `value` takes in decimal, its sign included. */
|
|
373
|
+
const pgDecimalLength = (value: i64): i32 => {
|
|
374
|
+
let n: i32 = value < 0 ? 2 : 1
|
|
375
|
+
let v: i64 = value < 0 ? value : -value
|
|
376
|
+
while (v <= -10) {
|
|
377
|
+
v = v / 10
|
|
378
|
+
n += 1
|
|
379
|
+
}
|
|
380
|
+
return n
|
|
381
|
+
}
|
|
382
|
+
|
|
309
383
|
/** Whether `text` holds a NUL byte, which a protocol string cannot. */
|
|
310
384
|
const pgHasNul = (text: string): boolean => {
|
|
311
385
|
const n: i32 = toI32(text.length)
|
|
@@ -317,6 +391,61 @@ const pgHasNul = (text: string): boolean => {
|
|
|
317
391
|
return false
|
|
318
392
|
}
|
|
319
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Whether `b[from, end)` is UTF-8 (RFC 3629: shortest forms, no surrogates,
|
|
396
|
+
* nothing past U+10FFFF) with no NUL: what a `text` value can hold, so that
|
|
397
|
+
* the server accepts it.
|
|
398
|
+
*/
|
|
399
|
+
const pgIsText = (b: u8[], from: i32, end: i32): boolean => {
|
|
400
|
+
let i: i32 = from
|
|
401
|
+
while (i >= 0 && i < end && i < toI32(b.length)) {
|
|
402
|
+
const c: i32 = toI32(b[i])
|
|
403
|
+
if (c < 0x80) {
|
|
404
|
+
if (c === 0) {
|
|
405
|
+
return false
|
|
406
|
+
}
|
|
407
|
+
i += 1
|
|
408
|
+
continue
|
|
409
|
+
}
|
|
410
|
+
// The bytes after the lead, and the range of the first: the rest are 0x80-0xBF.
|
|
411
|
+
let more: i32 = 0
|
|
412
|
+
let low: i32 = 0x80
|
|
413
|
+
let high: i32 = 0xbf
|
|
414
|
+
if (c >= 0xc2 && c <= 0xdf) {
|
|
415
|
+
more = 1
|
|
416
|
+
} else if (c >= 0xe0 && c <= 0xef) {
|
|
417
|
+
more = 2
|
|
418
|
+
if (c === 0xe0) {
|
|
419
|
+
low = 0xa0
|
|
420
|
+
} else if (c === 0xed) {
|
|
421
|
+
high = 0x9f
|
|
422
|
+
}
|
|
423
|
+
} else if (c >= 0xf0 && c <= 0xf4) {
|
|
424
|
+
more = 3
|
|
425
|
+
if (c === 0xf0) {
|
|
426
|
+
low = 0x90
|
|
427
|
+
} else if (c === 0xf4) {
|
|
428
|
+
high = 0x8f
|
|
429
|
+
}
|
|
430
|
+
} else {
|
|
431
|
+
return false
|
|
432
|
+
}
|
|
433
|
+
if (more > end - i - 1) {
|
|
434
|
+
return false
|
|
435
|
+
}
|
|
436
|
+
for (let j: i32 = 1; j <= more && i + j < toI32(b.length); j++) {
|
|
437
|
+
const d: i32 = toI32(b[i + j])
|
|
438
|
+
if (d < low || d > high) {
|
|
439
|
+
return false
|
|
440
|
+
}
|
|
441
|
+
low = 0x80
|
|
442
|
+
high = 0xbf
|
|
443
|
+
}
|
|
444
|
+
i += more + 1
|
|
445
|
+
}
|
|
446
|
+
return true
|
|
447
|
+
}
|
|
448
|
+
|
|
320
449
|
/** The first `byte` in `b[from, end)`, or `end`. */
|
|
321
450
|
const pgFind = (b: u8[], from: i32, end: i32, byte: i32): i32 => {
|
|
322
451
|
if (from < 0 || end > toI32(b.length)) {
|
|
@@ -481,28 +610,33 @@ export class PgText {
|
|
|
481
610
|
this.failed = true
|
|
482
611
|
return -1
|
|
483
612
|
}
|
|
484
|
-
// The fixed part: YYYY-MM-DD HH:MM:SS
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
let ok: boolean = c >= 48 && c <= 57
|
|
488
|
-
if (i === 4 || i === 7) {
|
|
489
|
-
ok = c === 45
|
|
490
|
-
} else if (i === 10) {
|
|
491
|
-
ok = c === 32
|
|
492
|
-
} else if (i === 13 || i === 16) {
|
|
493
|
-
ok = c === 58
|
|
494
|
-
}
|
|
495
|
-
if (!ok) {
|
|
496
|
-
this.failed = true
|
|
497
|
-
return -1
|
|
498
|
-
}
|
|
499
|
-
}
|
|
613
|
+
// The fixed part: YYYY-MM-DD HH:MM:SS. Its five separators are where they
|
|
614
|
+
// belong and are not digits, so the other fourteen bytes are digits
|
|
615
|
+
// exactly when the part holds five non-digits.
|
|
500
616
|
let p: i32 = start + 19
|
|
501
617
|
const end: i32 = start + length
|
|
502
618
|
if (p < 0 || end > toI32(b.length)) {
|
|
503
619
|
this.failed = true
|
|
504
620
|
return -1
|
|
505
621
|
}
|
|
622
|
+
let others: i32 = 0
|
|
623
|
+
for (let i: i32 = start; i < p && i < toI32(b.length); i++) {
|
|
624
|
+
const c: i32 = toI32(b[i])
|
|
625
|
+
if (c < 48 || c > 57) {
|
|
626
|
+
others++
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (
|
|
630
|
+
others !== 5 ||
|
|
631
|
+
toI32(b[start + 4]) !== 45 ||
|
|
632
|
+
toI32(b[start + 7]) !== 45 ||
|
|
633
|
+
toI32(b[start + 10]) !== 32 ||
|
|
634
|
+
toI32(b[start + 13]) !== 58 ||
|
|
635
|
+
toI32(b[start + 16]) !== 58
|
|
636
|
+
) {
|
|
637
|
+
this.failed = true
|
|
638
|
+
return -1
|
|
639
|
+
}
|
|
506
640
|
let millis: i32 = 0
|
|
507
641
|
if (p < end && toI32(b[p]) === 46) {
|
|
508
642
|
p++
|
|
@@ -526,13 +660,13 @@ export class PgText {
|
|
|
526
660
|
if (p + 3 === end && toI32(b[p]) === 43 && toI32(b[p + 1]) === 48 && toI32(b[p + 2]) === 48) {
|
|
527
661
|
p = end
|
|
528
662
|
}
|
|
529
|
-
|
|
663
|
+
// The year's four bytes are digits, so their OR is '0' exactly when the year is 0000.
|
|
664
|
+
const year: i32 = toI32(b[start]) | toI32(b[start + 1]) | toI32(b[start + 2]) | toI32(b[start + 3])
|
|
665
|
+
if (p !== end || year === 48) {
|
|
530
666
|
this.failed = true
|
|
531
667
|
return -1
|
|
532
668
|
}
|
|
533
|
-
|
|
534
|
-
out[at + i] = b[start + i]
|
|
535
|
-
}
|
|
669
|
+
copyBytes(out, at, b, start, 19)
|
|
536
670
|
out[at + 10] = toU8(84)
|
|
537
671
|
out[at + 19] = toU8(46)
|
|
538
672
|
out[at + 20] = toU8(millis / 100 + 48)
|
|
@@ -727,9 +861,14 @@ export class PgConnection {
|
|
|
727
861
|
private groupOpen: boolean = false
|
|
728
862
|
/** Whether a statement in the group `PG_GROUP_DONE` just finished failed. */
|
|
729
863
|
groupFailed: boolean = false
|
|
864
|
+
// Whether the open bind opened its group, which `bindCancel` closes again.
|
|
865
|
+
private bindOpened: boolean = false
|
|
730
866
|
private bindAt: i32 = -1
|
|
731
867
|
private paramCountAt: i32 = 0
|
|
732
868
|
private paramCount: i32 = 0
|
|
869
|
+
// The open array parameter's length word, -1 when none is open, and its elements so far.
|
|
870
|
+
private arrayAt: i32 = -1
|
|
871
|
+
private arrayCount: i32 = 0
|
|
733
872
|
private clientNonce: string = ""
|
|
734
873
|
private serverSignature: u8[]
|
|
735
874
|
|
|
@@ -743,31 +882,7 @@ export class PgConnection {
|
|
|
743
882
|
if (toI32(nonce.length) < PG_NONCE_BYTES) {
|
|
744
883
|
panic("nish/net/postgres: the SCRAM nonce needs at least 18 random bytes")
|
|
745
884
|
}
|
|
746
|
-
|
|
747
|
-
panic("nish/net/postgres: PgConfig.user is required")
|
|
748
|
-
}
|
|
749
|
-
if (
|
|
750
|
-
config.maxMessage < 1024 ||
|
|
751
|
-
config.maxMessage > PG_MAX_CAP ||
|
|
752
|
-
config.maxOutput < 1024 ||
|
|
753
|
-
config.maxOutput > PG_MAX_CAP ||
|
|
754
|
-
config.maxInFlight < 1 ||
|
|
755
|
-
config.maxColumns < 1 ||
|
|
756
|
-
config.maxColumns > 65535 ||
|
|
757
|
-
config.minIterations < 1 ||
|
|
758
|
-
config.maxIterations < config.minIterations ||
|
|
759
|
-
config.maxIterations > PG_MAX_ITERATIONS
|
|
760
|
-
) {
|
|
761
|
-
panic("nish/net/postgres: a PgConfig cap out of range")
|
|
762
|
-
}
|
|
763
|
-
if (
|
|
764
|
-
pgHasNul(config.user) ||
|
|
765
|
-
pgHasNul(config.database) ||
|
|
766
|
-
pgHasNul(config.applicationName) ||
|
|
767
|
-
pgHasNul(config.options)
|
|
768
|
-
) {
|
|
769
|
-
panic("nish/net/postgres: a startup parameter holds a NUL byte")
|
|
770
|
-
}
|
|
885
|
+
pgCheckConfig(config)
|
|
771
886
|
this.config = config
|
|
772
887
|
this.text = new PgText()
|
|
773
888
|
this.password = new PgPassword(password)
|
|
@@ -909,8 +1024,15 @@ export class PgConnection {
|
|
|
909
1024
|
return PG_NEED_MORE
|
|
910
1025
|
}
|
|
911
1026
|
const at: i32 = this.inStart
|
|
912
|
-
const
|
|
913
|
-
|
|
1027
|
+
const b: u8[] = this.input
|
|
1028
|
+
// inStart and inEnd stay within the buffer, so this test never fails; it proves the indices.
|
|
1029
|
+
if (at < 0 || at + 4 >= toI32(b.length)) {
|
|
1030
|
+
return this.fail(PG_ERR_FRAME)
|
|
1031
|
+
}
|
|
1032
|
+
const type: i32 = toI32(b[at])
|
|
1033
|
+
// getI32BE written out, as in readRow: this runs once per message.
|
|
1034
|
+
const length: i32 =
|
|
1035
|
+
(toI32(b[at + 1]) << 24) | (toI32(b[at + 2]) << 16) | (toI32(b[at + 3]) << 8) | toI32(b[at + 4])
|
|
914
1036
|
if (length < 4) {
|
|
915
1037
|
return this.fail(PG_ERR_FRAME)
|
|
916
1038
|
}
|
|
@@ -1133,18 +1255,24 @@ export class PgConnection {
|
|
|
1133
1255
|
return this.fail(PG_ERR_TOO_BIG)
|
|
1134
1256
|
}
|
|
1135
1257
|
this.reserveColumns(n)
|
|
1258
|
+
const starts: i32[] = this.colStarts
|
|
1259
|
+
const lengths: i32[] = this.colLengths
|
|
1136
1260
|
let p: i32 = body + 2
|
|
1137
|
-
|
|
1138
|
-
|
|
1261
|
+
// reserveColumns made both arrays hold n; the length tests prove the indices.
|
|
1262
|
+
for (let c: i32 = 0; c < n && c < toI32(starts.length) && c < toI32(lengths.length); c++) {
|
|
1263
|
+
// The buffer tests never fail on a frame next() cut from it; they prove the indices.
|
|
1264
|
+
if (end - p < 4 || p < 0 || p + 3 >= toI32(b.length)) {
|
|
1139
1265
|
return this.fail(PG_ERR_FRAME)
|
|
1140
1266
|
}
|
|
1141
|
-
|
|
1267
|
+
// getI32BE written out: a call per column was a fifth of the row's cost.
|
|
1268
|
+
const length: i32 =
|
|
1269
|
+
(toI32(b[p]) << 24) | (toI32(b[p + 1]) << 16) | (toI32(b[p + 2]) << 8) | toI32(b[p + 3])
|
|
1142
1270
|
p += 4
|
|
1143
1271
|
if (length < -1 || length > end - p) {
|
|
1144
1272
|
return this.fail(PG_ERR_FRAME)
|
|
1145
1273
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1274
|
+
starts[c] = p
|
|
1275
|
+
lengths[c] = length
|
|
1148
1276
|
if (length > 0) {
|
|
1149
1277
|
p += length
|
|
1150
1278
|
}
|
|
@@ -1479,6 +1607,9 @@ export class PgConnection {
|
|
|
1479
1607
|
this.bindAt -= shift
|
|
1480
1608
|
this.paramCountAt -= shift
|
|
1481
1609
|
}
|
|
1610
|
+
if (this.arrayAt >= 0) {
|
|
1611
|
+
this.arrayAt -= shift
|
|
1612
|
+
}
|
|
1482
1613
|
}
|
|
1483
1614
|
|
|
1484
1615
|
/** Makes the output buffer at least `want` bytes long. */
|
|
@@ -1609,9 +1740,11 @@ export class PgConnection {
|
|
|
1609
1740
|
* order, then `bindEnd()`. False as `prepare` is.
|
|
1610
1741
|
*/
|
|
1611
1742
|
bindStart(name: string): boolean {
|
|
1743
|
+
const opens: boolean = !this.groupOpen
|
|
1612
1744
|
if (pgHasNul(name) || toI32(name.length) > PG_MAX_CAP || !this.canOpen(11 + toI32(name.length))) {
|
|
1613
1745
|
return false
|
|
1614
1746
|
}
|
|
1747
|
+
this.bindOpened = opens
|
|
1615
1748
|
this.bindAt = this.startMessage(PG_FE_BIND)
|
|
1616
1749
|
this.putByte(PG_ZERO)
|
|
1617
1750
|
this.putCString(name)
|
|
@@ -1628,6 +1761,7 @@ export class PgConnection {
|
|
|
1628
1761
|
* bind has 65535 parameters already, or there is no room under `maxOutput`.
|
|
1629
1762
|
*/
|
|
1630
1763
|
private startParam(size: i32): i32 {
|
|
1764
|
+
this.noArrayOpen()
|
|
1631
1765
|
if (this.bindAt < 0 || this.paramCount === 65535 || size > PG_MAX_CAP || !this.room(4 + size)) {
|
|
1632
1766
|
return -1
|
|
1633
1767
|
}
|
|
@@ -1641,6 +1775,12 @@ export class PgConnection {
|
|
|
1641
1775
|
setI32BE(this.out, at, this.outEnd - at - 4)
|
|
1642
1776
|
}
|
|
1643
1777
|
|
|
1778
|
+
private noArrayOpen(): void {
|
|
1779
|
+
if (this.arrayAt >= 0) {
|
|
1780
|
+
panic("nish/net/postgres: a parameter while an array is open")
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1644
1784
|
// Every `param...` call answers false, and writes nothing, when no bind is
|
|
1645
1785
|
// open, the bind has 65535 parameters already, or the parameter would pass
|
|
1646
1786
|
// `maxOutput`; a program that checked `bindStart()` meets only the last.
|
|
@@ -1658,6 +1798,7 @@ export class PgConnection {
|
|
|
1658
1798
|
|
|
1659
1799
|
/** A text parameter from `bytes[start, start + length)`, UTF-8; false too when the window is not inside `bytes`. */
|
|
1660
1800
|
paramBytes(bytes: u8[], start: i32, length: i32): boolean {
|
|
1801
|
+
this.noArrayOpen()
|
|
1661
1802
|
if (start < 0 || length < 0 || length > toI32(bytes.length) - start) {
|
|
1662
1803
|
return false
|
|
1663
1804
|
}
|
|
@@ -1687,6 +1828,13 @@ export class PgConnection {
|
|
|
1687
1828
|
if (at < 0) {
|
|
1688
1829
|
return false
|
|
1689
1830
|
}
|
|
1831
|
+
this.putDecimal(value)
|
|
1832
|
+
this.endParam(at)
|
|
1833
|
+
return true
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
/** `value` in decimal, `pgDecimalLength(value)` bytes. */
|
|
1837
|
+
private putDecimal(value: i64): void {
|
|
1690
1838
|
this.reserve(this.outEnd + 20)
|
|
1691
1839
|
// The digits are written from the least significant, then reversed; the
|
|
1692
1840
|
// value is made negative first so that the most negative i64 has its digits.
|
|
@@ -1713,6 +1861,39 @@ export class PgConnection {
|
|
|
1713
1861
|
lo++
|
|
1714
1862
|
hi--
|
|
1715
1863
|
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
/**
|
|
1867
|
+
* An `int8[]` parameter from `values[start, start + count)`, in decimal;
|
|
1868
|
+
* panics when the range is not inside `values`.
|
|
1869
|
+
*/
|
|
1870
|
+
paramIntArray(values: i64[], start: i32, count: i32): boolean {
|
|
1871
|
+
if (start < 0 || count < 0 || count > toI32(values.length) - start) {
|
|
1872
|
+
panic("nish/net/postgres: paramIntArray() outside its array")
|
|
1873
|
+
}
|
|
1874
|
+
// Each loop is bounded by `values.length` as well as by the window's end,
|
|
1875
|
+
// which the guard above keeps inside it: that names the bound the compiler
|
|
1876
|
+
// proves `values[i]` against, where the guard alone does not reach it.
|
|
1877
|
+
const end: i32 = start + count
|
|
1878
|
+
// Its exact size first, so that room is asked for once and a refusal writes nothing.
|
|
1879
|
+
let size: i64 = count > 0 ? toI64(count + 1) : toI64(2)
|
|
1880
|
+
for (let i: i32 = start; i < end && i < toI32(values.length); i++) {
|
|
1881
|
+
size += toI64(pgDecimalLength(values[i]))
|
|
1882
|
+
}
|
|
1883
|
+
const at: i32 = this.startParam(size > toI64(PG_MAX_CAP) ? PG_MAX_CAP + 1 : toI32(size))
|
|
1884
|
+
if (at < 0) {
|
|
1885
|
+
return false
|
|
1886
|
+
}
|
|
1887
|
+
this.putByte(PG_LBRACE)
|
|
1888
|
+
for (let i: i32 = start; i < end && i < toI32(values.length); i++) {
|
|
1889
|
+
// Read before the comma's call, which could drop the length fact.
|
|
1890
|
+
const value: i64 = values[i]
|
|
1891
|
+
if (i > start) {
|
|
1892
|
+
this.putByte(PG_COMMA)
|
|
1893
|
+
}
|
|
1894
|
+
this.putDecimal(value)
|
|
1895
|
+
}
|
|
1896
|
+
this.putByte(PG_RBRACE)
|
|
1716
1897
|
this.endParam(at)
|
|
1717
1898
|
return true
|
|
1718
1899
|
}
|
|
@@ -1758,6 +1939,124 @@ export class PgConnection {
|
|
|
1758
1939
|
return true
|
|
1759
1940
|
}
|
|
1760
1941
|
|
|
1942
|
+
// An array parameter built element by element: `arrayStart()`, then
|
|
1943
|
+
// `arrayElement`, `arrayInt` or `arrayNull` for each element, then `arrayEnd()`. An
|
|
1944
|
+
// element refused for room writes nothing and leaves the array open, so a
|
|
1945
|
+
// program that batches rows ends the array with the elements it has and
|
|
1946
|
+
// sends the rest in the next statement. While an array is open, every other
|
|
1947
|
+
// `param...` call and `bindEnd()` panic, as do an element or `arrayEnd()`
|
|
1948
|
+
// with none open and a second `arrayStart()`; `sync()`, `prepare`,
|
|
1949
|
+
// `closeStatement` and `bindStart` answer false, the bind being open.
|
|
1950
|
+
|
|
1951
|
+
/** Opens an array parameter, `{}` so far; false as the `param...` calls are. */
|
|
1952
|
+
arrayStart(): boolean {
|
|
1953
|
+
if (this.arrayAt >= 0) {
|
|
1954
|
+
panic("nish/net/postgres: arrayStart() while an array is open")
|
|
1955
|
+
}
|
|
1956
|
+
// Room for `{}`: the `}` is owed, and every element keeps room for it.
|
|
1957
|
+
const at: i32 = this.startParam(toI32(2))
|
|
1958
|
+
if (at < 0) {
|
|
1959
|
+
return false
|
|
1960
|
+
}
|
|
1961
|
+
this.putByte(PG_LBRACE)
|
|
1962
|
+
this.arrayAt = at
|
|
1963
|
+
this.arrayCount = 0
|
|
1964
|
+
return true
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
private arrayOpen(): void {
|
|
1968
|
+
if (this.arrayAt < 0) {
|
|
1969
|
+
panic("nish/net/postgres: an array element with no arrayStart()")
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
/** Whether the open array has room for an element of `size` bytes, its comma, and the `}` it owes. */
|
|
1974
|
+
private elementRoom(size: i64): boolean {
|
|
1975
|
+
const total: i64 = size + toI64(this.arrayCount > 0 ? 2 : 1)
|
|
1976
|
+
return total <= toI64(PG_MAX_CAP) && this.room(toI32(total))
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
/**
|
|
1980
|
+
* A text element from `bytes[start, start + length)`, UTF-8, quoted with
|
|
1981
|
+
* `"` and `\` escaped; false, and nothing written, when there is no room.
|
|
1982
|
+
* Panics when no array is open or the window is not inside `bytes`.
|
|
1983
|
+
*/
|
|
1984
|
+
arrayElement(bytes: u8[], start: i32, length: i32): boolean {
|
|
1985
|
+
this.arrayOpen()
|
|
1986
|
+
if (start < 0 || length < 0 || length > toI32(bytes.length) - start) {
|
|
1987
|
+
panic("nish/net/postgres: arrayElement() outside its source")
|
|
1988
|
+
}
|
|
1989
|
+
// Both loops are bounded by `bytes.length` too, as `paramIntArray`'s are.
|
|
1990
|
+
const end: i32 = start + length
|
|
1991
|
+
let escapes: i32 = 0
|
|
1992
|
+
for (let i: i32 = start; i < end && i < toI32(bytes.length); i++) {
|
|
1993
|
+
const c: i32 = toI32(bytes[i])
|
|
1994
|
+
if (c === PG_QUOTE || c === PG_BACKSLASH) {
|
|
1995
|
+
escapes += 1
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
if (!this.elementRoom(toI64(length) + toI64(escapes) + 2)) {
|
|
1999
|
+
return false
|
|
2000
|
+
}
|
|
2001
|
+
this.elementComma()
|
|
2002
|
+
this.putByte(PG_QUOTE)
|
|
2003
|
+
this.reserve(this.outEnd + length + escapes + 1)
|
|
2004
|
+
if (escapes === 0) {
|
|
2005
|
+
copyBytes(this.out, this.outEnd, bytes, start, length)
|
|
2006
|
+
this.outEnd += length
|
|
2007
|
+
} else {
|
|
2008
|
+
for (let i: i32 = start; i < end && i < toI32(bytes.length); i++) {
|
|
2009
|
+
const c: i32 = toI32(bytes[i])
|
|
2010
|
+
if (c === PG_QUOTE || c === PG_BACKSLASH) {
|
|
2011
|
+
this.putByte(PG_BACKSLASH)
|
|
2012
|
+
}
|
|
2013
|
+
this.putByte(c)
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
this.putByte(PG_QUOTE)
|
|
2017
|
+
return true
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
/** A NULL element; false, and nothing written, when there is no room. Panics when no array is open. */
|
|
2021
|
+
arrayNull(): boolean {
|
|
2022
|
+
this.arrayOpen()
|
|
2023
|
+
if (!this.elementRoom(toI64(4))) {
|
|
2024
|
+
return false
|
|
2025
|
+
}
|
|
2026
|
+
this.elementComma()
|
|
2027
|
+
this.putString("NULL")
|
|
2028
|
+
return true
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
private elementComma(): void {
|
|
2032
|
+
if (this.arrayCount > 0) {
|
|
2033
|
+
this.putByte(PG_COMMA)
|
|
2034
|
+
}
|
|
2035
|
+
this.arrayCount += 1
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
/** An integer element, in decimal; false, and nothing written, when there is no room. Panics when no array is open. */
|
|
2039
|
+
arrayInt(value: i64): boolean {
|
|
2040
|
+
this.arrayOpen()
|
|
2041
|
+
if (!this.elementRoom(toI64(pgDecimalLength(value)))) {
|
|
2042
|
+
return false
|
|
2043
|
+
}
|
|
2044
|
+
this.elementComma()
|
|
2045
|
+
this.putDecimal(value)
|
|
2046
|
+
return true
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
/** Ends the open array parameter; never refused for room, which its elements kept. Panics when no array is open. */
|
|
2050
|
+
arrayEnd(): void {
|
|
2051
|
+
if (this.arrayAt < 0) {
|
|
2052
|
+
panic("nish/net/postgres: arrayEnd() with no arrayStart()")
|
|
2053
|
+
}
|
|
2054
|
+
this.tailRoom(toI32(1))
|
|
2055
|
+
this.putByte(PG_RBRACE)
|
|
2056
|
+
this.endParam(this.arrayAt)
|
|
2057
|
+
this.arrayAt = -1
|
|
2058
|
+
}
|
|
2059
|
+
|
|
1761
2060
|
/**
|
|
1762
2061
|
* Ends the Bind (every result column in text) and queues its Execute, for
|
|
1763
2062
|
* every row. False, and nothing written, when no bind is open; never for
|
|
@@ -1767,6 +2066,9 @@ export class PgConnection {
|
|
|
1767
2066
|
if (this.bindAt < 0) {
|
|
1768
2067
|
return false
|
|
1769
2068
|
}
|
|
2069
|
+
if (this.arrayAt >= 0) {
|
|
2070
|
+
panic("nish/net/postgres: bindEnd() while an array is open")
|
|
2071
|
+
}
|
|
1770
2072
|
this.tailRoom(PG_TAIL)
|
|
1771
2073
|
const at: i32 = this.bindAt
|
|
1772
2074
|
setU16BE(this.out, this.paramCountAt, toU16(this.paramCount))
|
|
@@ -1780,6 +2082,30 @@ export class PgConnection {
|
|
|
1780
2082
|
return true
|
|
1781
2083
|
}
|
|
1782
2084
|
|
|
2085
|
+
/**
|
|
2086
|
+
* Drops the open bind, and the array open in it, unsent, so that a program
|
|
2087
|
+
* whose parameter was refused for room can send the group without it; a
|
|
2088
|
+
* group the bind opened is closed again. False when no bind is open.
|
|
2089
|
+
*/
|
|
2090
|
+
bindCancel(): boolean {
|
|
2091
|
+
if (this.bindAt < 0) {
|
|
2092
|
+
return false
|
|
2093
|
+
}
|
|
2094
|
+
this.dropBind()
|
|
2095
|
+
if (this.bindOpened) {
|
|
2096
|
+
this.groupOpen = false
|
|
2097
|
+
this.inFlight -= 1
|
|
2098
|
+
}
|
|
2099
|
+
return true
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// Drops the open bind's bytes, and the array open in it; the group is left as it is.
|
|
2103
|
+
private dropBind(): void {
|
|
2104
|
+
this.outEnd = this.bindAt
|
|
2105
|
+
this.bindAt = -1
|
|
2106
|
+
this.arrayAt = -1
|
|
2107
|
+
}
|
|
2108
|
+
|
|
1783
2109
|
/** Sync: ends the open group, which the server answers with `PG_GROUP_DONE`. False when no group is open or a bind is; never for room. */
|
|
1784
2110
|
sync(): boolean {
|
|
1785
2111
|
if (!this.groupOpen || this.bindAt >= 0) {
|
|
@@ -1795,8 +2121,7 @@ export class PgConnection {
|
|
|
1795
2121
|
/** Terminate: tells the server the connection is ending; close the socket once it is sent. A bind still open is dropped unsent. */
|
|
1796
2122
|
terminate(): void {
|
|
1797
2123
|
if (this.bindAt >= 0) {
|
|
1798
|
-
this.
|
|
1799
|
-
this.bindAt = -1
|
|
2124
|
+
this.dropBind()
|
|
1800
2125
|
}
|
|
1801
2126
|
this.tailRoom(toI32(5))
|
|
1802
2127
|
const at: i32 = this.startMessage(PG_FE_TERMINATE)
|
|
@@ -1873,3 +2198,962 @@ export class PgConnection {
|
|
|
1873
2198
|
return overflowed ? -1 : rows
|
|
1874
2199
|
}
|
|
1875
2200
|
}
|
|
2201
|
+
|
|
2202
|
+
// --- The pool ------------------------------------------------------------------
|
|
2203
|
+
|
|
2204
|
+
/** `PgPool.next(slot)`: the request `ticket` names will get no answer, because its connection failed first. */
|
|
2205
|
+
export const PG_LOST: i32 = 9
|
|
2206
|
+
/** `PgPool.dispatch()` and `PgPool.wanted()`: no slot. */
|
|
2207
|
+
export const PG_POOL_NONE: i32 = -1
|
|
2208
|
+
/** `PgPool.request()`: the queue is full, and the request was not taken. */
|
|
2209
|
+
export const PG_POOL_FULL: i32 = -2
|
|
2210
|
+
|
|
2211
|
+
/** The most slots a pool may have. */
|
|
2212
|
+
const PG_POOL_MAX_SLOTS: i32 = 1024
|
|
2213
|
+
/** The longest queue a pool may have. */
|
|
2214
|
+
const PG_POOL_MAX_QUEUE: i32 = 1048576
|
|
2215
|
+
/** The most groups a pooled connection may have in flight, which sizes each slot's ring. */
|
|
2216
|
+
const PG_POOL_MAX_IN_FLIGHT: i32 = 1024
|
|
2217
|
+
/** The most statements a pool may register. */
|
|
2218
|
+
const PG_POOL_MAX_STATEMENTS: i32 = 256
|
|
2219
|
+
|
|
2220
|
+
/** No connection: one is built the next time a request needs it. */
|
|
2221
|
+
const PG_SLOT_IDLE: i32 = 0
|
|
2222
|
+
/** A request needs a connection here: the carrier is to make a socket. */
|
|
2223
|
+
const PG_SLOT_WANTED: i32 = 1
|
|
2224
|
+
/** The connection is made and authenticating. */
|
|
2225
|
+
const PG_SLOT_STARTING: i32 = 2
|
|
2226
|
+
const PG_SLOT_READY: i32 = 3
|
|
2227
|
+
/** The connection failed: `next` is reporting the requests it took with it. */
|
|
2228
|
+
const PG_SLOT_DRAINING: i32 = 4
|
|
2229
|
+
|
|
2230
|
+
/** One of a pool's connections, and the groups it has in flight. */
|
|
2231
|
+
class PgPoolSlot {
|
|
2232
|
+
state: i32 = 0
|
|
2233
|
+
conn: PgConnection | null = null
|
|
2234
|
+
/** How many registered statements this connection has queued, and how many the server has described. */
|
|
2235
|
+
queued: i32 = 0
|
|
2236
|
+
described: i32 = 0
|
|
2237
|
+
/** The groups in flight, oldest first, in a ring: each one's ticket, and how many of the pool's own prepares lead it. */
|
|
2238
|
+
readonly tickets: i32[]
|
|
2239
|
+
readonly leads: i32[]
|
|
2240
|
+
head: i32 = 0
|
|
2241
|
+
count: i32 = 0
|
|
2242
|
+
|
|
2243
|
+
constructor(ring: i32) {
|
|
2244
|
+
this.tickets = new Array<i32>(ring)
|
|
2245
|
+
this.leads = new Array<i32>(ring)
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
push(ticket: i32, leads: i32): void {
|
|
2249
|
+
const at: i32 = (this.head + this.count) % toI32(this.tickets.length)
|
|
2250
|
+
this.tickets[at] = ticket
|
|
2251
|
+
this.leads[at] = leads
|
|
2252
|
+
this.count += 1
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
pop(): void {
|
|
2256
|
+
this.head = (this.head + 1) % toI32(this.tickets.length)
|
|
2257
|
+
this.count -= 1
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
/**
|
|
2262
|
+
* A pool of connections to one server, sans-IO like `PgConnection`: it makes
|
|
2263
|
+
* no socket and reads no clock, and says which slot needs a socket and when.
|
|
2264
|
+
* Its size is a cap, and no connection is made until a request needs one and
|
|
2265
|
+
* no ready connection has room.
|
|
2266
|
+
*
|
|
2267
|
+
* A request is a ticket from `request()`, queued in FIFO order. `dispatch()`
|
|
2268
|
+
* hands the queue's head to the ready connection with the fewest groups in
|
|
2269
|
+
* flight, never past `maxInFlight` on one; the program builds the request's
|
|
2270
|
+
* group on `connection(slot)` and ends it with `send()`, not `sync()`.
|
|
2271
|
+
* `next(slot)` then reads that connection's events, `ticket` naming the
|
|
2272
|
+
* request each is about. A connection that fails reports `PG_LOST` for each
|
|
2273
|
+
* request it took with it, then `PG_FAILED`, and is built again on its next
|
|
2274
|
+
* use. Statements given to `register` are prepared on each connection in the
|
|
2275
|
+
* same group as its first request, so a request may bind them at once.
|
|
2276
|
+
*/
|
|
2277
|
+
export class PgPool {
|
|
2278
|
+
/** The request the last `dispatch()`, or event from `next(slot)`, is about; -1 for none. */
|
|
2279
|
+
ticket: i32 = -1
|
|
2280
|
+
/** The number of slots, the most connections the pool makes. */
|
|
2281
|
+
readonly size: i32
|
|
2282
|
+
|
|
2283
|
+
private readonly config: PgConfig
|
|
2284
|
+
private readonly password: PgPassword
|
|
2285
|
+
private readonly slots: PgPoolSlot[]
|
|
2286
|
+
private readonly queue: i32[]
|
|
2287
|
+
private queueHead: i32 = 0
|
|
2288
|
+
private queueCount: i32 = 0
|
|
2289
|
+
private nextTicket: i32 = 0
|
|
2290
|
+
/** The slot whose request is being built, between `dispatch()` and `send()`. */
|
|
2291
|
+
private building: i32 = -1
|
|
2292
|
+
private started: boolean = false
|
|
2293
|
+
private readonly names: string[]
|
|
2294
|
+
private readonly sqls: string[]
|
|
2295
|
+
private readonly types: i32[][]
|
|
2296
|
+
|
|
2297
|
+
/**
|
|
2298
|
+
* A pool of up to `size` connections (1 to 1024) and up to `queueSize`
|
|
2299
|
+
* waiting requests (1 to 2^20). `password` is copied and kept until
|
|
2300
|
+
* `dispose()`, since a connection is made again after a failure. Panics as
|
|
2301
|
+
* `PgConnection` does on `config`, and on a `maxInFlight` over 1024.
|
|
2302
|
+
*/
|
|
2303
|
+
constructor(config: PgConfig, password: Secret<u8[]>, size: i32, queueSize: i32) {
|
|
2304
|
+
pgCheckConfig(config)
|
|
2305
|
+
if (size < 1 || size > PG_POOL_MAX_SLOTS) {
|
|
2306
|
+
panic("nish/net/postgres: a PgPool size out of range")
|
|
2307
|
+
}
|
|
2308
|
+
if (queueSize < 1 || queueSize > PG_POOL_MAX_QUEUE) {
|
|
2309
|
+
panic("nish/net/postgres: a PgPool queueSize out of range")
|
|
2310
|
+
}
|
|
2311
|
+
if (config.maxInFlight > PG_POOL_MAX_IN_FLIGHT) {
|
|
2312
|
+
panic("nish/net/postgres: a PgPool connection's maxInFlight is over 1024")
|
|
2313
|
+
}
|
|
2314
|
+
this.config = config
|
|
2315
|
+
this.password = new PgPassword(password)
|
|
2316
|
+
this.size = size
|
|
2317
|
+
this.slots = []
|
|
2318
|
+
for (let i: i32 = 0; i < size; i++) {
|
|
2319
|
+
this.slots.push(new PgPoolSlot(config.maxInFlight))
|
|
2320
|
+
}
|
|
2321
|
+
this.queue = new Array<i32>(queueSize)
|
|
2322
|
+
this.names = []
|
|
2323
|
+
this.sqls = []
|
|
2324
|
+
this.types = []
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
/**
|
|
2328
|
+
* Registers a statement for every connection to prepare before its first
|
|
2329
|
+
* request. False when a connection has been made already, `name` or `sql`
|
|
2330
|
+
* holds a NUL, or 256 are registered.
|
|
2331
|
+
*/
|
|
2332
|
+
register(name: string, sql: string, paramTypes: i32[]): boolean {
|
|
2333
|
+
if (
|
|
2334
|
+
this.started ||
|
|
2335
|
+
pgHasNul(name) ||
|
|
2336
|
+
pgHasNul(sql) ||
|
|
2337
|
+
toI32(this.names.length) === PG_POOL_MAX_STATEMENTS
|
|
2338
|
+
) {
|
|
2339
|
+
return false
|
|
2340
|
+
}
|
|
2341
|
+
const types: i32[] = []
|
|
2342
|
+
for (const oid of paramTypes) {
|
|
2343
|
+
types.push(oid)
|
|
2344
|
+
}
|
|
2345
|
+
this.names.push(name)
|
|
2346
|
+
this.sqls.push(sql)
|
|
2347
|
+
this.types.push(types)
|
|
2348
|
+
return true
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
/** Queues a request and answers its ticket, or `PG_POOL_FULL` when `queueSize` are waiting. */
|
|
2352
|
+
request(): i32 {
|
|
2353
|
+
const room: i32 = toI32(this.queue.length)
|
|
2354
|
+
if (this.queueCount === room) {
|
|
2355
|
+
return PG_POOL_FULL
|
|
2356
|
+
}
|
|
2357
|
+
const ticket: i32 = this.nextTicket
|
|
2358
|
+
this.nextTicket = ticket === 2147483647 ? 0 : ticket + 1
|
|
2359
|
+
this.queue[(this.queueHead + this.queueCount) % room] = ticket
|
|
2360
|
+
this.queueCount += 1
|
|
2361
|
+
return ticket
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
/** The number of requests waiting for a connection. */
|
|
2365
|
+
waiting(): i32 {
|
|
2366
|
+
return this.queueCount
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
/**
|
|
2370
|
+
* Hands the oldest waiting request to a ready connection with room, and
|
|
2371
|
+
* answers its slot, `ticket` set to the request: build its group on
|
|
2372
|
+
* `connection(slot)`, then `send()`. `PG_POOL_NONE` when nothing waits, a
|
|
2373
|
+
* request is still being built, or no ready connection has room; then a
|
|
2374
|
+
* connection may be wanted (`wanted()`): one more for every `maxInFlight`
|
|
2375
|
+
* waiting requests that the connections already starting cannot take. A
|
|
2376
|
+
* request handed out is the program's to end with `send()`: until then no
|
|
2377
|
+
* other is handed out.
|
|
2378
|
+
*/
|
|
2379
|
+
dispatch(): i32 {
|
|
2380
|
+
if (this.building >= 0 || this.queueCount === 0) {
|
|
2381
|
+
return PG_POOL_NONE
|
|
2382
|
+
}
|
|
2383
|
+
const cap: i32 = this.config.maxInFlight
|
|
2384
|
+
let best: i32 = -1
|
|
2385
|
+
let load: i32 = 0
|
|
2386
|
+
let starting: i32 = 0
|
|
2387
|
+
let idle: i32 = -1
|
|
2388
|
+
for (let s: i32 = 0; s < this.size; s++) {
|
|
2389
|
+
const slot: PgPoolSlot = this.slots[s]
|
|
2390
|
+
const conn: PgConnection | null = slot.conn
|
|
2391
|
+
if (slot.state === PG_SLOT_READY && conn !== null) {
|
|
2392
|
+
if (conn.inFlight < cap && (best < 0 || conn.inFlight < load)) {
|
|
2393
|
+
best = s
|
|
2394
|
+
load = conn.inFlight
|
|
2395
|
+
}
|
|
2396
|
+
} else if (slot.state === PG_SLOT_WANTED || slot.state === PG_SLOT_STARTING) {
|
|
2397
|
+
starting += 1
|
|
2398
|
+
} else if (slot.state === PG_SLOT_IDLE && idle < 0) {
|
|
2399
|
+
idle = s
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
if (best < 0) {
|
|
2403
|
+
if (idle >= 0 && toI64(this.queueCount) > toI64(starting) * toI64(cap)) {
|
|
2404
|
+
this.slots[idle].state = PG_SLOT_WANTED
|
|
2405
|
+
}
|
|
2406
|
+
return PG_POOL_NONE
|
|
2407
|
+
}
|
|
2408
|
+
const ticket: i32 = this.queue[this.queueHead]
|
|
2409
|
+
this.queueHead = (this.queueHead + 1) % toI32(this.queue.length)
|
|
2410
|
+
this.queueCount -= 1
|
|
2411
|
+
const slot: PgPoolSlot = this.slots[best]
|
|
2412
|
+
const conn: PgConnection = this.connection(best)
|
|
2413
|
+
let leads: i32 = 0
|
|
2414
|
+
while (slot.queued < toI32(this.names.length)) {
|
|
2415
|
+
const k: i32 = slot.queued
|
|
2416
|
+
if (!conn.prepare(this.names[k], this.sqls[k], this.types[k])) {
|
|
2417
|
+
break
|
|
2418
|
+
}
|
|
2419
|
+
slot.queued += 1
|
|
2420
|
+
leads += 1
|
|
2421
|
+
}
|
|
2422
|
+
slot.push(ticket, leads)
|
|
2423
|
+
this.building = best
|
|
2424
|
+
this.ticket = ticket
|
|
2425
|
+
return best
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
/**
|
|
2429
|
+
* Ends the group of the request `dispatch()` handed out. False when there
|
|
2430
|
+
* is none; when a bind is still open, so finish it and call again; and
|
|
2431
|
+
* when no message of the group was written, so the request is dropped and
|
|
2432
|
+
* gets no events (`PG_LOST` if its connection has failed).
|
|
2433
|
+
*/
|
|
2434
|
+
send(): boolean {
|
|
2435
|
+
const s: i32 = this.building
|
|
2436
|
+
if (s < 0) {
|
|
2437
|
+
return false
|
|
2438
|
+
}
|
|
2439
|
+
const slot: PgPoolSlot = this.slots[s]
|
|
2440
|
+
const conn: PgConnection = this.connection(s)
|
|
2441
|
+
if (conn.sync()) {
|
|
2442
|
+
this.building = -1
|
|
2443
|
+
return true
|
|
2444
|
+
}
|
|
2445
|
+
if (conn.inFlight < slot.count) {
|
|
2446
|
+
// No message opened the group. A failed connection reports the request
|
|
2447
|
+
// lost with the rest; a live one would never answer it, so it is dropped.
|
|
2448
|
+
if (conn.failure === PG_ERR_NONE) {
|
|
2449
|
+
slot.count -= 1
|
|
2450
|
+
}
|
|
2451
|
+
this.building = -1
|
|
2452
|
+
}
|
|
2453
|
+
return false
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
/** A slot that needs a socket, or `PG_POOL_NONE`: connect it, then call `connect(slot, nonce)`. */
|
|
2457
|
+
wanted(): i32 {
|
|
2458
|
+
for (let s: i32 = 0; s < this.size; s++) {
|
|
2459
|
+
if (this.slots[s].state === PG_SLOT_WANTED) {
|
|
2460
|
+
return s
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
return PG_POOL_NONE
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
/**
|
|
2467
|
+
* Makes the wanted `slot`'s connection, `nonce` as `PgConnection` takes it:
|
|
2468
|
+
* send its output once the socket is connected, and if the socket fails,
|
|
2469
|
+
* call `connection(slot).closed()`. Panics when `slot` is not wanted or the
|
|
2470
|
+
* pool is disposed.
|
|
2471
|
+
*/
|
|
2472
|
+
connect(slot: i32, nonce: u8[]): void {
|
|
2473
|
+
if (slot < 0 || slot >= this.size || this.slots[slot].state !== PG_SLOT_WANTED) {
|
|
2474
|
+
panic("nish/net/postgres: PgPool.connect on a slot that wants no connection")
|
|
2475
|
+
}
|
|
2476
|
+
if (this.password.gone) {
|
|
2477
|
+
panic("nish/net/postgres: PgPool.connect after dispose")
|
|
2478
|
+
}
|
|
2479
|
+
const s: PgPoolSlot = this.slots[slot]
|
|
2480
|
+
s.conn = new PgConnection(this.config, this.password.bytes, nonce)
|
|
2481
|
+
s.state = PG_SLOT_STARTING
|
|
2482
|
+
s.queued = 0
|
|
2483
|
+
s.described = 0
|
|
2484
|
+
s.head = 0
|
|
2485
|
+
s.count = 0
|
|
2486
|
+
this.started = true
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
/** The connection in `slot`, its buffers the carrier's to fill and drain; the last one made after it fails. Panics when none was made. */
|
|
2490
|
+
connection(slot: i32): PgConnection {
|
|
2491
|
+
const conn: PgConnection | null = slot < 0 || slot >= this.size ? null : this.slots[slot].conn
|
|
2492
|
+
if (conn === null) {
|
|
2493
|
+
panic("nish/net/postgres: PgPool.connection on a slot with no connection")
|
|
2494
|
+
}
|
|
2495
|
+
return conn
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
/**
|
|
2499
|
+
* The next event from `slot`'s input, as `PgConnection.next()` answers it,
|
|
2500
|
+
* `ticket` naming the request it is about (-1 for `PG_READY`, `PG_FAILED`,
|
|
2501
|
+
* and a notice while no request is in flight). The `PG_DESCRIBED` of a registered
|
|
2502
|
+
* statement is the pool's and is not answered. When the connection fails,
|
|
2503
|
+
* `PG_LOST` comes for each request in flight on it, oldest first, then
|
|
2504
|
+
* `PG_FAILED`: close its socket. A connection that fails before it is ready,
|
|
2505
|
+
* while no other is, takes the oldest waiting request with it, so that a
|
|
2506
|
+
* server that cannot be reached empties the queue rather than holds it.
|
|
2507
|
+
* `PG_NEED_MORE` once `PG_FAILED` was answered, until `connect` builds the
|
|
2508
|
+
* slot again. Panics as `connection(slot)` does.
|
|
2509
|
+
*/
|
|
2510
|
+
next(slot: i32): i32 {
|
|
2511
|
+
const conn: PgConnection = this.connection(slot)
|
|
2512
|
+
const s: PgPoolSlot = this.slots[slot]
|
|
2513
|
+
if (s.state === PG_SLOT_IDLE || s.state === PG_SLOT_WANTED) {
|
|
2514
|
+
this.ticket = -1
|
|
2515
|
+
return PG_NEED_MORE
|
|
2516
|
+
}
|
|
2517
|
+
while (s.state === PG_SLOT_STARTING || s.state === PG_SLOT_READY) {
|
|
2518
|
+
this.ticket = s.count > 0 ? s.tickets[s.head] : -1
|
|
2519
|
+
const event: i32 = conn.next()
|
|
2520
|
+
if (event === PG_READY) {
|
|
2521
|
+
s.state = PG_SLOT_READY
|
|
2522
|
+
} else if (event === PG_DESCRIBED && s.count > 0 && s.leads[s.head] > 0) {
|
|
2523
|
+
s.leads[s.head] -= 1
|
|
2524
|
+
s.described += 1
|
|
2525
|
+
continue
|
|
2526
|
+
} else if (event === PG_ERROR && s.count > 0 && s.leads[s.head] > 0) {
|
|
2527
|
+
// The rest of the group's prepares were skipped: the next request prepares them again.
|
|
2528
|
+
s.leads[s.head] = 0
|
|
2529
|
+
s.queued = s.described
|
|
2530
|
+
} else if (event === PG_GROUP_DONE) {
|
|
2531
|
+
s.pop()
|
|
2532
|
+
} else if (event === PG_FAILED) {
|
|
2533
|
+
if (s.state === PG_SLOT_STARTING && this.queueCount > 0 && !this.anyReady()) {
|
|
2534
|
+
s.push(this.queue[this.queueHead], toI32(0))
|
|
2535
|
+
this.queueHead = (this.queueHead + 1) % toI32(this.queue.length)
|
|
2536
|
+
this.queueCount -= 1
|
|
2537
|
+
}
|
|
2538
|
+
if (this.building === slot) {
|
|
2539
|
+
this.building = -1
|
|
2540
|
+
}
|
|
2541
|
+
s.state = PG_SLOT_DRAINING
|
|
2542
|
+
break
|
|
2543
|
+
}
|
|
2544
|
+
return event
|
|
2545
|
+
}
|
|
2546
|
+
if (s.state === PG_SLOT_DRAINING && s.count > 0) {
|
|
2547
|
+
this.ticket = s.tickets[s.head]
|
|
2548
|
+
s.pop()
|
|
2549
|
+
return PG_LOST
|
|
2550
|
+
}
|
|
2551
|
+
s.state = PG_SLOT_IDLE
|
|
2552
|
+
this.ticket = -1
|
|
2553
|
+
return PG_FAILED
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
private anyReady(): boolean {
|
|
2557
|
+
for (const slot of this.slots) {
|
|
2558
|
+
if (slot.state === PG_SLOT_READY) {
|
|
2559
|
+
return true
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
return false
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
/** Whether every byte of the pool's copy of the password is zero, as `PgConnection.passwordWiped()` reads its own. */
|
|
2566
|
+
passwordWiped(): boolean {
|
|
2567
|
+
return this.password.wiped()
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
/** Wipes the pool's password and every connection's; `connect` panics after it. */
|
|
2571
|
+
dispose(): void {
|
|
2572
|
+
if (!this.password.gone) {
|
|
2573
|
+
this.password.wipe()
|
|
2574
|
+
}
|
|
2575
|
+
for (const slot of this.slots) {
|
|
2576
|
+
const conn: PgConnection | null = slot.conn
|
|
2577
|
+
if (conn !== null) {
|
|
2578
|
+
conn.dispose()
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// --- Batching ------------------------------------------------------------------
|
|
2585
|
+
|
|
2586
|
+
/** `PgBatchConfig.keyType`: the keys are `int8`, given to `PgBatcher.addInt`. */
|
|
2587
|
+
export const PG_BATCH_INT: i32 = 0
|
|
2588
|
+
/** `PgBatchConfig.keyType`: the keys are `text`, given to `PgBatcher.addText`. */
|
|
2589
|
+
export const PG_BATCH_TEXT: i32 = 1
|
|
2590
|
+
|
|
2591
|
+
/** `PgBatcher.next(run)`: no event until the next row, `end` or `fail`. */
|
|
2592
|
+
export const PG_BATCH_NONE: i32 = 0
|
|
2593
|
+
/** `PgBatcher.next(run)`: the connection's current row answers the request `tag`. */
|
|
2594
|
+
export const PG_BATCH_ROW: i32 = 1
|
|
2595
|
+
/** `PgBatcher.next(run)`: the request `tag` has had all its rows, at least one. */
|
|
2596
|
+
export const PG_BATCH_DONE: i32 = 2
|
|
2597
|
+
/** `PgBatcher.next(run)`: no row answered the request `tag`'s key. */
|
|
2598
|
+
export const PG_BATCH_MISSING: i32 = 3
|
|
2599
|
+
/** `PgBatcher.next(run)`: the request `tag`'s execution failed, and its rows so far may be incomplete. */
|
|
2600
|
+
export const PG_BATCH_FAILED: i32 = 4
|
|
2601
|
+
|
|
2602
|
+
// `take` probes at most maxRequests^2 / 2 slots, so the cap keeps that near 5 * 10^5;
|
|
2603
|
+
// a probe whose hash matches in full compares the key's bytes, so the bytes
|
|
2604
|
+
// compared are at most that times the longest key, which `maxBytes` bounds.
|
|
2605
|
+
const PG_BATCH_MAX_REQUESTS: i32 = 1024
|
|
2606
|
+
const PG_BATCH_MAX_QUEUE: i32 = 1048576
|
|
2607
|
+
const PG_BATCH_MAX_RUNS: i32 = 1024
|
|
2608
|
+
// The takes a run's handles count before they wrap: handles stay under 2^30.
|
|
2609
|
+
const PG_BATCH_USES: i32 = 1048576
|
|
2610
|
+
// The array's `{}` and the longest int8 element, `-9223372036854775808`.
|
|
2611
|
+
const PG_BATCH_MIN_BYTES: i32 = 22
|
|
2612
|
+
|
|
2613
|
+
/** What a `PgBatcher` sends: one statement shape, its key, and its caps. */
|
|
2614
|
+
export class PgBatchConfig {
|
|
2615
|
+
/** The prepared statement's name, which `PgBatcher.sql` is prepared under. */
|
|
2616
|
+
name: string = ""
|
|
2617
|
+
/** The query for one key, `q.key`: `select id, title from articles where slug = q.key`. */
|
|
2618
|
+
select: string = ""
|
|
2619
|
+
/** Columns of `t`, the query's rows, ordering the rows of one key: `t.created_at desc`; "" for none. */
|
|
2620
|
+
orderBy: string = ""
|
|
2621
|
+
/** `PG_BATCH_INT` or `PG_BATCH_TEXT`. */
|
|
2622
|
+
keyType: i32 = 0
|
|
2623
|
+
/** The most requests one execution answers. */
|
|
2624
|
+
maxRequests: i32 = 32
|
|
2625
|
+
/** The most bytes the key array parameter takes, `{}` included. */
|
|
2626
|
+
maxBytes: i32 = 32768
|
|
2627
|
+
/** The most requests waiting. */
|
|
2628
|
+
queueSize: i32 = 1024
|
|
2629
|
+
/** The most bytes of text keys waiting. */
|
|
2630
|
+
queueBytes: i32 = 65536
|
|
2631
|
+
/** The most executions in flight at once. */
|
|
2632
|
+
runs: i32 = 16
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
/** One execution's requests, grouped by key, and how far its rows have been routed. */
|
|
2636
|
+
class PgBatchRun {
|
|
2637
|
+
/** Key `k`'s requests' tags are `tags[first[k], first[k + 1])`, in the order they were added. */
|
|
2638
|
+
readonly tags: i32[]
|
|
2639
|
+
readonly first: i32[]
|
|
2640
|
+
keys: i32 = 0
|
|
2641
|
+
/** The key rows are being read for, whether it has had one, and the next of its requests to answer. */
|
|
2642
|
+
cur: i32 = 0
|
|
2643
|
+
curRows: boolean = false
|
|
2644
|
+
at: i32 = 0
|
|
2645
|
+
/** The key whose `PG_BATCH_ROW` events are owed for the connection's current row; -1 for none. */
|
|
2646
|
+
target: i32 = -1
|
|
2647
|
+
/** The handle `take` gave for the run's execution, and how many takes it has had, mod `PG_BATCH_USES`. */
|
|
2648
|
+
handle: i32 = -1
|
|
2649
|
+
uses: i32 = 0
|
|
2650
|
+
open: boolean = false
|
|
2651
|
+
ended: boolean = false
|
|
2652
|
+
failed: boolean = false
|
|
2653
|
+
|
|
2654
|
+
constructor(maxRequests: i32) {
|
|
2655
|
+
this.tags = new Array<i32>(maxRequests)
|
|
2656
|
+
this.first = new Array<i32>(maxRequests + 1)
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
/** Opens the run for `keys` keys, none of whose rows have been read. */
|
|
2660
|
+
reset(keys: i32): void {
|
|
2661
|
+
this.open = true
|
|
2662
|
+
this.keys = keys
|
|
2663
|
+
this.cur = 0
|
|
2664
|
+
this.curRows = false
|
|
2665
|
+
this.at = 0
|
|
2666
|
+
this.target = -1
|
|
2667
|
+
this.ended = false
|
|
2668
|
+
this.failed = false
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
/**
|
|
2673
|
+
* Batches keyed reads of one statement shape, sans-IO like `PgConnection`:
|
|
2674
|
+
* it reads no clock and makes no socket. Each request is a key and the
|
|
2675
|
+
* caller's `tag`, queued by `addInt` or `addText`. When a connection is free,
|
|
2676
|
+
* `take(conn)` binds up to `maxRequests` of them, and up to `maxBytes` of
|
|
2677
|
+
* keys, as one execution of `sql`, each distinct key once, and answers the
|
|
2678
|
+
* execution's run handle. Each `PG_ROW` of that execution goes to
|
|
2679
|
+
* `row(run, conn)`, its `PG_COMPLETE` to `end(run)`, and an error or a lost
|
|
2680
|
+
* connection to `fail(run)`; after each, `next(run)` answers events until
|
|
2681
|
+
* `PG_BATCH_NONE`, `tag` naming the request each is about. Every row goes to
|
|
2682
|
+
* every request for its key, and every request gets one last event:
|
|
2683
|
+
* `PG_BATCH_DONE`, `PG_BATCH_MISSING` or `PG_BATCH_FAILED`.
|
|
2684
|
+
*
|
|
2685
|
+
* Nothing outlives its execution: a run is free again once the events
|
|
2686
|
+
* `end` or `fail` owe are answered, and its handle is then spent. What the
|
|
2687
|
+
* server or the network can send after that — a second `PG_COMPLETE`, a row
|
|
2688
|
+
* or an error after one, a lost connection — goes to a spent handle, and
|
|
2689
|
+
* `row`, `end` and `fail` count it in `stray` and do nothing, even once
|
|
2690
|
+
* `take` has given the run out again under a new handle. The ordinal each
|
|
2691
|
+
* row carries comes from the server and is checked before it routes
|
|
2692
|
+
* anything. What panics is only the program's own misuse: a handle out of
|
|
2693
|
+
* the range `take` can give (-1 or any negative, or `runs` * 2^20 or more),
|
|
2694
|
+
* or `row`, `end` or `fail` before `next(run)` has answered `PG_BATCH_NONE`
|
|
2695
|
+
* for the event before. A handle in that range that `take` never gave is
|
|
2696
|
+
* spent like any other: counted in `stray`, not a panic.
|
|
2697
|
+
*/
|
|
2698
|
+
export class PgBatcher {
|
|
2699
|
+
/** The prepared statement's name. */
|
|
2700
|
+
readonly name: string
|
|
2701
|
+
/** The statement to prepare under `name` (`PgPool.register` or `PgConnection.prepare`, no parameter types); each row's last column is the ordinal it routes by. */
|
|
2702
|
+
readonly sql: string
|
|
2703
|
+
// The queue, a ring: each request's tag, its key's hash and encoded size,
|
|
2704
|
+
// and the key: an int8, or a window of `store`, where text keys sit in the
|
|
2705
|
+
// order they were added, from the oldest's start to `storeEnd`.
|
|
2706
|
+
private readonly qTags: i32[]
|
|
2707
|
+
private readonly qHashes: i32[]
|
|
2708
|
+
private readonly qSizes: i32[]
|
|
2709
|
+
private readonly qInts: i64[]
|
|
2710
|
+
private readonly qStarts: i32[]
|
|
2711
|
+
private readonly qLengths: i32[]
|
|
2712
|
+
private readonly store: u8[]
|
|
2713
|
+
// `take`'s open-addressed table of the keys it has bound, each a key number
|
|
2714
|
+
// plus one, 0 for an empty slot; and for each key, its slot and its first request.
|
|
2715
|
+
private readonly table: i32[]
|
|
2716
|
+
private readonly keySlots: i32[]
|
|
2717
|
+
private readonly keyEntries: i32[]
|
|
2718
|
+
private readonly keyCounts: i32[]
|
|
2719
|
+
private readonly takenKeys: i32[]
|
|
2720
|
+
private readonly runs: PgBatchRun[]
|
|
2721
|
+
/** The request the last `next(run)` answered about; -1 for none. */
|
|
2722
|
+
tag: i32 = -1
|
|
2723
|
+
/** The number of runs failed by a bad ordinal (missing, NULL, not an integer, or naming no key still open), stopping at 2^31 - 1: each counts its first refused row, and the rows after it, dropped with the failed run, are not counted. */
|
|
2724
|
+
refused: i32 = 0
|
|
2725
|
+
/** The number of `row`, `end` and `fail` calls on a spent handle, each ignored, stopping at 2^31 - 1. */
|
|
2726
|
+
stray: i32 = 0
|
|
2727
|
+
private readonly keyType: i32
|
|
2728
|
+
private readonly maxRequests: i32
|
|
2729
|
+
private readonly maxBytes: i32
|
|
2730
|
+
private storeEnd: i32 = 0
|
|
2731
|
+
private head: i32 = 0
|
|
2732
|
+
private count: i32 = 0
|
|
2733
|
+
|
|
2734
|
+
/** A batcher for `config`; panics when a name or query is empty or holds a NUL, or a cap is out of range. */
|
|
2735
|
+
constructor(config: PgBatchConfig) {
|
|
2736
|
+
const keyType: i32 = config.keyType
|
|
2737
|
+
if (keyType !== PG_BATCH_INT && keyType !== PG_BATCH_TEXT) {
|
|
2738
|
+
panic("nish/net/postgres: a PgBatchConfig keyType out of range")
|
|
2739
|
+
}
|
|
2740
|
+
if (
|
|
2741
|
+
config.name.length === 0 ||
|
|
2742
|
+
config.select.length === 0 ||
|
|
2743
|
+
pgHasNul(config.name) ||
|
|
2744
|
+
pgHasNul(config.select) ||
|
|
2745
|
+
pgHasNul(config.orderBy)
|
|
2746
|
+
) {
|
|
2747
|
+
panic("nish/net/postgres: a PgBatchConfig name or query empty or with a NUL")
|
|
2748
|
+
}
|
|
2749
|
+
const maxRequests: i32 = config.maxRequests
|
|
2750
|
+
if (
|
|
2751
|
+
maxRequests < 1 ||
|
|
2752
|
+
maxRequests > PG_BATCH_MAX_REQUESTS ||
|
|
2753
|
+
config.maxBytes < PG_BATCH_MIN_BYTES ||
|
|
2754
|
+
config.maxBytes > PG_MAX_CAP ||
|
|
2755
|
+
config.queueSize < 1 ||
|
|
2756
|
+
config.queueSize > PG_BATCH_MAX_QUEUE ||
|
|
2757
|
+
config.queueBytes < 0 ||
|
|
2758
|
+
config.queueBytes > PG_MAX_CAP ||
|
|
2759
|
+
config.runs < 1 ||
|
|
2760
|
+
config.runs > PG_BATCH_MAX_RUNS
|
|
2761
|
+
) {
|
|
2762
|
+
panic("nish/net/postgres: a PgBatchConfig cap out of range")
|
|
2763
|
+
}
|
|
2764
|
+
this.keyType = keyType
|
|
2765
|
+
this.maxRequests = maxRequests
|
|
2766
|
+
this.maxBytes = config.maxBytes
|
|
2767
|
+
this.name = config.name
|
|
2768
|
+
const type: string = keyType === PG_BATCH_INT ? "int8" : "text"
|
|
2769
|
+
const order: string = config.orderBy.length > 0 ? `, ${config.orderBy}` : ""
|
|
2770
|
+
this.sql = `select t.*, q.ord from unnest($1::${type}[]) with ordinality as q(key, ord) cross join lateral (${config.select}) as t order by q.ord${order}`
|
|
2771
|
+
const queueSize: i32 = config.queueSize
|
|
2772
|
+
this.qTags = new Array<i32>(queueSize)
|
|
2773
|
+
this.qHashes = new Array<i32>(queueSize)
|
|
2774
|
+
this.qSizes = new Array<i32>(queueSize)
|
|
2775
|
+
const text: boolean = keyType === PG_BATCH_TEXT
|
|
2776
|
+
const intSize: i32 = text ? 0 : queueSize
|
|
2777
|
+
const textSize: i32 = text ? queueSize : 0
|
|
2778
|
+
const storeSize: i32 = text ? config.queueBytes : 0
|
|
2779
|
+
this.qInts = new Array<i64>(intSize)
|
|
2780
|
+
this.qStarts = new Array<i32>(textSize)
|
|
2781
|
+
this.qLengths = new Array<i32>(textSize)
|
|
2782
|
+
this.store = new Array<u8>(storeSize)
|
|
2783
|
+
// At least twice the keys a take can hold, a power of two for the mask.
|
|
2784
|
+
let tableSize: i32 = 2
|
|
2785
|
+
while (tableSize < 2 * maxRequests) {
|
|
2786
|
+
tableSize *= 2
|
|
2787
|
+
}
|
|
2788
|
+
this.table = new Array<i32>(tableSize)
|
|
2789
|
+
this.keySlots = new Array<i32>(maxRequests)
|
|
2790
|
+
this.keyEntries = new Array<i32>(maxRequests)
|
|
2791
|
+
this.keyCounts = new Array<i32>(maxRequests)
|
|
2792
|
+
this.takenKeys = new Array<i32>(maxRequests)
|
|
2793
|
+
this.runs = []
|
|
2794
|
+
for (let r: i32 = 0; r < config.runs; r++) {
|
|
2795
|
+
this.runs.push(new PgBatchRun(maxRequests))
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
/** The number of requests waiting for `take`. */
|
|
2800
|
+
waiting(): i32 {
|
|
2801
|
+
return this.count
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
/** Queues a request for the int8 `key`; false when `queueSize` are waiting. Panics on a text batcher. */
|
|
2805
|
+
addInt(key: i64, tag: i32): boolean {
|
|
2806
|
+
if (this.keyType !== PG_BATCH_INT) {
|
|
2807
|
+
panic("nish/net/postgres: PgBatcher.addInt on a text batcher")
|
|
2808
|
+
}
|
|
2809
|
+
const q: i32 = this.slotToAdd()
|
|
2810
|
+
if (q < 0) {
|
|
2811
|
+
return false
|
|
2812
|
+
}
|
|
2813
|
+
// Both halves folded to 31 bits, so that no step can overflow.
|
|
2814
|
+
const h: i32 = toI32(key & toI64(0x7fffffff)) ^ toI32((key >> toI64(31)) & toI64(0x7fffffff))
|
|
2815
|
+
this.qInts[q] = key
|
|
2816
|
+
this.added(q, tag, h ^ (h >> 15), pgDecimalLength(key))
|
|
2817
|
+
return true
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
/**
|
|
2821
|
+
* Queues a request for the text key `bytes[start, start + length)`,
|
|
2822
|
+
* which is copied; false when the key is not UTF-8 or holds a NUL (the
|
|
2823
|
+
* server would fail the whole execution over it), `queueSize` are waiting,
|
|
2824
|
+
* the key would not fit in `queueBytes` beside those waiting, or it is too
|
|
2825
|
+
* long for one execution's `maxBytes`. Panics on an int8 batcher, or when
|
|
2826
|
+
* the window is not inside `bytes`.
|
|
2827
|
+
*/
|
|
2828
|
+
addText(bytes: u8[], start: i32, length: i32, tag: i32): boolean {
|
|
2829
|
+
if (this.keyType !== PG_BATCH_TEXT) {
|
|
2830
|
+
panic("nish/net/postgres: PgBatcher.addText on an int8 batcher")
|
|
2831
|
+
}
|
|
2832
|
+
if (start < 0 || length < 0 || length > toI32(bytes.length) - start) {
|
|
2833
|
+
panic("nish/net/postgres: PgBatcher.addText outside its source")
|
|
2834
|
+
}
|
|
2835
|
+
const end: i32 = start + length
|
|
2836
|
+
if (!pgIsText(bytes, start, end)) {
|
|
2837
|
+
return false
|
|
2838
|
+
}
|
|
2839
|
+
// The element as `arrayElement` writes it: quoted, `"` and `\` escaped;
|
|
2840
|
+
// and FNV-1a, whose unsigned multiply wraps by definition.
|
|
2841
|
+
let size: i64 = toI64(length) + 2
|
|
2842
|
+
let h: u32 = 2166136261
|
|
2843
|
+
for (let i: i32 = start; i < end && i < toI32(bytes.length); i++) {
|
|
2844
|
+
const c: u8 = bytes[i]
|
|
2845
|
+
if (toI32(c) === PG_QUOTE || toI32(c) === PG_BACKSLASH) {
|
|
2846
|
+
size += 1
|
|
2847
|
+
}
|
|
2848
|
+
h = (h ^ toU32(c)) * 16777619
|
|
2849
|
+
}
|
|
2850
|
+
if (size + 2 > toI64(this.maxBytes)) {
|
|
2851
|
+
return false
|
|
2852
|
+
}
|
|
2853
|
+
const q: i32 = this.slotToAdd()
|
|
2854
|
+
if (q < 0) {
|
|
2855
|
+
return false
|
|
2856
|
+
}
|
|
2857
|
+
let at: i32 = this.storeEnd
|
|
2858
|
+
if (length > toI32(this.store.length) - at) {
|
|
2859
|
+
// The waiting keys are moved to the front, the room the taken ones left after them.
|
|
2860
|
+
// Refused first when even that would not make room, so a full store is not moved for nothing.
|
|
2861
|
+
const from: i32 = this.count > 0 ? this.qStarts[this.head] : at
|
|
2862
|
+
if (length > toI32(this.store.length) - (at - from)) {
|
|
2863
|
+
return false
|
|
2864
|
+
}
|
|
2865
|
+
copyBytes(this.store, 0, this.store, from, at - from)
|
|
2866
|
+
for (let i: i32 = 0; i < this.count; i++) {
|
|
2867
|
+
const w: i32 = (this.head + i) % toI32(this.qStarts.length)
|
|
2868
|
+
this.qStarts[w] -= from
|
|
2869
|
+
}
|
|
2870
|
+
at -= from
|
|
2871
|
+
this.storeEnd = at
|
|
2872
|
+
}
|
|
2873
|
+
copyBytes(this.store, at, bytes, start, length)
|
|
2874
|
+
this.storeEnd = at + length
|
|
2875
|
+
this.qStarts[q] = at
|
|
2876
|
+
this.qLengths[q] = length
|
|
2877
|
+
this.added(q, tag, toI32(h >> 1), toI32(size))
|
|
2878
|
+
return true
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
/** The ring slot the next request goes in, or -1 when the queue is full. */
|
|
2882
|
+
private slotToAdd(): i32 {
|
|
2883
|
+
const size: i32 = toI32(this.qTags.length)
|
|
2884
|
+
return this.count === size ? -1 : (this.head + this.count) % size
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
private added(q: i32, tag: i32, hash: i32, size: i32): void {
|
|
2888
|
+
this.qTags[q] = tag
|
|
2889
|
+
this.qHashes[q] = hash
|
|
2890
|
+
this.qSizes[q] = size
|
|
2891
|
+
this.count += 1
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
/** Whether the keys of the queued requests `a` and `b` are equal. */
|
|
2895
|
+
private sameKey(a: i32, b: i32): boolean {
|
|
2896
|
+
if (this.keyType === PG_BATCH_INT) {
|
|
2897
|
+
return this.qInts[a] === this.qInts[b]
|
|
2898
|
+
}
|
|
2899
|
+
const n: i32 = this.qLengths[a]
|
|
2900
|
+
if (n !== this.qLengths[b]) {
|
|
2901
|
+
return false
|
|
2902
|
+
}
|
|
2903
|
+
const x: i32 = this.qStarts[a]
|
|
2904
|
+
const y: i32 = this.qStarts[b]
|
|
2905
|
+
for (let i: i32 = 0; i < n; i++) {
|
|
2906
|
+
if (this.store[x + i] !== this.store[y + i]) {
|
|
2907
|
+
return false
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
return true
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
/**
|
|
2914
|
+
* Binds the oldest waiting requests on `conn` as one execution of `sql`,
|
|
2915
|
+
* prepared there already, and answers its run's handle, which names this
|
|
2916
|
+
* execution and no later one of the same run; the caller ends the group
|
|
2917
|
+
* (`sync()`, or `PgPool.send()`). It takes up to `maxRequests` requests,
|
|
2918
|
+
* oldest first, and stops at the first whose key would pass `maxBytes` or
|
|
2919
|
+
* the connection's `maxOutput`; those not taken stay queued. -1, and
|
|
2920
|
+
* nothing written, when none waits, every run is in flight, or `conn`
|
|
2921
|
+
* refused the bind or the first key.
|
|
2922
|
+
*/
|
|
2923
|
+
take(conn: PgConnection): i32 {
|
|
2924
|
+
if (this.count === 0) {
|
|
2925
|
+
return -1
|
|
2926
|
+
}
|
|
2927
|
+
let r: i32 = -1
|
|
2928
|
+
for (let i: i32 = 0; i < toI32(this.runs.length); i++) {
|
|
2929
|
+
if (!this.runs[i].open) {
|
|
2930
|
+
r = i
|
|
2931
|
+
break
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
if (r < 0 || !conn.bindStart(this.name)) {
|
|
2935
|
+
return -1
|
|
2936
|
+
}
|
|
2937
|
+
if (!conn.arrayStart()) {
|
|
2938
|
+
conn.bindCancel()
|
|
2939
|
+
return -1
|
|
2940
|
+
}
|
|
2941
|
+
const ring: i32 = toI32(this.qTags.length)
|
|
2942
|
+
const mask: i32 = toI32(this.table.length) - 1
|
|
2943
|
+
let keys: i32 = 0
|
|
2944
|
+
let taken: i32 = 0
|
|
2945
|
+
let bytes: i32 = 2
|
|
2946
|
+
while (taken < this.count && taken < this.maxRequests && taken < toI32(this.takenKeys.length)) {
|
|
2947
|
+
const q: i32 = (this.head + taken) % ring
|
|
2948
|
+
// Probe for the key: a slot holding it, or the empty slot it would take.
|
|
2949
|
+
let slot: i32 = this.qHashes[q] & mask
|
|
2950
|
+
let k: i32 = -1
|
|
2951
|
+
while (this.table[slot] > 0) {
|
|
2952
|
+
const seen: i32 = this.table[slot] - 1
|
|
2953
|
+
const e: i32 = this.keyEntries[seen]
|
|
2954
|
+
if (this.qHashes[e] === this.qHashes[q] && this.sameKey(e, q)) {
|
|
2955
|
+
k = seen
|
|
2956
|
+
break
|
|
2957
|
+
}
|
|
2958
|
+
slot = (slot + 1) & mask
|
|
2959
|
+
}
|
|
2960
|
+
if (k < 0) {
|
|
2961
|
+
const cost: i32 = this.qSizes[q] + (keys > 0 ? 1 : 0)
|
|
2962
|
+
if (cost > this.maxBytes - bytes || !this.bindKey(conn, q)) {
|
|
2963
|
+
break
|
|
2964
|
+
}
|
|
2965
|
+
bytes += cost
|
|
2966
|
+
k = keys
|
|
2967
|
+
keys += 1
|
|
2968
|
+
this.table[slot] = keys
|
|
2969
|
+
this.keySlots[k] = slot
|
|
2970
|
+
this.keyEntries[k] = q
|
|
2971
|
+
this.keyCounts[k] = 0
|
|
2972
|
+
}
|
|
2973
|
+
this.keyCounts[k] += 1
|
|
2974
|
+
this.takenKeys[taken] = k
|
|
2975
|
+
taken += 1
|
|
2976
|
+
}
|
|
2977
|
+
for (let k: i32 = 0; k < keys && k < toI32(this.keySlots.length); k++) {
|
|
2978
|
+
this.table[this.keySlots[k]] = 0
|
|
2979
|
+
}
|
|
2980
|
+
if (taken === 0) {
|
|
2981
|
+
conn.bindCancel()
|
|
2982
|
+
return -1
|
|
2983
|
+
}
|
|
2984
|
+
conn.arrayEnd()
|
|
2985
|
+
conn.bindEnd()
|
|
2986
|
+
// The requests grouped by key, each key's in the order they were added.
|
|
2987
|
+
const run: PgBatchRun = this.runs[r]
|
|
2988
|
+
run.first[0] = 0
|
|
2989
|
+
for (let k: i32 = 0; k < keys && k < toI32(this.keyCounts.length); k++) {
|
|
2990
|
+
run.first[k + 1] = run.first[k] + this.keyCounts[k]
|
|
2991
|
+
this.keyCounts[k] = run.first[k]
|
|
2992
|
+
}
|
|
2993
|
+
for (let i: i32 = 0; i < taken && i < toI32(this.takenKeys.length); i++) {
|
|
2994
|
+
const k: i32 = this.takenKeys[i]
|
|
2995
|
+
run.tags[this.keyCounts[k]] = this.qTags[(this.head + i) % ring]
|
|
2996
|
+
this.keyCounts[k] += 1
|
|
2997
|
+
}
|
|
2998
|
+
run.reset(keys)
|
|
2999
|
+
run.handle = run.uses * toI32(this.runs.length) + r
|
|
3000
|
+
run.uses = (run.uses + 1) % PG_BATCH_USES
|
|
3001
|
+
this.head = (this.head + taken) % ring
|
|
3002
|
+
this.count -= taken
|
|
3003
|
+
if (this.count === 0) {
|
|
3004
|
+
this.storeEnd = 0
|
|
3005
|
+
}
|
|
3006
|
+
return run.handle
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
/** Writes the queued request `q`'s key as the open array's next element; false when `conn` has no room. */
|
|
3010
|
+
private bindKey(conn: PgConnection, q: i32): boolean {
|
|
3011
|
+
if (this.keyType === PG_BATCH_INT) {
|
|
3012
|
+
return conn.arrayInt(this.qInts[q])
|
|
3013
|
+
}
|
|
3014
|
+
return conn.arrayElement(this.store, this.qStarts[q], this.qLengths[q])
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
/** The run handle `h` names while its execution's events are owed; null once it is spent. Panics on a handle `take` cannot give. */
|
|
3018
|
+
private run(h: i32): PgBatchRun | null {
|
|
3019
|
+
const n: i32 = toI32(this.runs.length)
|
|
3020
|
+
if (h < 0 || h >= n * PG_BATCH_USES) {
|
|
3021
|
+
panic("nish/net/postgres: a PgBatcher run out of range")
|
|
3022
|
+
}
|
|
3023
|
+
const run: PgBatchRun = this.runs[h % n]
|
|
3024
|
+
if (run.handle !== h || !run.open) {
|
|
3025
|
+
return null
|
|
3026
|
+
}
|
|
3027
|
+
return run
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
/** The run handle `h` names, not ended and with its events all answered; null, counted in `stray`, once it is spent. */
|
|
3031
|
+
private quietRun(h: i32, what: string): PgBatchRun | null {
|
|
3032
|
+
const run: PgBatchRun | null = this.run(h)
|
|
3033
|
+
if (run === null) {
|
|
3034
|
+
if (this.stray < 2147483647) {
|
|
3035
|
+
this.stray += 1
|
|
3036
|
+
}
|
|
3037
|
+
return null
|
|
3038
|
+
}
|
|
3039
|
+
if (run.target >= 0 || run.ended) {
|
|
3040
|
+
panic(`nish/net/postgres: PgBatcher.${what} before next(run) answered PG_BATCH_NONE`)
|
|
3041
|
+
}
|
|
3042
|
+
return run
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
/**
|
|
3046
|
+
* Routes `conn`'s current row, of run `r`'s execution, by the ordinal in
|
|
3047
|
+
* its last column: `next(r)` then answers `PG_BATCH_ROW` for each request
|
|
3048
|
+
* of its key, after the last events of the keys before it. False, and the
|
|
3049
|
+
* row dropped, when the run has failed; and when the ordinal is NULL, not
|
|
3050
|
+
* an integer, or names no key still open (one out of range, or one before
|
|
3051
|
+
* the key the last row was for): that counts in `refused` and fails the
|
|
3052
|
+
* run; and on a spent handle, which counts in `stray`. Panics on a handle
|
|
3053
|
+
* `take` cannot give, or when the run's events are not all answered.
|
|
3054
|
+
*/
|
|
3055
|
+
row(r: i32, conn: PgConnection): boolean {
|
|
3056
|
+
const run: PgBatchRun | null = this.quietRun(r, "row")
|
|
3057
|
+
if (run === null) {
|
|
3058
|
+
return false
|
|
3059
|
+
}
|
|
3060
|
+
if (run.failed) {
|
|
3061
|
+
return false
|
|
3062
|
+
}
|
|
3063
|
+
const last: i32 = conn.columns - 1
|
|
3064
|
+
let ordinal: i64 = 0
|
|
3065
|
+
let bad: boolean = last < 0 || conn.isNull(last)
|
|
3066
|
+
if (!bad) {
|
|
3067
|
+
// Read without touching `text.failed`, which belongs to the program's own decoding.
|
|
3068
|
+
const failed: boolean = conn.text.failed
|
|
3069
|
+
conn.text.failed = false
|
|
3070
|
+
ordinal = conn.int(last)
|
|
3071
|
+
bad = conn.text.failed
|
|
3072
|
+
conn.text.failed = failed
|
|
3073
|
+
}
|
|
3074
|
+
// Ordinals count from 1 and come in order: a row may name the current key again, never one before it.
|
|
3075
|
+
if (bad || ordinal < toI64(run.cur) + 1 || ordinal > toI64(run.keys)) {
|
|
3076
|
+
if (this.refused < 2147483647) {
|
|
3077
|
+
this.refused += 1
|
|
3078
|
+
}
|
|
3079
|
+
run.failed = true
|
|
3080
|
+
return false
|
|
3081
|
+
}
|
|
3082
|
+
run.target = toI32(ordinal) - 1
|
|
3083
|
+
return true
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
/** Ends run `r`'s execution, on its `PG_COMPLETE`: `next(r)` then closes every request left. Ignores a spent handle, and panics, as `row` does. */
|
|
3087
|
+
end(r: i32): void {
|
|
3088
|
+
const run: PgBatchRun | null = this.quietRun(r, "end")
|
|
3089
|
+
if (run !== null) {
|
|
3090
|
+
run.ended = true
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
/** Fails run `r`'s execution, on its `PG_ERROR` or `PG_LOST`: `next(r)` then answers `PG_BATCH_FAILED` for every request left. Ignores a spent handle, and panics, as `row` does. */
|
|
3095
|
+
fail(r: i32): void {
|
|
3096
|
+
const run: PgBatchRun | null = this.quietRun(r, "fail")
|
|
3097
|
+
if (run !== null) {
|
|
3098
|
+
run.failed = true
|
|
3099
|
+
run.ended = true
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
// Names the current key's next unanswered request as `tag`; false when it has none left.
|
|
3104
|
+
private emit(run: PgBatchRun): boolean {
|
|
3105
|
+
const at: i32 = run.first[run.cur] + run.at
|
|
3106
|
+
if (at >= run.first[run.cur + 1]) {
|
|
3107
|
+
return false
|
|
3108
|
+
}
|
|
3109
|
+
run.at += 1
|
|
3110
|
+
this.tag = run.tags[at]
|
|
3111
|
+
return true
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
/**
|
|
3115
|
+
* The next event `row`, `end` or `fail` owes on run `r`, with `tag` the
|
|
3116
|
+
* request it is about; `PG_BATCH_NONE`, and `tag` -1, when there is none.
|
|
3117
|
+
* Once `end` or `fail` has had all its events the run is over, free for
|
|
3118
|
+
* `take` again, and its handle spent: `PG_BATCH_NONE` ever after. Panics
|
|
3119
|
+
* on a handle `take` cannot give.
|
|
3120
|
+
*/
|
|
3121
|
+
next(r: i32): i32 {
|
|
3122
|
+
const found: PgBatchRun | null = this.run(r)
|
|
3123
|
+
this.tag = -1
|
|
3124
|
+
if (found === null) {
|
|
3125
|
+
return PG_BATCH_NONE
|
|
3126
|
+
}
|
|
3127
|
+
const run: PgBatchRun = found
|
|
3128
|
+
// The keys before the row's, or every key once the execution is over, have had all their rows.
|
|
3129
|
+
let limit: i32 = run.cur
|
|
3130
|
+
if (run.target >= 0) {
|
|
3131
|
+
limit = run.target
|
|
3132
|
+
} else if (run.ended) {
|
|
3133
|
+
limit = run.keys
|
|
3134
|
+
}
|
|
3135
|
+
while (run.cur < limit && run.cur < toI32(run.tags.length)) {
|
|
3136
|
+
if (this.emit(run)) {
|
|
3137
|
+
if (run.failed) {
|
|
3138
|
+
return PG_BATCH_FAILED
|
|
3139
|
+
}
|
|
3140
|
+
return run.curRows ? PG_BATCH_DONE : PG_BATCH_MISSING
|
|
3141
|
+
}
|
|
3142
|
+
run.cur += 1
|
|
3143
|
+
run.at = 0
|
|
3144
|
+
run.curRows = false
|
|
3145
|
+
}
|
|
3146
|
+
if (run.target >= 0 && run.cur < toI32(run.tags.length)) {
|
|
3147
|
+
run.curRows = true
|
|
3148
|
+
if (this.emit(run)) {
|
|
3149
|
+
return PG_BATCH_ROW
|
|
3150
|
+
}
|
|
3151
|
+
run.target = -1
|
|
3152
|
+
run.at = 0
|
|
3153
|
+
}
|
|
3154
|
+
if (run.ended && run.cur >= run.keys) {
|
|
3155
|
+
run.open = false
|
|
3156
|
+
}
|
|
3157
|
+
return PG_BATCH_NONE
|
|
3158
|
+
}
|
|
3159
|
+
}
|