@mark1russell7/service 0.5.11 → 0.5.13

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/build/pg.js ADDED
@@ -0,0 +1,531 @@
1
+ import { realClock } from "./clock.js";
2
+ import { B as zeroProblems, C as notSent, E as poolProblems, F as timeProblems, G as immediate, J as keepaliveSilent, K as keepaliveAnswered, L as wholeProblems, M as sendWithReissue, N as socketEnds, P as stepChecks, R as withinBound, S as ms, T as orZero, U as WHAT, V as MANIFESTATIONS, W as firstDetector, X as unwatched, Y as kernelGiveUp, _ as inFlight, a as helloDial, b as message, d as clientProblems, f as clientRecorder, h as finiteMs, j as roundTripCheck, l as attachClient, m as dialCheck, n as closeTimer, p as deadlineProblem, q as keepaliveReset, r as transition, s as STARTING_POLL, t as clientKnob, u as clientBounds, w as operationTable, y as localStateCheck } from "./machine-BhTRu3u9.js";
3
+ //#region src/pg/connection.ts
4
+ /**
5
+ * This factory makes a `pg.Pool`. It loads pg only when a client connects, thus a service that does
6
+ * not use PostgreSQL does not need pg. The specifier is a value, thus the package needs no types of pg.
7
+ */
8
+ const nodePostgres = async (options) => {
9
+ return new (await (import("pg"))).default.Pool(options);
10
+ };
11
+ //#endregion
12
+ //#region src/pg/meets.ts
13
+ /**
14
+ * This function gives the manifestations that a pg client can meet, and the detectors of each, from
15
+ * its knobs. The cells of S1 measured each one on node-postgres 8.23.1:
16
+ * - A reset of a query gives ECONNRESET in 0.6 ms, and a FIN to an idle client gives EOF in 18 µs.
17
+ * - A frozen server and a black hole have no detector in 60 s at the defaults: pg has no timer on a query and no keepalive.
18
+ * - `query_timeout` ends a query in a black hole on time (9.503 s for 9.5 s).
19
+ * - `statement_timeout` ends a slow statement on the server with ERROR 57014 (10.002 s for 10 s).
20
+ *
21
+ * pg has no watchdog and does not test an idle client. In place of a watchdog, the pool ends an
22
+ * idle client at its idle timer. A later checkout then opens a new connection, which `connect_bound` bounds.
23
+ */
24
+ const pgMeetings = (name, knobs) => {
25
+ const query = {
26
+ by: "request_timeout",
27
+ within: knobs.request_timeout,
28
+ situation: "in flight",
29
+ effect: `the query fails as indeterminate, and ${name} gives the client back with an error, thus the pool ends it`
30
+ };
31
+ const idleExpiry = {
32
+ by: "pool_idle_expiry",
33
+ within: knobs.pool_idle_expiry,
34
+ situation: "idle",
35
+ effect: "the pool ends the idle client, thus no checkout gets the dead connection"
36
+ };
37
+ const giveUp = kernelGiveUp(knobs.retrans_giveup_time, "the query fails with its error");
38
+ const silent = (manifestation) => ({
39
+ manifestation,
40
+ what: WHAT[manifestation],
41
+ detectors: [
42
+ query,
43
+ giveUp,
44
+ idleExpiry,
45
+ keepaliveSilent(knobs.keepalive_idle)
46
+ ]
47
+ });
48
+ return [
49
+ {
50
+ manifestation: "rst",
51
+ what: WHAT.rst,
52
+ detectors: [
53
+ immediate("in flight", "the query fails with ECONNRESET, and each new connect is refused"),
54
+ idleExpiry,
55
+ keepaliveReset(knobs.keepalive_idle)
56
+ ]
57
+ },
58
+ {
59
+ manifestation: "fin",
60
+ what: WHAT.fin,
61
+ detectors: [immediate("in flight", "the query fails: 'Connection terminated unexpectedly'"), immediate("idle", "the pool removes the idle client, and emits error")]
62
+ },
63
+ silent("blackhole"),
64
+ silent("freeze-host"),
65
+ {
66
+ manifestation: "freeze-process",
67
+ what: WHAT["freeze-process"],
68
+ detectors: [
69
+ query,
70
+ idleExpiry,
71
+ keepaliveAnswered()
72
+ ]
73
+ },
74
+ {
75
+ manifestation: "app-stall",
76
+ what: "the server stops its replies for a time, for example behind a lock, and its kernel stays up",
77
+ detectors: [
78
+ {
79
+ by: "statement_timeout",
80
+ within: knobs.statement_timeout,
81
+ situation: "in flight",
82
+ effect: "the server ends the statement with ERROR 57014, a certain outcome, and the client stays"
83
+ },
84
+ query,
85
+ idleExpiry,
86
+ keepaliveAnswered()
87
+ ]
88
+ }
89
+ ];
90
+ };
91
+ //#endregion
92
+ //#region src/pg/client.ts
93
+ /** This function gives the problems of a spec: the knobs that pg cannot use. */
94
+ const specProblems = (name, spec) => {
95
+ const { knobs } = spec;
96
+ return [
97
+ ...timeProblems(name, knobs),
98
+ ...zeroProblems(name, "pg", knobs, {
99
+ bounded: [
100
+ "connect_bound",
101
+ "request_timeout",
102
+ "close_bound"
103
+ ],
104
+ open: [
105
+ "statement_timeout",
106
+ "pool_idle_expiry",
107
+ "keepalive_idle",
108
+ "max_connection_age"
109
+ ]
110
+ }),
111
+ ...wholeProblems(name, knobs, {
112
+ millis: [
113
+ "connect_bound",
114
+ "request_timeout",
115
+ "statement_timeout",
116
+ "pool_idle_expiry"
117
+ ],
118
+ seconds: ["keepalive_idle", "max_connection_age"]
119
+ }),
120
+ ...poolProblems(name, spec.pool)
121
+ ];
122
+ };
123
+ /** This function gives the options of `pg.Pool` from a spec. Each timer of pg comes from the declaration. */
124
+ const poolOptions = (name, spec) => {
125
+ const { knobs } = spec;
126
+ return {
127
+ host: spec.host,
128
+ port: spec.port,
129
+ ...spec.database === void 0 ? {} : { database: spec.database },
130
+ ...spec.user === void 0 ? {} : { user: spec.user },
131
+ ...spec.password === void 0 ? {} : { password: spec.password },
132
+ application_name: name,
133
+ max: spec.pool.max,
134
+ min: spec.pool.min,
135
+ connectionTimeoutMillis: ms(knobs.connect_bound),
136
+ query_timeout: ms(knobs.request_timeout),
137
+ statement_timeout: finiteMs(knobs.statement_timeout),
138
+ idleTimeoutMillis: orZero(knobs.pool_idle_expiry),
139
+ maxLifetimeSeconds: orZero(knobs.max_connection_age) / 1e3,
140
+ keepAlive: knobs.keepalive_idle.value !== "infinite",
141
+ keepAliveInitialDelayMillis: orZero(knobs.keepalive_idle),
142
+ allowExitOnIdle: true
143
+ };
144
+ };
145
+ /**
146
+ * This dial function opens a TCP connection, sends an SSLRequest, and waits for its one byte. The
147
+ * server answers it before the authentication, thus the dial needs no password and opens no backend.
148
+ */
149
+ const sslRequestDial = helloDial(Uint8Array.of(0, 0, 0, 8, 4, 210, 22, 47), (received) => {
150
+ const answer = String.fromCharCode(received[0] ?? 0);
151
+ return answer === "S" || answer === "N" ? null : /* @__PURE__ */ new Error(`the answer to the SSLRequest was ${JSON.stringify(answer)}`);
152
+ });
153
+ /** A database error is an ErrorResponse of the server: pg gives its severity and its SQLSTATE code. */
154
+ const replyOf = (error) => {
155
+ if (typeof error !== "object" || error === null) return null;
156
+ const { severity, code } = error;
157
+ return typeof severity === "string" && typeof code === "string" && /^[0-9A-Z]{5}$/.test(code) ? {
158
+ severity,
159
+ code
160
+ } : null;
161
+ };
162
+ /**
163
+ * This function makes a PostgreSQL client, a dependency of a service over a `pg.Pool`.
164
+ * - It sets each timer of pg from the declaration. The driver sends nothing again by itself, and the client
165
+ * sends again only an idempotent operation of its table.
166
+ * - It tells the outcome of a query in three values. An ERROR of the server is certain. A FATAL
167
+ * ends the session, thus its outcome is not certain.
168
+ * - It gives back a client whose query has no certain outcome with an error. The pool then ends it,
169
+ * thus an orphaned query does not hold the next checkout.
170
+ * - It listens to each error of the pool and of a client that it holds. In Node, an error with no
171
+ * listener throws, and library code does not end the process (A5).
172
+ * - It records the process id of the backend of each connection (D1).
173
+ *
174
+ * The outcomes, the second try, the queries in flight, the bounded close and the checks come from
175
+ * the core of the clients. This entry gives what pg does in its own way.
176
+ */
177
+ const pgClient = (name, spec, options = {}) => {
178
+ const problems = specProblems(name, spec);
179
+ if (problems.length > 0) throw new Error(problems.join("; "));
180
+ const { knobs } = spec;
181
+ const clock = options.clock ?? realClock;
182
+ const create = options.create ?? nodePostgres;
183
+ const dial = options.dial ?? sslRequestDial;
184
+ const operations = operationTable({}, spec.operations);
185
+ const requestTimeout = ms(knobs.request_timeout);
186
+ let pool = null;
187
+ let status = "none";
188
+ const joins = [];
189
+ const open = /* @__PURE__ */ new Set();
190
+ const flight = inFlight();
191
+ const emit = clientRecorder(options.record, clock, name);
192
+ const record = (on, to = status, detail) => {
193
+ const from = status;
194
+ status = to;
195
+ emit(from, to, on, detail);
196
+ };
197
+ const closing = () => status === "closing" || status === "end";
198
+ const onConnect = (client) => {
199
+ open.add(client);
200
+ const generation = joins.length + 1;
201
+ const pid = client.processID;
202
+ joins.push({
203
+ generation,
204
+ at: clock.now(),
205
+ backendPid: typeof pid === "number" ? pid : null,
206
+ ...socketEnds(client.connection?.stream)
207
+ });
208
+ record("join", closing() ? status : "ready", `generation ${generation}, backend ${pid ?? "unknown"}`);
209
+ };
210
+ const onRemove = (client) => {
211
+ open.delete(client);
212
+ record("client.removed");
213
+ };
214
+ const onPoolError = (error) => {
215
+ if (status !== "end") record("client.error", status, message(error));
216
+ };
217
+ /** This function records a failed checkout: a full pool at its bound, or a connect that failed. */
218
+ const checkoutFailed = (error) => {
219
+ const text = message(error);
220
+ if (/timeout exceeded when trying to connect/i.test(text)) record("checkout.timeout", status, text);
221
+ else record("connect.failed", closing() ? status : "failing", text);
222
+ };
223
+ /** This function checks out a client, or gives the reason why the query cannot leave the client. */
224
+ const checkout = (signal) => {
225
+ const p = pool;
226
+ if (p === null) return Promise.resolve(notSent(`${name} has not connected`));
227
+ if (closing()) return Promise.resolve(notSent(`${name} is ${status}: the query did not leave the client`));
228
+ if (signal?.aborted) return Promise.resolve(notSent("the caller aborted the query before it left the client"));
229
+ return new Promise((resolve) => {
230
+ let done = false;
231
+ const aborted = () => {
232
+ done = true;
233
+ resolve(notSent("the caller abandoned the query before it left the client"));
234
+ };
235
+ signal?.addEventListener("abort", aborted, { once: true });
236
+ p.connect().then((client) => {
237
+ signal?.removeEventListener("abort", aborted);
238
+ if (done) client.release();
239
+ else resolve(client);
240
+ }, (error) => {
241
+ signal?.removeEventListener("abort", aborted);
242
+ checkoutFailed(error);
243
+ if (!done) resolve(notSent(message(error)));
244
+ });
245
+ });
246
+ };
247
+ /** This function makes one try of a query: a checkout, the query, and the release. */
248
+ const attempt = async (text, values, signal) => {
249
+ const got = await checkout(signal);
250
+ if (!("release" in got)) return got;
251
+ const client = got;
252
+ const onClientError = (error) => record("client.error", status, message(error));
253
+ client.on("error", onClientError);
254
+ return flight.track(signal, "the caller abandoned the query after it was sent", (settle) => {
255
+ client.query(text, values).then((result) => settle({
256
+ outcome: "ok",
257
+ value: {
258
+ rows: result.rows,
259
+ rowCount: result.rowCount
260
+ }
261
+ }), (error) => {
262
+ const reply = replyOf(error);
263
+ const said = reply === null ? message(error) : `${reply.code} ${message(error)}`;
264
+ if (reply !== null && reply.severity === "ERROR") settle({
265
+ outcome: "error",
266
+ error: said,
267
+ reply: true
268
+ });
269
+ else settle({
270
+ outcome: "indeterminate",
271
+ error: said
272
+ }, error instanceof Error ? error : new Error(said));
273
+ });
274
+ }, (_value, cause) => {
275
+ client.off("error", onClientError);
276
+ client.release(cause);
277
+ });
278
+ };
279
+ const query = (operation, text, values, sendOptions = {}) => sendWithReissue({
280
+ clock,
281
+ bound: requestTimeout,
282
+ operation: operations[operation],
283
+ options: sendOptions,
284
+ attempt: (signal) => attempt(text, values, signal),
285
+ onReissue: () => record("command.reissue", status, operation)
286
+ });
287
+ const handle = {
288
+ async connect(signal) {
289
+ record("status.connecting", "connecting");
290
+ const p = await create(poolOptions(name, spec));
291
+ pool = p;
292
+ p.on("connect", onConnect);
293
+ p.on("remove", onRemove);
294
+ p.on("error", onPoolError);
295
+ if (signal.aborted) throw new Error(`${name}: the service stopped before the connect`);
296
+ const first = await checkout(signal);
297
+ if ("release" in first) first.release();
298
+ },
299
+ async close() {
300
+ const p = pool;
301
+ if (p === null || closing()) return;
302
+ record("close.request", "closing");
303
+ if (!await withinBound(clock, ms(knobs.close_bound), p.end())) {
304
+ record("close.bound");
305
+ for (const client of [...open]) client.connection?.stream?.destroy?.();
306
+ record("close.destroy", status, `${open.size} sockets`);
307
+ }
308
+ flight.settleAll("the client closed with the query in flight");
309
+ p.off("connect", onConnect);
310
+ p.off("remove", onRemove);
311
+ record("status.end", "end");
312
+ }
313
+ };
314
+ const declaration = {
315
+ system: spec.system,
316
+ knobs: {
317
+ connect_bound: knobs.connect_bound,
318
+ request_timeout: knobs.request_timeout,
319
+ statement_timeout: knobs.statement_timeout,
320
+ pool_idle_expiry: knobs.pool_idle_expiry,
321
+ keepalive_idle: knobs.keepalive_idle,
322
+ idle_refresh_interval: knobs.keepalive_idle,
323
+ max_connection_age: knobs.max_connection_age,
324
+ close_bound: knobs.close_bound,
325
+ retrans_giveup_time: knobs.retrans_giveup_time
326
+ },
327
+ operations
328
+ };
329
+ const checks = (checkOptions) => stepChecks(name, checkOptions, {
330
+ "local-state": localStateCheck(() => status === "ready"),
331
+ checkout: {
332
+ ownConnection: true,
333
+ run: async (signal) => {
334
+ const got = await checkout(signal);
335
+ if (!("release" in got)) return "error";
336
+ got.release();
337
+ return "ok";
338
+ }
339
+ },
340
+ "round-trip": roundTripCheck((sendOptions) => query("check", "SELECT 1", [], sendOptions)),
341
+ dial: dialCheck(dial, spec.host, spec.port)
342
+ });
343
+ return {
344
+ name,
345
+ declaration,
346
+ handle,
347
+ meets: pgMeetings(name, knobs),
348
+ get status() {
349
+ return status;
350
+ },
351
+ joins,
352
+ query,
353
+ checks,
354
+ attach: (svc, checkOptions) => attachClient(svc, name, declaration, handle, checks(checkOptions)),
355
+ problems(description) {
356
+ const deadline = deadlineProblem(name, knobs.request_timeout, knobs.statement_timeout, "statement");
357
+ return [...clientProblems(name, description, clientBounds(knobs)), ...deadline === null ? [] : [deadline]];
358
+ }
359
+ };
360
+ };
361
+ //#endregion
362
+ //#region src/pg/machine.ts
363
+ /** The states where the pool exists and serves. */
364
+ const SERVING = [
365
+ "connecting",
366
+ "ready",
367
+ "failing"
368
+ ];
369
+ const OPEN = [...SERVING, "closing"];
370
+ /**
371
+ * The pg client of the service as a `mark1russell7.machine/1` document. It is the pool as the
372
+ * wrapper sees it, not one connection: the document `node-postgres` of `@mark1russell7/machines`
373
+ * gives each client of the pool. The state is the outcome of the last connect:
374
+ * - `ready`: the last connect of the pool succeeded.
375
+ * - `failing`: the last connect failed. pg does not connect again by itself. The next checkout tries again.
376
+ *
377
+ * An idle client that fails and a client that the pool removes do not change the state. The pool
378
+ * removed that client, and the next checkout opens a new connection. A test records the transitions
379
+ * of the client and replays them against this document.
380
+ */
381
+ const PG_CLIENT_MACHINE = {
382
+ format: "mark1russell7.machine/1",
383
+ id: "service.machine.pg-client",
384
+ title: "The PostgreSQL client of the service, over a pool of node-postgres 8",
385
+ layer: "driver",
386
+ version: "0.1.0",
387
+ sources: [{
388
+ ref: "packages/service/src/pg (the client implements this document)",
389
+ rung: "implemented"
390
+ }],
391
+ states: {
392
+ none: {
393
+ initial: true,
394
+ note: "the client exists, and its pool does not"
395
+ },
396
+ connecting: { note: "the first checkout of the startup opens the first connection" },
397
+ ready: { note: "the last connect succeeded. A dead idle client stays hidden until the pool ends it or a query meets it" },
398
+ failing: { note: "the last connect failed. A checkout of a full pool that times out does not change the state" },
399
+ closing: { note: "the end of the pool: each idle client sends Terminate, and each held client goes at its release" },
400
+ end: {
401
+ final: true,
402
+ note: "the service closed the client"
403
+ }
404
+ },
405
+ events: {
406
+ "status.connecting": { kind: "local" },
407
+ join: {
408
+ kind: "rx",
409
+ note: "the first ReadyForQuery of a new connection, with the process id of its backend (D1)"
410
+ },
411
+ "connect.failed": {
412
+ kind: "local",
413
+ note: "a connect failed: its error, a close, a FATAL of the server, or connect_bound"
414
+ },
415
+ "checkout.timeout": {
416
+ kind: "timer",
417
+ note: "a checkout of a full pool waited connect_bound"
418
+ },
419
+ "client.error": {
420
+ kind: "rx",
421
+ note: "an error of an idle client, or of a client that the service holds"
422
+ },
423
+ "client.removed": {
424
+ kind: "local",
425
+ note: "the socket of a client of the pool closed"
426
+ },
427
+ "command.reissue": { kind: "local" },
428
+ "close.request": { kind: "local" },
429
+ "close.bound": { kind: "timer" },
430
+ "close.destroy": {
431
+ kind: "local",
432
+ note: "the bound destroys each socket that a silent server does not close"
433
+ },
434
+ "status.end": { kind: "local" }
435
+ },
436
+ knobs: {
437
+ connect_bound: clientKnob("connectionTimeoutMillis"),
438
+ close_bound: clientKnob("close.bound")
439
+ },
440
+ timers: {
441
+ connect_timeout: {
442
+ duration: {
443
+ knob: "connect_bound",
444
+ unit: "s"
445
+ },
446
+ arms: ["enter:connecting"],
447
+ cancels: ["join", "connect.failed"],
448
+ fires: "connect.failed",
449
+ note: "pg-pool destroys the socket of the attempt: 'Connection terminated due to connection timeout'"
450
+ },
451
+ close: closeTimer()
452
+ },
453
+ transitions: [
454
+ transition("connect", "none", "status.connecting", "connecting"),
455
+ transition("join", [...SERVING], "join", "ready"),
456
+ transition("connect-failed", [...SERVING], "connect.failed", "failing", { timer: "connect_timeout" }),
457
+ transition("join-closing", "closing", "join", "closing"),
458
+ transition("connect-failed-closing", "closing", "connect.failed", "closing"),
459
+ ...OPEN.map((state) => transition(`checkout-timeout-${state}`, state, "checkout.timeout", state)),
460
+ ...OPEN.map((state) => transition(`client-error-${state}`, state, "client.error", state)),
461
+ ...OPEN.map((state) => transition(`removed-${state}`, state, "client.removed", state)),
462
+ ...OPEN.map((state) => transition(`reissue-${state}`, state, "command.reissue", state)),
463
+ transition("close", [...SERVING], "close.request", "closing"),
464
+ transition("close-bound", "closing", "close.bound", "closing", { timer: "close" }),
465
+ transition("close-destroy", "closing", "close.destroy", "closing"),
466
+ transition("end", "closing", "status.end", "end")
467
+ ]
468
+ };
469
+ //#endregion
470
+ //#region src/pg/starting.ts
471
+ /**
472
+ * This poll is the poll of the checks that `PG_STARTING_KNOBS` fits. It is `STARTING_POLL` of the
473
+ * core: each 5 s with a jitter of 20 %, and a timeout of 4 s (B6, O12). The poll does not depend on the driver.
474
+ */
475
+ const PG_STARTING_POLL = STARTING_POLL;
476
+ /**
477
+ * These knobs are a declaration to start from, from what the cells of S1 measured on node-postgres
478
+ * 8.23.1 against PostgreSQL 17.11. The runtime has no defaults: a service gives these knobs, or its
479
+ * own. Each value holds the borders that the cells measured:
480
+ * - `connect_bound` 5 s: at the defaults pg has no bound, and the kernel gives up on the SYNs only
481
+ * after 131 s. The timer of the pool fired 7 ms late at most. Thus 5 s is in a startup budget of 30 s (O15).
482
+ * - `request_timeout` 9.5 s: the cells ended a query in a black hole at 9.503 s. That is in the slowest poll of 10 s (O13).
483
+ * - `statement_timeout` 9 s: below `request_timeout`, thus the server ends a slow statement first,
484
+ * with ERROR 57014 (O07). The cells measured that end 2 ms after the deadline.
485
+ * - `pool_idle_expiry` 10 s: the idle timer of pg-pool at its defaults, measured at 10.004 s. It is
486
+ * below a router that forgets an idle flow at 12 s (O03), and below a `client_idle_timeout` of PgBouncer at 12 s (O01).
487
+ * - `keepalive_idle` 30 s: at the defaults pg has no keepalive. This value refreshes a held idle
488
+ * connection through the NAT forms of the cells (240 s and 350 s).
489
+ * - `max_connection_age` "infinite": no cell measured an age, thus the declaration gives none.
490
+ * - `close_bound` 2 s: behind a black hole the end of the pool waits with no bound. The cells saw
491
+ * the pool emit `remove` only when the socket closes.
492
+ * - `retrans_giveup_time` 924.6 s: the give-up of Linux at `tcp_retries2 = 15`. The cells measured 3.120 s at `tcp_retries2 = 3`.
493
+ */
494
+ const PG_STARTING_KNOBS = {
495
+ connect_bound: {
496
+ value: 5,
497
+ unit: "s"
498
+ },
499
+ request_timeout: {
500
+ value: 9.5,
501
+ unit: "s"
502
+ },
503
+ statement_timeout: {
504
+ value: 9,
505
+ unit: "s"
506
+ },
507
+ pool_idle_expiry: {
508
+ value: 10,
509
+ unit: "s"
510
+ },
511
+ keepalive_idle: {
512
+ value: 30,
513
+ unit: "s"
514
+ },
515
+ max_connection_age: {
516
+ value: "infinite",
517
+ unit: "s"
518
+ },
519
+ close_bound: {
520
+ value: 2,
521
+ unit: "s"
522
+ },
523
+ retrans_giveup_time: {
524
+ value: 924.6,
525
+ unit: "s"
526
+ }
527
+ };
528
+ //#endregion
529
+ export { MANIFESTATIONS, PG_CLIENT_MACHINE, PG_STARTING_KNOBS, PG_STARTING_POLL, STARTING_POLL, firstDetector, nodePostgres, pgClient, pgMeetings, poolOptions, specProblems, sslRequestDial, unwatched };
530
+
531
+ //# sourceMappingURL=pg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pg.js","names":["knob","t"],"sources":["../src/pg/connection.ts","../src/pg/meets.ts","../src/pg/client.ts","../src/pg/machine.ts","../src/pg/starting.ts"],"sourcesContent":["import type { ClientSocket, Factory } from \"../client/core.ts\";\n\n/** A pg socket is the socket of a client in the core: the join of the prober and the bounded close read it. */\nexport type PgSocket = ClientSocket;\n\n/** A query result is the part of a result of pg that the wrapper gives back. */\nexport interface PgResult<Row = Record<string, unknown>> {\n readonly rows: Row[];\n readonly rowCount: number | null;\n}\n\n/**\n * A pool client is the part of a `pg.PoolClient` that the wrapper uses. A pg 8 client has each part.\n * A fake for tests has them too. Thus the package depends on no type of pg.\n */\nexport interface PgPoolClient {\n /** This number is the id of the backend from BackendKeyData, which the server sends at the startup. */\n readonly processID?: number | null | undefined;\n /** This connection holds the socket of the client. */\n readonly connection?: { readonly stream?: PgSocket | undefined } | undefined;\n query(text: string, values?: readonly unknown[]): Promise<PgResult<never>>;\n /** This method gives the client back to the pool. With an error, the pool ends the client. */\n release(error?: Error | boolean): void;\n on(event: \"error\", listener: (error: Error) => void): unknown;\n off(event: \"error\", listener: (error: Error) => void): unknown;\n}\n\n/** A pool is the part of a `pg.Pool` that the wrapper uses. */\nexport interface PgPool {\n readonly totalCount: number;\n readonly idleCount: number;\n readonly waitingCount: number;\n connect(): Promise<PgPoolClient>;\n end(): Promise<void>;\n on(event: \"connect\" | \"remove\", listener: (client: PgPoolClient) => void): unknown;\n on(event: \"error\", listener: (error: Error, client: PgPoolClient) => void): unknown;\n off(event: \"connect\" | \"remove\", listener: (client: PgPoolClient) => void): unknown;\n off(event: \"error\", listener: (error: Error, client: PgPoolClient) => void): unknown;\n}\n\n/**\n * These are the options that the wrapper gives `pg.Pool`. Each one comes from the declaration. The\n * wrapper sets each option that has an effect on a timer, thus no default of pg decides one. A knob\n * of pg at 0 has no timer. The wrapper writes \"infinite\" as 0, and it refuses a bound of 0.\n */\nexport interface PgPoolOptions {\n readonly host: string;\n readonly port: number;\n readonly database?: string | undefined;\n readonly user?: string | undefined;\n readonly password?: string | undefined;\n /** The server shows this name in `pg_stat_activity`, thus the prober can join on it too. */\n readonly application_name: string;\n readonly max: number;\n readonly min: number;\n /** This time bounds the TCP connect, the startup and the authentication. It also bounds a checkout from a full pool. */\n readonly connectionTimeoutMillis: number;\n /** This time bounds each query on the client, from `client.query`. The client sends no cancel at its end. */\n readonly query_timeout: number;\n /** This time is the deadline of each statement on the server. The client sends it in the startup message. Undefined does not send it. */\n readonly statement_timeout: number | undefined;\n /** This time ends an idle client of the pool. 0 is no idle timer. */\n readonly idleTimeoutMillis: number;\n /** This time ends a client at this age from its connect, in whole seconds. 0 is no age. */\n readonly maxLifetimeSeconds: number;\n readonly keepAlive: boolean;\n /** This time is the idle time of TCP keepalive. Node truncates it to whole seconds. */\n readonly keepAliveInitialDelayMillis: number;\n /** The idle timer and the idle sockets do not keep the process alive (A14). The lifecycle holds the process. */\n readonly allowExitOnIdle: true;\n}\n\n/** A pool factory makes the pool of the client from its options. */\nexport type PoolFactory = Factory<PgPoolOptions, PgPool>;\n\n/**\n * This factory makes a `pg.Pool`. It loads pg only when a client connects, thus a service that does\n * not use PostgreSQL does not need pg. The specifier is a value, thus the package needs no types of pg.\n */\nexport const nodePostgres: PoolFactory = async (options) => {\n const specifier = \"pg\";\n const loaded = (await import(specifier)) as { readonly default: { readonly Pool: new (options: PgPoolOptions) => PgPool } };\n return new loaded.default.Pool(options);\n};\n","import type { Detector, Meeting } from \"../client/meets.ts\";\nimport { WHAT, immediate, keepaliveAnswered, keepaliveReset, keepaliveSilent, kernelGiveUp } from \"../client/meets.ts\";\nimport type { PgKnobs } from \"./client.ts\";\n\n/**\n * This function gives the manifestations that a pg client can meet, and the detectors of each, from\n * its knobs. The cells of S1 measured each one on node-postgres 8.23.1:\n * - A reset of a query gives ECONNRESET in 0.6 ms, and a FIN to an idle client gives EOF in 18 µs.\n * - A frozen server and a black hole have no detector in 60 s at the defaults: pg has no timer on a query and no keepalive.\n * - `query_timeout` ends a query in a black hole on time (9.503 s for 9.5 s).\n * - `statement_timeout` ends a slow statement on the server with ERROR 57014 (10.002 s for 10 s).\n *\n * pg has no watchdog and does not test an idle client. In place of a watchdog, the pool ends an\n * idle client at its idle timer. A later checkout then opens a new connection, which `connect_bound` bounds.\n */\nexport const pgMeetings = (name: string, knobs: PgKnobs): Meeting[] => {\n const query: Detector = {\n by: \"request_timeout\",\n within: knobs.request_timeout,\n situation: \"in flight\",\n effect: `the query fails as indeterminate, and ${name} gives the client back with an error, thus the pool ends it`,\n };\n const idleExpiry: Detector = {\n by: \"pool_idle_expiry\",\n within: knobs.pool_idle_expiry,\n situation: \"idle\",\n effect: \"the pool ends the idle client, thus no checkout gets the dead connection\",\n };\n const giveUp = kernelGiveUp(knobs.retrans_giveup_time, \"the query fails with its error\");\n const silent = (manifestation: \"blackhole\" | \"freeze-host\"): Meeting => ({\n manifestation,\n what: WHAT[manifestation],\n detectors: [query, giveUp, idleExpiry, keepaliveSilent(knobs.keepalive_idle)],\n });\n return [\n {\n manifestation: \"rst\",\n what: WHAT.rst,\n detectors: [\n immediate(\"in flight\", \"the query fails with ECONNRESET, and each new connect is refused\"),\n idleExpiry,\n keepaliveReset(knobs.keepalive_idle),\n ],\n },\n {\n manifestation: \"fin\",\n what: WHAT.fin,\n detectors: [\n immediate(\"in flight\", \"the query fails: 'Connection terminated unexpectedly'\"),\n immediate(\"idle\", \"the pool removes the idle client, and emits error\"),\n ],\n },\n silent(\"blackhole\"),\n silent(\"freeze-host\"),\n {\n manifestation: \"freeze-process\",\n what: WHAT[\"freeze-process\"],\n detectors: [query, idleExpiry, keepaliveAnswered()],\n },\n {\n manifestation: \"app-stall\",\n what: \"the server stops its replies for a time, for example behind a lock, and its kernel stays up\",\n detectors: [\n {\n by: \"statement_timeout\",\n within: knobs.statement_timeout,\n situation: \"in flight\",\n effect: \"the server ends the statement with ERROR 57014, a certain outcome, and the client stays\",\n },\n query,\n idleExpiry,\n keepaliveAnswered(),\n ],\n },\n ];\n};\n","import type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport { realClock } from \"../clock.ts\";\nimport type { Attempt, CheckOptions, Client, ClientJoin, ClientKnobs, ClientOptions, ClientSpec, ClientStep, DialFn, SendOptions, Sent } from \"../client/core.ts\";\nimport {\n attachClient,\n clientBounds,\n clientProblems,\n clientRecorder,\n deadlineProblem,\n dialCheck,\n finiteMs,\n inFlight,\n localStateCheck,\n message,\n ms,\n notSent,\n operationTable,\n orZero,\n poolProblems,\n roundTripCheck,\n sendWithReissue,\n socketEnds,\n stepChecks,\n timeProblems,\n wholeProblems,\n withinBound,\n zeroProblems,\n} from \"../client/core.ts\";\nimport { helloDial } from \"../client/dial.ts\";\nimport type { Dependency } from \"../description.ts\";\nimport type { DependencyHandle } from \"../lifecycle.ts\";\nimport type { Operation } from \"../operation.ts\";\nimport type { PgPool, PgPoolClient, PgPoolOptions, PgResult, PoolFactory } from \"./connection.ts\";\nimport { nodePostgres } from \"./connection.ts\";\nimport { pgMeetings } from \"./meets.ts\";\n\n/**\n * These are the knobs of the client, each a time, by the names of the ordering law where the law\n * has them. The runtime has no defaults, thus each knob is necessary. A bound that nobody gives is\n * \"infinite\", not zero. `PG_STARTING_KNOBS` gives a declaration from the cells of S1. The knobs\n * that each client has come from the core.\n */\nexport interface PgKnobs extends ClientKnobs {\n /**\n * This time bounds a new connection (`connectionTimeoutMillis`): the TCP connect, the startup and\n * the authentication. The pool uses the same knob to bound a checkout from a full pool.\n * At the defaults pg has no bound, and the kernel gives up on the SYNs only after 131 s (A3, O15).\n */\n readonly connect_bound: Quantity;\n /** This time bounds each query on the client (`query_timeout`). pg sends no cancel at its end. */\n readonly request_timeout: Quantity;\n /**\n * This time is the deadline of each statement on the server (`statement_timeout`). The server then\n * ends the statement with ERROR 57014, a certain outcome. \"infinite\" sends no deadline.\n */\n readonly statement_timeout: Extent;\n /** This time ends an idle client of the pool (`idleTimeoutMillis`). The pool sends no test on an idle client. */\n readonly pool_idle_expiry: Extent;\n /** This time is the idle time of TCP keepalive, in whole seconds. Node then sends 10 probes 1 s apart. */\n readonly keepalive_idle: Extent;\n /** This time is the age at which the pool replaces a client (`maxLifetimeSeconds`), in whole seconds. */\n readonly max_connection_age: Extent;\n /** This time bounds the close of the pool at the shutdown: its end, then a destroy of each socket that stays. */\n readonly close_bound: Quantity;\n}\n\n/** A pg client spec is the declaration of one PostgreSQL client: where it connects, its pool, and its knobs. */\nexport interface PgClientSpec extends ClientSpec<PgKnobs> {\n readonly database?: string | undefined;\n readonly user?: string | undefined;\n /** These counts are the sizes of the pool: at most `max` clients, and an idle timer only above `min`. */\n readonly pool: { readonly max: number; readonly min: number };\n /**\n * These operations are the queries of the service, by name, with their idempotency. SQL text does\n * not tell if a statement changes a state, thus the client sends again only a query that this table names.\n */\n readonly operations?: Readonly<Record<string, Operation>> | undefined;\n}\n\n/** These options make a client. Without them, it uses pg, the real clock, and a dial with an SSLRequest. */\nexport type PgClientOptions = ClientOptions<PoolFactory>;\n\n/**\n * A join is one connection of the pool, at its connect (D1, D2). The prober joins its kernel records\n * to the server's view of the connection through it. The server's view is the process id of the backend.\n */\nexport interface PgJoin extends ClientJoin {\n /** This number is the process id of the backend, or null when the server gave none. Through a pooler, it is the id that the pooler gives. */\n readonly backendPid: number | null;\n}\n\n/** The steps that the pg client gives a check for: the steps of each client, and a checkout from the pool (B14). */\nexport type PgStep = ClientStep | \"checkout\";\n\n/** A pg client is a dependency of a service: its declaration, its handle, its checks and its queries. */\nexport interface PgClient extends Client<PgStep, PgJoin> {\n /** This text is the state of the client, as `PG_CLIENT_MACHINE` names it. */\n readonly status: string;\n /**\n * This method sends a query, and gives its outcome in three values. `operation` names the query\n * in the table of operations. The client sends again only an idempotent operation, one time at\n * most, after an outcome that is not certain. The second try stays in the bound of the request.\n */\n query<Row = Record<string, unknown>>(\n operation: string,\n text: string,\n values?: readonly unknown[],\n options?: SendOptions,\n ): Promise<Sent<PgResult<Row>>>;\n}\n\n/** This function gives the problems of a spec: the knobs that pg cannot use. */\nexport const specProblems = (name: string, spec: PgClientSpec): string[] => {\n const { knobs } = spec;\n return [\n ...timeProblems(name, knobs),\n // pg reads 0 as no timer, thus a bound of 0 is not a bound\n ...zeroProblems(name, \"pg\", knobs, {\n bounded: [\"connect_bound\", \"request_timeout\", \"close_bound\"],\n open: [\"statement_timeout\", \"pool_idle_expiry\", \"keepalive_idle\", \"max_connection_age\"],\n }),\n // Node truncates the keepalive delay to whole seconds, and pg takes the age in whole seconds\n ...wholeProblems(name, knobs, {\n millis: [\"connect_bound\", \"request_timeout\", \"statement_timeout\", \"pool_idle_expiry\"],\n seconds: [\"keepalive_idle\", \"max_connection_age\"],\n }),\n ...poolProblems(name, spec.pool),\n ];\n};\n\n/** This function gives the options of `pg.Pool` from a spec. Each timer of pg comes from the declaration. */\nexport const poolOptions = (name: string, spec: PgClientSpec): PgPoolOptions => {\n const { knobs } = spec;\n return {\n host: spec.host,\n port: spec.port,\n ...(spec.database === undefined ? {} : { database: spec.database }),\n ...(spec.user === undefined ? {} : { user: spec.user }),\n ...(spec.password === undefined ? {} : { password: spec.password }),\n application_name: name,\n max: spec.pool.max,\n min: spec.pool.min,\n connectionTimeoutMillis: ms(knobs.connect_bound),\n query_timeout: ms(knobs.request_timeout),\n statement_timeout: finiteMs(knobs.statement_timeout),\n idleTimeoutMillis: orZero(knobs.pool_idle_expiry),\n maxLifetimeSeconds: orZero(knobs.max_connection_age) / 1000,\n keepAlive: knobs.keepalive_idle.value !== \"infinite\",\n keepAliveInitialDelayMillis: orZero(knobs.keepalive_idle),\n allowExitOnIdle: true,\n };\n};\n\n/** The bytes of an SSLRequest: a length of 8, then the code 80877103. A server answers with one byte, S or N. */\nconst SSL_REQUEST = Uint8Array.of(0, 0, 0, 8, 0x04, 0xd2, 0x16, 0x2f);\n\n/**\n * This dial function opens a TCP connection, sends an SSLRequest, and waits for its one byte. The\n * server answers it before the authentication, thus the dial needs no password and opens no backend.\n */\nexport const sslRequestDial: DialFn = helloDial(SSL_REQUEST, (received) => {\n const answer = String.fromCharCode(received[0] ?? 0);\n return answer === \"S\" || answer === \"N\" ? null : new Error(`the answer to the SSLRequest was ${JSON.stringify(answer)}`);\n});\n\n/** A database error is an ErrorResponse of the server: pg gives its severity and its SQLSTATE code. */\nconst replyOf = (error: unknown): { readonly severity: string; readonly code: string } | null => {\n if (typeof error !== \"object\" || error === null) return null;\n const { severity, code } = error as { severity?: unknown; code?: unknown };\n return typeof severity === \"string\" && typeof code === \"string\" && /^[0-9A-Z]{5}$/.test(code) ? { severity, code } : null;\n};\n\n/**\n * This function makes a PostgreSQL client, a dependency of a service over a `pg.Pool`.\n * - It sets each timer of pg from the declaration. The driver sends nothing again by itself, and the client\n * sends again only an idempotent operation of its table.\n * - It tells the outcome of a query in three values. An ERROR of the server is certain. A FATAL\n * ends the session, thus its outcome is not certain.\n * - It gives back a client whose query has no certain outcome with an error. The pool then ends it,\n * thus an orphaned query does not hold the next checkout.\n * - It listens to each error of the pool and of a client that it holds. In Node, an error with no\n * listener throws, and library code does not end the process (A5).\n * - It records the process id of the backend of each connection (D1).\n *\n * The outcomes, the second try, the queries in flight, the bounded close and the checks come from\n * the core of the clients. This entry gives what pg does in its own way.\n */\nexport const pgClient = (name: string, spec: PgClientSpec, options: PgClientOptions = {}): PgClient => {\n const problems = specProblems(name, spec);\n if (problems.length > 0) throw new Error(problems.join(\"; \"));\n const { knobs } = spec;\n const clock = options.clock ?? realClock;\n const create = options.create ?? nodePostgres;\n const dial = options.dial ?? sslRequestDial;\n const operations = operationTable({}, spec.operations);\n const requestTimeout = ms(knobs.request_timeout);\n\n let pool: PgPool | null = null;\n let status = \"none\";\n const joins: PgJoin[] = [];\n // the clients of the pool whose socket did not close yet: the bound of the close destroys them\n const open = new Set<PgPoolClient>();\n // the queries in flight: the end of the close settles each one\n const flight = inFlight();\n\n const emit = clientRecorder(options.record, clock, name);\n // the state of the pg client is the outcome of its last connect: each record can move it\n const record = (on: string, to: string = status, detail?: string): void => {\n const from = status;\n status = to;\n emit(from, to, on, detail);\n };\n const closing = (): boolean => status === \"closing\" || status === \"end\";\n\n const onConnect = (client: PgPoolClient): void => {\n open.add(client);\n const generation = joins.length + 1;\n const pid = client.processID;\n joins.push({ generation, at: clock.now(), backendPid: typeof pid === \"number\" ? pid : null, ...socketEnds(client.connection?.stream) });\n record(\"join\", closing() ? status : \"ready\", `generation ${generation}, backend ${pid ?? \"unknown\"}`);\n };\n const onRemove = (client: PgPoolClient): void => {\n open.delete(client);\n record(\"client.removed\");\n };\n // an idle client failed. The pool removed it, and the next checkout opens a new connection\n // after the end, the listener only keeps the error from a throw\n const onPoolError = (error: Error): void => {\n if (status !== \"end\") record(\"client.error\", status, message(error));\n };\n\n /** This function records a failed checkout: a full pool at its bound, or a connect that failed. */\n const checkoutFailed = (error: unknown): void => {\n const text = message(error);\n if (/timeout exceeded when trying to connect/i.test(text)) record(\"checkout.timeout\", status, text);\n else record(\"connect.failed\", closing() ? status : \"failing\", text);\n };\n\n /** This function checks out a client, or gives the reason why the query cannot leave the client. */\n const checkout = (signal: AbortSignal | undefined): Promise<PgPoolClient | Attempt> => {\n const p = pool;\n if (p === null) return Promise.resolve(notSent(`${name} has not connected`));\n if (closing()) return Promise.resolve(notSent(`${name} is ${status}: the query did not leave the client`));\n if (signal?.aborted) return Promise.resolve(notSent(\"the caller aborted the query before it left the client\"));\n return new Promise((resolve) => {\n let done = false;\n const aborted = (): void => {\n done = true;\n resolve(notSent(\"the caller abandoned the query before it left the client\"));\n };\n signal?.addEventListener(\"abort\", aborted, { once: true });\n p.connect().then(\n (client) => {\n signal?.removeEventListener(\"abort\", aborted);\n // the caller left while the pool connected: the client goes back unused\n if (done) client.release();\n else resolve(client);\n },\n (error: unknown) => {\n signal?.removeEventListener(\"abort\", aborted);\n checkoutFailed(error);\n if (!done) resolve(notSent(message(error)));\n },\n );\n });\n };\n\n /** This function makes one try of a query: a checkout, the query, and the release. */\n const attempt = async (text: string, values: readonly unknown[] | undefined, signal: AbortSignal | undefined): Promise<Attempt> => {\n const got = await checkout(signal);\n if (!(\"release\" in got)) return got;\n const client = got;\n // a client that the application holds emits its own errors, and the pool does not listen to them\n const onClientError = (error: Error): void => record(\"client.error\", status, message(error));\n client.on(\"error\", onClientError);\n return flight.track(\n signal,\n \"the caller abandoned the query after it was sent\",\n (settle) => {\n client.query(text, values).then(\n (result) => settle({ outcome: \"ok\", value: { rows: result.rows, rowCount: result.rowCount } }),\n (error: unknown) => {\n const reply = replyOf(error);\n const said = reply === null ? message(error) : `${reply.code} ${message(error)}`;\n // an ERROR ends the statement and keeps the session. A FATAL or a PANIC ends the session, and the commit is not certain\n if (reply !== null && reply.severity === \"ERROR\") settle({ outcome: \"error\", error: said, reply: true });\n else settle({ outcome: \"indeterminate\", error: said }, error instanceof Error ? error : new Error(said));\n },\n );\n },\n (_value, cause) => {\n client.off(\"error\", onClientError);\n // pg keeps a query active on the server after its timeout: an error makes the pool end the client, and the end destroys the socket\n client.release(cause);\n },\n );\n };\n\n const query = <Row>(operation: string, text: string, values?: readonly unknown[], sendOptions: SendOptions = {}): Promise<Sent<PgResult<Row>>> =>\n sendWithReissue({\n clock,\n bound: requestTimeout,\n operation: operations[operation],\n options: sendOptions,\n attempt: (signal) => attempt(text, values, signal) as Promise<Attempt<PgResult<Row>>>,\n onReissue: () => record(\"command.reissue\", status, operation),\n });\n\n const handle: DependencyHandle = {\n async connect(signal) {\n record(\"status.connecting\", \"connecting\");\n const p = await create(poolOptions(name, spec));\n pool = p;\n p.on(\"connect\", onConnect);\n p.on(\"remove\", onRemove);\n p.on(\"error\", onPoolError);\n if (signal.aborted) throw new Error(`${name}: the service stopped before the connect`);\n // one checkout opens the first connection. A server that is down does not stop the startup:\n // the checks keep readiness tripped until a connect succeeds (readiness is earned)\n const first = await checkout(signal);\n if (\"release\" in first) first.release();\n },\n async close() {\n const p = pool;\n if (p === null || closing()) return;\n record(\"close.request\", \"closing\");\n if (!(await withinBound(clock, ms(knobs.close_bound), p.end()))) {\n // the end of a client sends Terminate and half-closes the socket. Behind a black hole the\n // socket never closes, and the pool waits with no bound: the bound destroys each socket\n record(\"close.bound\");\n for (const client of [...open]) client.connection?.stream?.destroy?.();\n record(\"close.destroy\", status, `${open.size} sockets`);\n }\n flight.settleAll(\"the client closed with the query in flight\");\n p.off(\"connect\", onConnect);\n p.off(\"remove\", onRemove);\n // the error listener stays: a socket that closes later emits an error, and with no listener it throws\n record(\"status.end\", \"end\");\n },\n };\n\n const declaration: Omit<Dependency, \"name\"> = {\n system: spec.system,\n knobs: {\n connect_bound: knobs.connect_bound,\n request_timeout: knobs.request_timeout,\n statement_timeout: knobs.statement_timeout,\n pool_idle_expiry: knobs.pool_idle_expiry,\n keepalive_idle: knobs.keepalive_idle,\n // pg sends nothing on an idle connection, thus its keepalive is the only refresh of an idle flow (O03)\n idle_refresh_interval: knobs.keepalive_idle,\n max_connection_age: knobs.max_connection_age,\n close_bound: knobs.close_bound,\n retrans_giveup_time: knobs.retrans_giveup_time,\n },\n operations,\n };\n\n const checks = (checkOptions: CheckOptions<PgStep>) =>\n stepChecks<PgStep>(name, checkOptions, {\n // the outcome of the last connect, without a round trip. pg does not connect again by itself,\n // thus pair it with a check that uses the pool\n \"local-state\": localStateCheck(() => status === \"ready\"),\n // a checkout and its release, with no query: it finds a full pool and a connect that fails,\n // but the pool sends no test on an idle client, thus not a dead idle connection\n checkout: {\n ownConnection: true,\n run: async (signal) => {\n const got = await checkout(signal);\n if (!(\"release\" in got)) return \"error\";\n got.release();\n return \"ok\";\n },\n },\n // a SELECT 1 through the pool of the application\n \"round-trip\": roundTripCheck((sendOptions) => query(\"check\", \"SELECT 1\", [], sendOptions)),\n // a new connection each time, with no backend\n dial: dialCheck(dial, spec.host, spec.port),\n });\n\n return {\n name,\n declaration,\n handle,\n meets: pgMeetings(name, knobs),\n get status() {\n return status;\n },\n joins,\n query,\n checks,\n attach: (svc, checkOptions) => attachClient(svc, name, declaration, handle, checks(checkOptions)),\n problems(description) {\n // O07: the server must end a slow statement first, with a certain error\n const deadline = deadlineProblem(name, knobs.request_timeout, knobs.statement_timeout, \"statement\");\n return [...clientProblems(name, description, clientBounds(knobs)), ...(deadline === null ? [] : [deadline])];\n },\n };\n};\n","import type { Machine } from \"@mark1russell7/algebra\";\nimport { clientKnob as knob, closeTimer, transition as t } from \"../client/machine.ts\";\n\n/** The states where the pool exists and serves. */\nconst SERVING = [\"connecting\", \"ready\", \"failing\"] as const;\nconst OPEN = [...SERVING, \"closing\"] as const;\n\n/**\n * The pg client of the service as a `mark1russell7.machine/1` document. It is the pool as the\n * wrapper sees it, not one connection: the document `node-postgres` of `@mark1russell7/machines`\n * gives each client of the pool. The state is the outcome of the last connect:\n * - `ready`: the last connect of the pool succeeded.\n * - `failing`: the last connect failed. pg does not connect again by itself. The next checkout tries again.\n *\n * An idle client that fails and a client that the pool removes do not change the state. The pool\n * removed that client, and the next checkout opens a new connection. A test records the transitions\n * of the client and replays them against this document.\n */\nexport const PG_CLIENT_MACHINE: Machine = {\n format: \"mark1russell7.machine/1\",\n id: \"service.machine.pg-client\",\n title: \"The PostgreSQL client of the service, over a pool of node-postgres 8\",\n layer: \"driver\",\n version: \"0.1.0\",\n sources: [{ ref: \"packages/service/src/pg (the client implements this document)\", rung: \"implemented\" }],\n states: {\n none: { initial: true, note: \"the client exists, and its pool does not\" },\n connecting: { note: \"the first checkout of the startup opens the first connection\" },\n ready: { note: \"the last connect succeeded. A dead idle client stays hidden until the pool ends it or a query meets it\" },\n failing: { note: \"the last connect failed. A checkout of a full pool that times out does not change the state\" },\n closing: { note: \"the end of the pool: each idle client sends Terminate, and each held client goes at its release\" },\n end: { final: true, note: \"the service closed the client\" },\n },\n events: {\n \"status.connecting\": { kind: \"local\" },\n join: { kind: \"rx\", note: \"the first ReadyForQuery of a new connection, with the process id of its backend (D1)\" },\n \"connect.failed\": { kind: \"local\", note: \"a connect failed: its error, a close, a FATAL of the server, or connect_bound\" },\n \"checkout.timeout\": { kind: \"timer\", note: \"a checkout of a full pool waited connect_bound\" },\n \"client.error\": { kind: \"rx\", note: \"an error of an idle client, or of a client that the service holds\" },\n \"client.removed\": { kind: \"local\", note: \"the socket of a client of the pool closed\" },\n \"command.reissue\": { kind: \"local\" },\n \"close.request\": { kind: \"local\" },\n \"close.bound\": { kind: \"timer\" },\n \"close.destroy\": { kind: \"local\", note: \"the bound destroys each socket that a silent server does not close\" },\n \"status.end\": { kind: \"local\" },\n },\n knobs: {\n connect_bound: knob(\"connectionTimeoutMillis\"),\n close_bound: knob(\"close.bound\"),\n },\n timers: {\n connect_timeout: {\n duration: { knob: \"connect_bound\", unit: \"s\" },\n arms: [\"enter:connecting\"],\n cancels: [\"join\", \"connect.failed\"],\n fires: \"connect.failed\",\n note: \"pg-pool destroys the socket of the attempt: 'Connection terminated due to connection timeout'\",\n },\n close: closeTimer(),\n },\n transitions: [\n t(\"connect\", \"none\", \"status.connecting\", \"connecting\"),\n t(\"join\", [...SERVING], \"join\", \"ready\"),\n t(\"connect-failed\", [...SERVING], \"connect.failed\", \"failing\", { timer: \"connect_timeout\" }),\n // a checkout that is in flight at the close can still connect, or fail\n t(\"join-closing\", \"closing\", \"join\", \"closing\"),\n t(\"connect-failed-closing\", \"closing\", \"connect.failed\", \"closing\"),\n ...OPEN.map((state) => t(`checkout-timeout-${state}`, state, \"checkout.timeout\", state)),\n ...OPEN.map((state) => t(`client-error-${state}`, state, \"client.error\", state)),\n ...OPEN.map((state) => t(`removed-${state}`, state, \"client.removed\", state)),\n ...OPEN.map((state) => t(`reissue-${state}`, state, \"command.reissue\", state)),\n t(\"close\", [...SERVING], \"close.request\", \"closing\"),\n t(\"close-bound\", \"closing\", \"close.bound\", \"closing\", { timer: \"close\" }),\n t(\"close-destroy\", \"closing\", \"close.destroy\", \"closing\"),\n t(\"end\", \"closing\", \"status.end\", \"end\"),\n ],\n};\n","import { STARTING_POLL } from \"../client/core.ts\";\nimport type { Poll } from \"../timing.ts\";\nimport type { PgKnobs } from \"./client.ts\";\n\n/**\n * This poll is the poll of the checks that `PG_STARTING_KNOBS` fits. It is `STARTING_POLL` of the\n * core: each 5 s with a jitter of 20 %, and a timeout of 4 s (B6, O12). The poll does not depend on the driver.\n */\nexport const PG_STARTING_POLL: Poll = STARTING_POLL;\n\n/**\n * These knobs are a declaration to start from, from what the cells of S1 measured on node-postgres\n * 8.23.1 against PostgreSQL 17.11. The runtime has no defaults: a service gives these knobs, or its\n * own. Each value holds the borders that the cells measured:\n * - `connect_bound` 5 s: at the defaults pg has no bound, and the kernel gives up on the SYNs only\n * after 131 s. The timer of the pool fired 7 ms late at most. Thus 5 s is in a startup budget of 30 s (O15).\n * - `request_timeout` 9.5 s: the cells ended a query in a black hole at 9.503 s. That is in the slowest poll of 10 s (O13).\n * - `statement_timeout` 9 s: below `request_timeout`, thus the server ends a slow statement first,\n * with ERROR 57014 (O07). The cells measured that end 2 ms after the deadline.\n * - `pool_idle_expiry` 10 s: the idle timer of pg-pool at its defaults, measured at 10.004 s. It is\n * below a router that forgets an idle flow at 12 s (O03), and below a `client_idle_timeout` of PgBouncer at 12 s (O01).\n * - `keepalive_idle` 30 s: at the defaults pg has no keepalive. This value refreshes a held idle\n * connection through the NAT forms of the cells (240 s and 350 s).\n * - `max_connection_age` \"infinite\": no cell measured an age, thus the declaration gives none.\n * - `close_bound` 2 s: behind a black hole the end of the pool waits with no bound. The cells saw\n * the pool emit `remove` only when the socket closes.\n * - `retrans_giveup_time` 924.6 s: the give-up of Linux at `tcp_retries2 = 15`. The cells measured 3.120 s at `tcp_retries2 = 3`.\n */\nexport const PG_STARTING_KNOBS: PgKnobs = {\n connect_bound: { value: 5, unit: \"s\" },\n request_timeout: { value: 9.5, unit: \"s\" },\n statement_timeout: { value: 9, unit: \"s\" },\n pool_idle_expiry: { value: 10, unit: \"s\" },\n keepalive_idle: { value: 30, unit: \"s\" },\n max_connection_age: { value: \"infinite\", unit: \"s\" },\n close_bound: { value: 2, unit: \"s\" },\n retrans_giveup_time: { value: 924.6, unit: \"s\" },\n};\n"],"mappings":"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"}