@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.
@@ -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, its separators where they belong.
485
- for (let i: i32 = 0; i < 19; i++) {
486
- const c: i32 = toI32(b[start + i])
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
- if (p !== end || pgBytesAre(b, start, 4, "0000")) {
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
- for (let i: i32 = 0; i < 19; i++) {
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
- if (config.user.length === 0) {
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 type: i32 = toI32(this.input[at])
913
- const length: i32 = getI32BE(this.input, at + 1)
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
- for (let c: i32 = 0; c < n; c++) {
1138
- if (end - p < 4) {
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
- const length: i32 = getI32BE(b, p)
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
- this.colStarts[c] = p
1147
- this.colLengths[c] = length
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
+ }
@@ -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 (!stopping) {
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`, or `PREFORK_SPAWN_FAILED` when a worker's
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
  }