@amritk/nish-x86_64-linux 0.21.0 → 0.22.1
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 +18 -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 +2 -2
- 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 +471 -52
- package/std/net/prefork.ts +23 -3
- package/std/random.ts +66 -4
package/std/net/postgres.ts
CHANGED
|
@@ -90,6 +90,20 @@
|
|
|
90
90
|
* connection with their own `failure` code. The constructor panics on a cap
|
|
91
91
|
* out of range, as on a missing user or a short nonce.
|
|
92
92
|
*
|
|
93
|
+
* **Pools.** `PgPool` holds up to `size` connections to one server and a FIFO
|
|
94
|
+
* queue of up to `queueSize` requests, still sans-IO: `wanted()` names a slot
|
|
95
|
+
* that needs a socket, `connect(slot, nonce)` starts its connection, and the
|
|
96
|
+
* program moves its bytes as above through `connection(slot)`. A request is a
|
|
97
|
+
* ticket from `request()`; `dispatch()` gives the oldest to the ready
|
|
98
|
+
* connection with the fewest groups in flight, the program builds its group and
|
|
99
|
+
* ends it with `send()`, and `next(slot)` reads that connection's events with
|
|
100
|
+
* `ticket` naming the request. Statements given to `register` are prepared on
|
|
101
|
+
* each connection ahead of its first request. A connection that fails reports
|
|
102
|
+
* `PG_LOST` for each request it took, then `PG_FAILED`; the program closes the
|
|
103
|
+
* socket and the slot is built again on its next use. `tests/postgres/carrier.ts`
|
|
104
|
+
* is a whole carrier: one `pollWait` loop holding the pool's sockets beside an
|
|
105
|
+
* HTTP listener.
|
|
106
|
+
*
|
|
93
107
|
* Written from PostgreSQL's "Frontend/Backend Protocol" chapter (version 18)
|
|
94
108
|
* and RFC 5802 and RFC 7677; not ported from another implementation.
|
|
95
109
|
*/
|
|
@@ -296,6 +310,35 @@ class PgPassword {
|
|
|
296
310
|
}
|
|
297
311
|
}
|
|
298
312
|
|
|
313
|
+
/** Panics on a `PgConfig` that is a mistake in the program: no user, a cap out of range, a NUL in a startup parameter. */
|
|
314
|
+
const pgCheckConfig = (config: PgConfig): void => {
|
|
315
|
+
if (config.user.length === 0) {
|
|
316
|
+
panic("nish/net/postgres: PgConfig.user is required")
|
|
317
|
+
}
|
|
318
|
+
if (
|
|
319
|
+
config.maxMessage < 1024 ||
|
|
320
|
+
config.maxMessage > PG_MAX_CAP ||
|
|
321
|
+
config.maxOutput < 1024 ||
|
|
322
|
+
config.maxOutput > PG_MAX_CAP ||
|
|
323
|
+
config.maxInFlight < 1 ||
|
|
324
|
+
config.maxColumns < 1 ||
|
|
325
|
+
config.maxColumns > 65535 ||
|
|
326
|
+
config.minIterations < 1 ||
|
|
327
|
+
config.maxIterations < config.minIterations ||
|
|
328
|
+
config.maxIterations > PG_MAX_ITERATIONS
|
|
329
|
+
) {
|
|
330
|
+
panic("nish/net/postgres: a PgConfig cap out of range")
|
|
331
|
+
}
|
|
332
|
+
if (
|
|
333
|
+
pgHasNul(config.user) ||
|
|
334
|
+
pgHasNul(config.database) ||
|
|
335
|
+
pgHasNul(config.applicationName) ||
|
|
336
|
+
pgHasNul(config.options)
|
|
337
|
+
) {
|
|
338
|
+
panic("nish/net/postgres: a startup parameter holds a NUL byte")
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
299
342
|
/** The bytes of `text`. */
|
|
300
343
|
const pgBytesOf = (text: string): u8[] => {
|
|
301
344
|
const out: u8[] = []
|
|
@@ -481,28 +524,33 @@ export class PgText {
|
|
|
481
524
|
this.failed = true
|
|
482
525
|
return -1
|
|
483
526
|
}
|
|
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
|
-
}
|
|
527
|
+
// The fixed part: YYYY-MM-DD HH:MM:SS. Its five separators are where they
|
|
528
|
+
// belong and are not digits, so the other fourteen bytes are digits
|
|
529
|
+
// exactly when the part holds five non-digits.
|
|
500
530
|
let p: i32 = start + 19
|
|
501
531
|
const end: i32 = start + length
|
|
502
532
|
if (p < 0 || end > toI32(b.length)) {
|
|
503
533
|
this.failed = true
|
|
504
534
|
return -1
|
|
505
535
|
}
|
|
536
|
+
let others: i32 = 0
|
|
537
|
+
for (let i: i32 = start; i < p && i < toI32(b.length); i++) {
|
|
538
|
+
const c: i32 = toI32(b[i])
|
|
539
|
+
if (c < 48 || c > 57) {
|
|
540
|
+
others++
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (
|
|
544
|
+
others !== 5 ||
|
|
545
|
+
toI32(b[start + 4]) !== 45 ||
|
|
546
|
+
toI32(b[start + 7]) !== 45 ||
|
|
547
|
+
toI32(b[start + 10]) !== 32 ||
|
|
548
|
+
toI32(b[start + 13]) !== 58 ||
|
|
549
|
+
toI32(b[start + 16]) !== 58
|
|
550
|
+
) {
|
|
551
|
+
this.failed = true
|
|
552
|
+
return -1
|
|
553
|
+
}
|
|
506
554
|
let millis: i32 = 0
|
|
507
555
|
if (p < end && toI32(b[p]) === 46) {
|
|
508
556
|
p++
|
|
@@ -526,13 +574,13 @@ export class PgText {
|
|
|
526
574
|
if (p + 3 === end && toI32(b[p]) === 43 && toI32(b[p + 1]) === 48 && toI32(b[p + 2]) === 48) {
|
|
527
575
|
p = end
|
|
528
576
|
}
|
|
529
|
-
|
|
577
|
+
// The year's four bytes are digits, so their OR is '0' exactly when the year is 0000.
|
|
578
|
+
const year: i32 = toI32(b[start]) | toI32(b[start + 1]) | toI32(b[start + 2]) | toI32(b[start + 3])
|
|
579
|
+
if (p !== end || year === 48) {
|
|
530
580
|
this.failed = true
|
|
531
581
|
return -1
|
|
532
582
|
}
|
|
533
|
-
|
|
534
|
-
out[at + i] = b[start + i]
|
|
535
|
-
}
|
|
583
|
+
copyBytes(out, at, b, start, 19)
|
|
536
584
|
out[at + 10] = toU8(84)
|
|
537
585
|
out[at + 19] = toU8(46)
|
|
538
586
|
out[at + 20] = toU8(millis / 100 + 48)
|
|
@@ -743,31 +791,7 @@ export class PgConnection {
|
|
|
743
791
|
if (toI32(nonce.length) < PG_NONCE_BYTES) {
|
|
744
792
|
panic("nish/net/postgres: the SCRAM nonce needs at least 18 random bytes")
|
|
745
793
|
}
|
|
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
|
-
}
|
|
794
|
+
pgCheckConfig(config)
|
|
771
795
|
this.config = config
|
|
772
796
|
this.text = new PgText()
|
|
773
797
|
this.password = new PgPassword(password)
|
|
@@ -909,8 +933,15 @@ export class PgConnection {
|
|
|
909
933
|
return PG_NEED_MORE
|
|
910
934
|
}
|
|
911
935
|
const at: i32 = this.inStart
|
|
912
|
-
const
|
|
913
|
-
|
|
936
|
+
const b: u8[] = this.input
|
|
937
|
+
// inStart and inEnd stay within the buffer, so this test never fails; it proves the indices.
|
|
938
|
+
if (at < 0 || at + 4 >= toI32(b.length)) {
|
|
939
|
+
return this.fail(PG_ERR_FRAME)
|
|
940
|
+
}
|
|
941
|
+
const type: i32 = toI32(b[at])
|
|
942
|
+
// getI32BE written out, as in readRow: this runs once per message.
|
|
943
|
+
const length: i32 =
|
|
944
|
+
(toI32(b[at + 1]) << 24) | (toI32(b[at + 2]) << 16) | (toI32(b[at + 3]) << 8) | toI32(b[at + 4])
|
|
914
945
|
if (length < 4) {
|
|
915
946
|
return this.fail(PG_ERR_FRAME)
|
|
916
947
|
}
|
|
@@ -1133,18 +1164,24 @@ export class PgConnection {
|
|
|
1133
1164
|
return this.fail(PG_ERR_TOO_BIG)
|
|
1134
1165
|
}
|
|
1135
1166
|
this.reserveColumns(n)
|
|
1167
|
+
const starts: i32[] = this.colStarts
|
|
1168
|
+
const lengths: i32[] = this.colLengths
|
|
1136
1169
|
let p: i32 = body + 2
|
|
1137
|
-
|
|
1138
|
-
|
|
1170
|
+
// reserveColumns made both arrays hold n; the length tests prove the indices.
|
|
1171
|
+
for (let c: i32 = 0; c < n && c < toI32(starts.length) && c < toI32(lengths.length); c++) {
|
|
1172
|
+
// The buffer tests never fail on a frame next() cut from it; they prove the indices.
|
|
1173
|
+
if (end - p < 4 || p < 0 || p + 3 >= toI32(b.length)) {
|
|
1139
1174
|
return this.fail(PG_ERR_FRAME)
|
|
1140
1175
|
}
|
|
1141
|
-
|
|
1176
|
+
// getI32BE written out: a call per column was a fifth of the row's cost.
|
|
1177
|
+
const length: i32 =
|
|
1178
|
+
(toI32(b[p]) << 24) | (toI32(b[p + 1]) << 16) | (toI32(b[p + 2]) << 8) | toI32(b[p + 3])
|
|
1142
1179
|
p += 4
|
|
1143
1180
|
if (length < -1 || length > end - p) {
|
|
1144
1181
|
return this.fail(PG_ERR_FRAME)
|
|
1145
1182
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1183
|
+
starts[c] = p
|
|
1184
|
+
lengths[c] = length
|
|
1148
1185
|
if (length > 0) {
|
|
1149
1186
|
p += length
|
|
1150
1187
|
}
|
|
@@ -1873,3 +1910,385 @@ export class PgConnection {
|
|
|
1873
1910
|
return overflowed ? -1 : rows
|
|
1874
1911
|
}
|
|
1875
1912
|
}
|
|
1913
|
+
|
|
1914
|
+
// --- The pool ------------------------------------------------------------------
|
|
1915
|
+
|
|
1916
|
+
/** `PgPool.next(slot)`: the request `ticket` names will get no answer, because its connection failed first. */
|
|
1917
|
+
export const PG_LOST: i32 = 9
|
|
1918
|
+
/** `PgPool.dispatch()` and `PgPool.wanted()`: no slot. */
|
|
1919
|
+
export const PG_POOL_NONE: i32 = -1
|
|
1920
|
+
/** `PgPool.request()`: the queue is full, and the request was not taken. */
|
|
1921
|
+
export const PG_POOL_FULL: i32 = -2
|
|
1922
|
+
|
|
1923
|
+
/** The most slots a pool may have. */
|
|
1924
|
+
const PG_POOL_MAX_SLOTS: i32 = 1024
|
|
1925
|
+
/** The longest queue a pool may have. */
|
|
1926
|
+
const PG_POOL_MAX_QUEUE: i32 = 1048576
|
|
1927
|
+
/** The most groups a pooled connection may have in flight, which sizes each slot's ring. */
|
|
1928
|
+
const PG_POOL_MAX_IN_FLIGHT: i32 = 1024
|
|
1929
|
+
/** The most statements a pool may register. */
|
|
1930
|
+
const PG_POOL_MAX_STATEMENTS: i32 = 256
|
|
1931
|
+
|
|
1932
|
+
/** No connection: one is built the next time a request needs it. */
|
|
1933
|
+
const PG_SLOT_IDLE: i32 = 0
|
|
1934
|
+
/** A request needs a connection here: the carrier is to make a socket. */
|
|
1935
|
+
const PG_SLOT_WANTED: i32 = 1
|
|
1936
|
+
/** The connection is made and authenticating. */
|
|
1937
|
+
const PG_SLOT_STARTING: i32 = 2
|
|
1938
|
+
const PG_SLOT_READY: i32 = 3
|
|
1939
|
+
/** The connection failed: `next` is reporting the requests it took with it. */
|
|
1940
|
+
const PG_SLOT_DRAINING: i32 = 4
|
|
1941
|
+
|
|
1942
|
+
/** One of a pool's connections, and the groups it has in flight. */
|
|
1943
|
+
class PgPoolSlot {
|
|
1944
|
+
state: i32 = 0
|
|
1945
|
+
conn: PgConnection | null = null
|
|
1946
|
+
/** How many registered statements this connection has queued, and how many the server has described. */
|
|
1947
|
+
queued: i32 = 0
|
|
1948
|
+
described: i32 = 0
|
|
1949
|
+
/** The groups in flight, oldest first, in a ring: each one's ticket, and how many of the pool's own prepares lead it. */
|
|
1950
|
+
readonly tickets: i32[]
|
|
1951
|
+
readonly leads: i32[]
|
|
1952
|
+
head: i32 = 0
|
|
1953
|
+
count: i32 = 0
|
|
1954
|
+
|
|
1955
|
+
constructor(ring: i32) {
|
|
1956
|
+
this.tickets = new Array<i32>(ring)
|
|
1957
|
+
this.leads = new Array<i32>(ring)
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
push(ticket: i32, leads: i32): void {
|
|
1961
|
+
const at: i32 = (this.head + this.count) % toI32(this.tickets.length)
|
|
1962
|
+
this.tickets[at] = ticket
|
|
1963
|
+
this.leads[at] = leads
|
|
1964
|
+
this.count += 1
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
pop(): void {
|
|
1968
|
+
this.head = (this.head + 1) % toI32(this.tickets.length)
|
|
1969
|
+
this.count -= 1
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
/**
|
|
1974
|
+
* A pool of connections to one server, sans-IO like `PgConnection`: it makes
|
|
1975
|
+
* no socket and reads no clock, and says which slot needs a socket and when.
|
|
1976
|
+
* Its size is a cap, and no connection is made until a request needs one and
|
|
1977
|
+
* no ready connection has room.
|
|
1978
|
+
*
|
|
1979
|
+
* A request is a ticket from `request()`, queued in FIFO order. `dispatch()`
|
|
1980
|
+
* hands the queue's head to the ready connection with the fewest groups in
|
|
1981
|
+
* flight, never past `maxInFlight` on one; the program builds the request's
|
|
1982
|
+
* group on `connection(slot)` and ends it with `send()`, not `sync()`.
|
|
1983
|
+
* `next(slot)` then reads that connection's events, `ticket` naming the
|
|
1984
|
+
* request each is about. A connection that fails reports `PG_LOST` for each
|
|
1985
|
+
* request it took with it, then `PG_FAILED`, and is built again on its next
|
|
1986
|
+
* use. Statements given to `register` are prepared on each connection in the
|
|
1987
|
+
* same group as its first request, so a request may bind them at once.
|
|
1988
|
+
*/
|
|
1989
|
+
export class PgPool {
|
|
1990
|
+
/** The request the last `dispatch()`, or event from `next(slot)`, is about; -1 for none. */
|
|
1991
|
+
ticket: i32 = -1
|
|
1992
|
+
/** The number of slots, the most connections the pool makes. */
|
|
1993
|
+
readonly size: i32
|
|
1994
|
+
|
|
1995
|
+
private readonly config: PgConfig
|
|
1996
|
+
private readonly password: PgPassword
|
|
1997
|
+
private readonly slots: PgPoolSlot[]
|
|
1998
|
+
private readonly queue: i32[]
|
|
1999
|
+
private queueHead: i32 = 0
|
|
2000
|
+
private queueCount: i32 = 0
|
|
2001
|
+
private nextTicket: i32 = 0
|
|
2002
|
+
/** The slot whose request is being built, between `dispatch()` and `send()`. */
|
|
2003
|
+
private building: i32 = -1
|
|
2004
|
+
private started: boolean = false
|
|
2005
|
+
private readonly names: string[]
|
|
2006
|
+
private readonly sqls: string[]
|
|
2007
|
+
private readonly types: i32[][]
|
|
2008
|
+
|
|
2009
|
+
/**
|
|
2010
|
+
* A pool of up to `size` connections (1 to 1024) and up to `queueSize`
|
|
2011
|
+
* waiting requests (1 to 2^20). `password` is copied and kept until
|
|
2012
|
+
* `dispose()`, since a connection is made again after a failure. Panics as
|
|
2013
|
+
* `PgConnection` does on `config`, and on a `maxInFlight` over 1024.
|
|
2014
|
+
*/
|
|
2015
|
+
constructor(config: PgConfig, password: Secret<u8[]>, size: i32, queueSize: i32) {
|
|
2016
|
+
pgCheckConfig(config)
|
|
2017
|
+
if (size < 1 || size > PG_POOL_MAX_SLOTS) {
|
|
2018
|
+
panic("nish/net/postgres: a PgPool size out of range")
|
|
2019
|
+
}
|
|
2020
|
+
if (queueSize < 1 || queueSize > PG_POOL_MAX_QUEUE) {
|
|
2021
|
+
panic("nish/net/postgres: a PgPool queueSize out of range")
|
|
2022
|
+
}
|
|
2023
|
+
if (config.maxInFlight > PG_POOL_MAX_IN_FLIGHT) {
|
|
2024
|
+
panic("nish/net/postgres: a PgPool connection's maxInFlight is over 1024")
|
|
2025
|
+
}
|
|
2026
|
+
this.config = config
|
|
2027
|
+
this.password = new PgPassword(password)
|
|
2028
|
+
this.size = size
|
|
2029
|
+
this.slots = []
|
|
2030
|
+
for (let i: i32 = 0; i < size; i++) {
|
|
2031
|
+
this.slots.push(new PgPoolSlot(config.maxInFlight))
|
|
2032
|
+
}
|
|
2033
|
+
this.queue = new Array<i32>(queueSize)
|
|
2034
|
+
this.names = []
|
|
2035
|
+
this.sqls = []
|
|
2036
|
+
this.types = []
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
/**
|
|
2040
|
+
* Registers a statement for every connection to prepare before its first
|
|
2041
|
+
* request. False when a connection has been made already, `name` or `sql`
|
|
2042
|
+
* holds a NUL, or 256 are registered.
|
|
2043
|
+
*/
|
|
2044
|
+
register(name: string, sql: string, paramTypes: i32[]): boolean {
|
|
2045
|
+
if (
|
|
2046
|
+
this.started ||
|
|
2047
|
+
pgHasNul(name) ||
|
|
2048
|
+
pgHasNul(sql) ||
|
|
2049
|
+
toI32(this.names.length) === PG_POOL_MAX_STATEMENTS
|
|
2050
|
+
) {
|
|
2051
|
+
return false
|
|
2052
|
+
}
|
|
2053
|
+
const types: i32[] = []
|
|
2054
|
+
for (const oid of paramTypes) {
|
|
2055
|
+
types.push(oid)
|
|
2056
|
+
}
|
|
2057
|
+
this.names.push(name)
|
|
2058
|
+
this.sqls.push(sql)
|
|
2059
|
+
this.types.push(types)
|
|
2060
|
+
return true
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
/** Queues a request and answers its ticket, or `PG_POOL_FULL` when `queueSize` are waiting. */
|
|
2064
|
+
request(): i32 {
|
|
2065
|
+
const room: i32 = toI32(this.queue.length)
|
|
2066
|
+
if (this.queueCount === room) {
|
|
2067
|
+
return PG_POOL_FULL
|
|
2068
|
+
}
|
|
2069
|
+
const ticket: i32 = this.nextTicket
|
|
2070
|
+
this.nextTicket = ticket === 2147483647 ? 0 : ticket + 1
|
|
2071
|
+
this.queue[(this.queueHead + this.queueCount) % room] = ticket
|
|
2072
|
+
this.queueCount += 1
|
|
2073
|
+
return ticket
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
/** The number of requests waiting for a connection. */
|
|
2077
|
+
waiting(): i32 {
|
|
2078
|
+
return this.queueCount
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
/**
|
|
2082
|
+
* Hands the oldest waiting request to a ready connection with room, and
|
|
2083
|
+
* answers its slot, `ticket` set to the request: build its group on
|
|
2084
|
+
* `connection(slot)`, then `send()`. `PG_POOL_NONE` when nothing waits, a
|
|
2085
|
+
* request is still being built, or no ready connection has room; then a
|
|
2086
|
+
* connection may be wanted (`wanted()`): one more for every `maxInFlight`
|
|
2087
|
+
* waiting requests that the connections already starting cannot take. A
|
|
2088
|
+
* request handed out is the program's to end with `send()`: until then no
|
|
2089
|
+
* other is handed out.
|
|
2090
|
+
*/
|
|
2091
|
+
dispatch(): i32 {
|
|
2092
|
+
if (this.building >= 0 || this.queueCount === 0) {
|
|
2093
|
+
return PG_POOL_NONE
|
|
2094
|
+
}
|
|
2095
|
+
const cap: i32 = this.config.maxInFlight
|
|
2096
|
+
let best: i32 = -1
|
|
2097
|
+
let load: i32 = 0
|
|
2098
|
+
let starting: i32 = 0
|
|
2099
|
+
let idle: i32 = -1
|
|
2100
|
+
for (let s: i32 = 0; s < this.size; s++) {
|
|
2101
|
+
const slot: PgPoolSlot = this.slots[s]
|
|
2102
|
+
const conn: PgConnection | null = slot.conn
|
|
2103
|
+
if (slot.state === PG_SLOT_READY && conn !== null) {
|
|
2104
|
+
if (conn.inFlight < cap && (best < 0 || conn.inFlight < load)) {
|
|
2105
|
+
best = s
|
|
2106
|
+
load = conn.inFlight
|
|
2107
|
+
}
|
|
2108
|
+
} else if (slot.state === PG_SLOT_WANTED || slot.state === PG_SLOT_STARTING) {
|
|
2109
|
+
starting += 1
|
|
2110
|
+
} else if (slot.state === PG_SLOT_IDLE && idle < 0) {
|
|
2111
|
+
idle = s
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
if (best < 0) {
|
|
2115
|
+
if (idle >= 0 && toI64(this.queueCount) > toI64(starting) * toI64(cap)) {
|
|
2116
|
+
this.slots[idle].state = PG_SLOT_WANTED
|
|
2117
|
+
}
|
|
2118
|
+
return PG_POOL_NONE
|
|
2119
|
+
}
|
|
2120
|
+
const ticket: i32 = this.queue[this.queueHead]
|
|
2121
|
+
this.queueHead = (this.queueHead + 1) % toI32(this.queue.length)
|
|
2122
|
+
this.queueCount -= 1
|
|
2123
|
+
const slot: PgPoolSlot = this.slots[best]
|
|
2124
|
+
const conn: PgConnection = this.connection(best)
|
|
2125
|
+
let leads: i32 = 0
|
|
2126
|
+
while (slot.queued < toI32(this.names.length)) {
|
|
2127
|
+
const k: i32 = slot.queued
|
|
2128
|
+
if (!conn.prepare(this.names[k], this.sqls[k], this.types[k])) {
|
|
2129
|
+
break
|
|
2130
|
+
}
|
|
2131
|
+
slot.queued += 1
|
|
2132
|
+
leads += 1
|
|
2133
|
+
}
|
|
2134
|
+
slot.push(ticket, leads)
|
|
2135
|
+
this.building = best
|
|
2136
|
+
this.ticket = ticket
|
|
2137
|
+
return best
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
/**
|
|
2141
|
+
* Ends the group of the request `dispatch()` handed out. False when there
|
|
2142
|
+
* is none; when a bind is still open, so finish it and call again; and
|
|
2143
|
+
* when no message of the group was written, so the request is dropped and
|
|
2144
|
+
* gets no events (`PG_LOST` if its connection has failed).
|
|
2145
|
+
*/
|
|
2146
|
+
send(): boolean {
|
|
2147
|
+
const s: i32 = this.building
|
|
2148
|
+
if (s < 0) {
|
|
2149
|
+
return false
|
|
2150
|
+
}
|
|
2151
|
+
const slot: PgPoolSlot = this.slots[s]
|
|
2152
|
+
const conn: PgConnection = this.connection(s)
|
|
2153
|
+
if (conn.sync()) {
|
|
2154
|
+
this.building = -1
|
|
2155
|
+
return true
|
|
2156
|
+
}
|
|
2157
|
+
if (conn.inFlight < slot.count) {
|
|
2158
|
+
// No message opened the group. A failed connection reports the request
|
|
2159
|
+
// lost with the rest; a live one would never answer it, so it is dropped.
|
|
2160
|
+
if (conn.failure === PG_ERR_NONE) {
|
|
2161
|
+
slot.count -= 1
|
|
2162
|
+
}
|
|
2163
|
+
this.building = -1
|
|
2164
|
+
}
|
|
2165
|
+
return false
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
/** A slot that needs a socket, or `PG_POOL_NONE`: connect it, then call `connect(slot, nonce)`. */
|
|
2169
|
+
wanted(): i32 {
|
|
2170
|
+
for (let s: i32 = 0; s < this.size; s++) {
|
|
2171
|
+
if (this.slots[s].state === PG_SLOT_WANTED) {
|
|
2172
|
+
return s
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
return PG_POOL_NONE
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
/**
|
|
2179
|
+
* Makes the wanted `slot`'s connection, `nonce` as `PgConnection` takes it:
|
|
2180
|
+
* send its output once the socket is connected, and if the socket fails,
|
|
2181
|
+
* call `connection(slot).closed()`. Panics when `slot` is not wanted or the
|
|
2182
|
+
* pool is disposed.
|
|
2183
|
+
*/
|
|
2184
|
+
connect(slot: i32, nonce: u8[]): void {
|
|
2185
|
+
if (slot < 0 || slot >= this.size || this.slots[slot].state !== PG_SLOT_WANTED) {
|
|
2186
|
+
panic("nish/net/postgres: PgPool.connect on a slot that wants no connection")
|
|
2187
|
+
}
|
|
2188
|
+
if (this.password.gone) {
|
|
2189
|
+
panic("nish/net/postgres: PgPool.connect after dispose")
|
|
2190
|
+
}
|
|
2191
|
+
const s: PgPoolSlot = this.slots[slot]
|
|
2192
|
+
s.conn = new PgConnection(this.config, this.password.bytes, nonce)
|
|
2193
|
+
s.state = PG_SLOT_STARTING
|
|
2194
|
+
s.queued = 0
|
|
2195
|
+
s.described = 0
|
|
2196
|
+
s.head = 0
|
|
2197
|
+
s.count = 0
|
|
2198
|
+
this.started = true
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
/** 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. */
|
|
2202
|
+
connection(slot: i32): PgConnection {
|
|
2203
|
+
const conn: PgConnection | null = slot < 0 || slot >= this.size ? null : this.slots[slot].conn
|
|
2204
|
+
if (conn === null) {
|
|
2205
|
+
panic("nish/net/postgres: PgPool.connection on a slot with no connection")
|
|
2206
|
+
}
|
|
2207
|
+
return conn
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
/**
|
|
2211
|
+
* The next event from `slot`'s input, as `PgConnection.next()` answers it,
|
|
2212
|
+
* `ticket` naming the request it is about (-1 for `PG_READY`, `PG_FAILED`,
|
|
2213
|
+
* and a notice while no request is in flight). The `PG_DESCRIBED` of a registered
|
|
2214
|
+
* statement is the pool's and is not answered. When the connection fails,
|
|
2215
|
+
* `PG_LOST` comes for each request in flight on it, oldest first, then
|
|
2216
|
+
* `PG_FAILED`: close its socket. A connection that fails before it is ready,
|
|
2217
|
+
* while no other is, takes the oldest waiting request with it, so that a
|
|
2218
|
+
* server that cannot be reached empties the queue rather than holds it.
|
|
2219
|
+
* `PG_NEED_MORE` once `PG_FAILED` was answered, until `connect` builds the
|
|
2220
|
+
* slot again. Panics as `connection(slot)` does.
|
|
2221
|
+
*/
|
|
2222
|
+
next(slot: i32): i32 {
|
|
2223
|
+
const conn: PgConnection = this.connection(slot)
|
|
2224
|
+
const s: PgPoolSlot = this.slots[slot]
|
|
2225
|
+
if (s.state === PG_SLOT_IDLE || s.state === PG_SLOT_WANTED) {
|
|
2226
|
+
this.ticket = -1
|
|
2227
|
+
return PG_NEED_MORE
|
|
2228
|
+
}
|
|
2229
|
+
while (s.state === PG_SLOT_STARTING || s.state === PG_SLOT_READY) {
|
|
2230
|
+
this.ticket = s.count > 0 ? s.tickets[s.head] : -1
|
|
2231
|
+
const event: i32 = conn.next()
|
|
2232
|
+
if (event === PG_READY) {
|
|
2233
|
+
s.state = PG_SLOT_READY
|
|
2234
|
+
} else if (event === PG_DESCRIBED && s.count > 0 && s.leads[s.head] > 0) {
|
|
2235
|
+
s.leads[s.head] -= 1
|
|
2236
|
+
s.described += 1
|
|
2237
|
+
continue
|
|
2238
|
+
} else if (event === PG_ERROR && s.count > 0 && s.leads[s.head] > 0) {
|
|
2239
|
+
// The rest of the group's prepares were skipped: the next request prepares them again.
|
|
2240
|
+
s.leads[s.head] = 0
|
|
2241
|
+
s.queued = s.described
|
|
2242
|
+
} else if (event === PG_GROUP_DONE) {
|
|
2243
|
+
s.pop()
|
|
2244
|
+
} else if (event === PG_FAILED) {
|
|
2245
|
+
if (s.state === PG_SLOT_STARTING && this.queueCount > 0 && !this.anyReady()) {
|
|
2246
|
+
s.push(this.queue[this.queueHead], toI32(0))
|
|
2247
|
+
this.queueHead = (this.queueHead + 1) % toI32(this.queue.length)
|
|
2248
|
+
this.queueCount -= 1
|
|
2249
|
+
}
|
|
2250
|
+
if (this.building === slot) {
|
|
2251
|
+
this.building = -1
|
|
2252
|
+
}
|
|
2253
|
+
s.state = PG_SLOT_DRAINING
|
|
2254
|
+
break
|
|
2255
|
+
}
|
|
2256
|
+
return event
|
|
2257
|
+
}
|
|
2258
|
+
if (s.state === PG_SLOT_DRAINING && s.count > 0) {
|
|
2259
|
+
this.ticket = s.tickets[s.head]
|
|
2260
|
+
s.pop()
|
|
2261
|
+
return PG_LOST
|
|
2262
|
+
}
|
|
2263
|
+
s.state = PG_SLOT_IDLE
|
|
2264
|
+
this.ticket = -1
|
|
2265
|
+
return PG_FAILED
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
private anyReady(): boolean {
|
|
2269
|
+
for (const slot of this.slots) {
|
|
2270
|
+
if (slot.state === PG_SLOT_READY) {
|
|
2271
|
+
return true
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
return false
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
/** Whether every byte of the pool's copy of the password is zero, as `PgConnection.passwordWiped()` reads its own. */
|
|
2278
|
+
passwordWiped(): boolean {
|
|
2279
|
+
return this.password.wiped()
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
/** Wipes the pool's password and every connection's; `connect` panics after it. */
|
|
2283
|
+
dispose(): void {
|
|
2284
|
+
if (!this.password.gone) {
|
|
2285
|
+
this.password.wipe()
|
|
2286
|
+
}
|
|
2287
|
+
for (const slot of this.slots) {
|
|
2288
|
+
const conn: PgConnection | null = slot.conn
|
|
2289
|
+
if (conn !== null) {
|
|
2290
|
+
conn.dispose()
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
}
|
package/std/net/prefork.ts
CHANGED
|
@@ -84,6 +84,11 @@ export const PREFORK_STOPPED: i32 = -1
|
|
|
84
84
|
* supervisor could not listen for SIGTERM and so starts none.
|
|
85
85
|
*/
|
|
86
86
|
export const PREFORK_SPAWN_FAILED: i32 = -2
|
|
87
|
+
/**
|
|
88
|
+
* What `preforkWith` answers in the supervisor when `restart` is off and every
|
|
89
|
+
* worker has exited.
|
|
90
|
+
*/
|
|
91
|
+
export const PREFORK_WORKERS_EXITED: i32 = -3
|
|
87
92
|
|
|
88
93
|
/** The supervisor's numbers; every one is checked by `preforkWith`. */
|
|
89
94
|
export class PreforkConfig {
|
|
@@ -99,6 +104,11 @@ export class PreforkConfig {
|
|
|
99
104
|
quickExitMs: i32 = 1000
|
|
100
105
|
/** How long the workers get to exit after SIGTERM before SIGKILL. */
|
|
101
106
|
graceMs: i32 = 5000
|
|
107
|
+
/**
|
|
108
|
+
* Start a worker that exited again. Off, a worker that exits stays gone,
|
|
109
|
+
* and the supervisor returns once none is left.
|
|
110
|
+
*/
|
|
111
|
+
restart: boolean = true
|
|
102
112
|
}
|
|
103
113
|
|
|
104
114
|
/** True when `s` is one or more ASCII digits and nothing else. */
|
|
@@ -177,6 +187,8 @@ class PreforkSlot {
|
|
|
177
187
|
pid: i32 = 0
|
|
178
188
|
/** The wait before the last restart, 0 after a worker that ran long. */
|
|
179
189
|
delay: i32 = 0
|
|
190
|
+
/** The worker exited with `restart` off, and is never started again. */
|
|
191
|
+
gone: boolean = false
|
|
180
192
|
}
|
|
181
193
|
|
|
182
194
|
/** The supervisor's state while it runs. */
|
|
@@ -269,7 +281,9 @@ class PreforkSupervisor {
|
|
|
269
281
|
const slot: PreforkSlot = this.slots[index]
|
|
270
282
|
this.running = this.running - 1
|
|
271
283
|
slot.pid = 0
|
|
272
|
-
if (!
|
|
284
|
+
if (!this.config.restart) {
|
|
285
|
+
slot.gone = true
|
|
286
|
+
} else if (!stopping) {
|
|
273
287
|
this.backoff(slot, preforkNowMs(), false)
|
|
274
288
|
}
|
|
275
289
|
}
|
|
@@ -285,7 +299,7 @@ class PreforkSupervisor {
|
|
|
285
299
|
const now: i64 = preforkNowMs()
|
|
286
300
|
for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
|
|
287
301
|
const slot: PreforkSlot = this.slots[i]
|
|
288
|
-
if (slot.pid === 0 && slot.at <= now && !this.start(i)) {
|
|
302
|
+
if (slot.pid === 0 && !slot.gone && slot.at <= now && !this.start(i)) {
|
|
289
303
|
if (first) {
|
|
290
304
|
return false
|
|
291
305
|
}
|
|
@@ -355,7 +369,9 @@ const preforkInvalid = (config: PreforkConfig): string => {
|
|
|
355
369
|
|
|
356
370
|
/**
|
|
357
371
|
* In a worker, its index. In the supervisor, run the workers until SIGTERM or
|
|
358
|
-
* SIGINT and answer `PREFORK_STOPPED
|
|
372
|
+
* SIGINT and answer `PREFORK_STOPPED`; with `restart` off, also until every
|
|
373
|
+
* worker has exited, answering `PREFORK_WORKERS_EXITED`; or
|
|
374
|
+
* `PREFORK_SPAWN_FAILED` when a worker's
|
|
359
375
|
* first start failed (those already started are stopped first) or no signal
|
|
360
376
|
* listener could be made (no worker is started). Panics on a config
|
|
361
377
|
* `preforkInvalid` names a field of.
|
|
@@ -399,6 +415,10 @@ export const preforkWith = (args: string[], config: PreforkConfig): i32 => {
|
|
|
399
415
|
running = false
|
|
400
416
|
} else {
|
|
401
417
|
supervisor.reap(false)
|
|
418
|
+
if (!config.restart && supervisor.running === 0) {
|
|
419
|
+
result = PREFORK_WORKERS_EXITED
|
|
420
|
+
running = false
|
|
421
|
+
}
|
|
402
422
|
supervisor.restart(false)
|
|
403
423
|
}
|
|
404
424
|
}
|