@effectstream/db 0.100.18 → 0.100.19
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/package.json +6 -6
- package/scripts/start-pglite.ts +9 -2
- package/src/mod.ts +1 -0
- package/src/pg-connection.ts +26 -27
- package/src/pool-error-tracker.test.ts +65 -0
- package/src/pool-error-tracker.ts +87 -0
- package/src/transient-pg-errors.test.ts +47 -0
- package/src/transient-pg-errors.ts +56 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effectstream/db",
|
|
3
|
-
"version": "0.100.
|
|
3
|
+
"version": "0.100.19",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://effectstream.github.io/docs/",
|
|
6
6
|
"repository": {
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"./version": "./migrations/system-version.ts"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@effectstream/coroutine": "0.100.
|
|
25
|
-
"@effectstream/log": "0.100.
|
|
26
|
-
"@effectstream/runtime": "0.100.
|
|
27
|
-
"@effectstream/sync": "0.100.
|
|
28
|
-
"@effectstream/utils": "0.100.
|
|
24
|
+
"@effectstream/coroutine": "0.100.19",
|
|
25
|
+
"@effectstream/log": "0.100.19",
|
|
26
|
+
"@effectstream/runtime": "0.100.19",
|
|
27
|
+
"@effectstream/sync": "0.100.19",
|
|
28
|
+
"@effectstream/utils": "0.100.19",
|
|
29
29
|
"@electric-sql/pglite": "^0.3.14",
|
|
30
30
|
"@pgtyped/runtime": "2.4.2",
|
|
31
31
|
"@pgtyped/parser": "2.4.2",
|
package/scripts/start-pglite.ts
CHANGED
|
@@ -31,6 +31,10 @@ export async function startPglite(port = 5432): Promise<PgliteHandle> {
|
|
|
31
31
|
},
|
|
32
32
|
);
|
|
33
33
|
|
|
34
|
+
// Serialize every client's execProtocolRaw through one promise chain
|
|
35
|
+
// PGlite allows only one in-flight query, and per-process db_mutex can't coordinate across separate TCP clients.
|
|
36
|
+
let queue: Promise<unknown> = Promise.resolve();
|
|
37
|
+
|
|
34
38
|
// TODO: consider switching to pglite-socket once it works
|
|
35
39
|
// https://discord.com/channels/933657521581858818/1371976702674075780/1371992712076595250
|
|
36
40
|
const server = net.createServer(async (socket) => {
|
|
@@ -54,8 +58,11 @@ export async function startPglite(port = 5432): Promise<PgliteHandle> {
|
|
|
54
58
|
return;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
// Forward raw message to PGlite and send response to client
|
|
58
|
-
|
|
61
|
+
// Forward raw message to PGlite and send response to client, serialized
|
|
62
|
+
// through the queue so the single WASM backend handles one at a time.
|
|
63
|
+
const run = queue.then(() => db.execProtocolRaw(data));
|
|
64
|
+
queue = run.catch(() => {}); // keep the chain alive even if a query errors
|
|
65
|
+
return await run;
|
|
59
66
|
},
|
|
60
67
|
});
|
|
61
68
|
});
|
package/src/mod.ts
CHANGED
|
@@ -54,6 +54,7 @@ export * from "./sql/tables.queries.ts";
|
|
|
54
54
|
|
|
55
55
|
export * from "./event-indexing.ts";
|
|
56
56
|
export * from "./register-events.ts";
|
|
57
|
+
export * from "./transient-pg-errors.ts";
|
|
57
58
|
export * from "./pg-connection.ts";
|
|
58
59
|
export * from "./scheduled-constructors.ts";
|
|
59
60
|
export * from "./system.ts";
|
package/src/pg-connection.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { Client, PoolClient, PoolConfig } from "pg";
|
|
2
2
|
import pg from "pg";
|
|
3
|
-
import { ComponentNames,
|
|
3
|
+
import { ComponentNames, installUnhandledRejectionLogger } from "@effectstream/log";
|
|
4
4
|
import { type Operation, run, sleep } from "effection";
|
|
5
5
|
import { ENV } from "@effectstream/utils/node-env";
|
|
6
|
+
import { isTransientPgError } from "./transient-pg-errors.ts";
|
|
7
|
+
import { poolErrors } from "./pool-error-tracker.ts";
|
|
8
|
+
|
|
9
|
+
export { isTransientPgError } from "./transient-pg-errors.ts";
|
|
10
|
+
export { poolErrors, PoolErrorTracker } from "./pool-error-tracker.ts";
|
|
6
11
|
|
|
7
12
|
let readonlyDBConn: pg.Pool | null;
|
|
13
|
+
let unhandledRejectionLoggerInstalled = false;
|
|
8
14
|
|
|
9
15
|
// PGLite does not support multiple connections, so we need to use a mutex to ensure that only one query is executed at a time.
|
|
10
16
|
// * For transactions use yield* acquireDBMutex(); ...Your Operations... releaseDBMutex();
|
|
@@ -138,6 +144,8 @@ export async function runPreparedQuery<T>(
|
|
|
138
144
|
});
|
|
139
145
|
result = await Promise.race([p, timeout]);
|
|
140
146
|
clearTimeout(t);
|
|
147
|
+
// A successful query proves the DB is reachable end-to-end
|
|
148
|
+
poolErrors.markHealthy();
|
|
141
149
|
} finally {
|
|
142
150
|
releaseDBMutex(`pq:${name}`);
|
|
143
151
|
}
|
|
@@ -148,6 +156,14 @@ export const getConnection = (
|
|
|
148
156
|
creds?: PoolConfig,
|
|
149
157
|
readonly = false,
|
|
150
158
|
): pg.Pool => {
|
|
159
|
+
if (!unhandledRejectionLoggerInstalled) {
|
|
160
|
+
unhandledRejectionLoggerInstalled = true;
|
|
161
|
+
installUnhandledRejectionLogger(
|
|
162
|
+
ComponentNames.EFFECTSTREAM_PGLITE,
|
|
163
|
+
isTransientPgError,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
151
167
|
if (!creds) {
|
|
152
168
|
creds = {
|
|
153
169
|
host: ENV.DB_HOST,
|
|
@@ -163,30 +179,21 @@ export const getConnection = (
|
|
|
163
179
|
const MAX_CONNECTIONS = ENV.PGLITE ? 1 : 10;
|
|
164
180
|
|
|
165
181
|
const pool = new pg.Pool({ ...creds, max: MAX_CONNECTIONS });
|
|
166
|
-
pool.on("error", (err: unknown) =>
|
|
167
|
-
log.remote(
|
|
168
|
-
ComponentNames.EFFECTSTREAM_PGLITE,
|
|
169
|
-
["query"],
|
|
170
|
-
SeverityNumber.ERROR,
|
|
171
|
-
(log) => log(err),
|
|
172
|
-
));
|
|
182
|
+
pool.on("error", (err: unknown) => poolErrors.log(err, ["query"]));
|
|
173
183
|
pool.on("connect", (_client: PoolClient) => {
|
|
174
|
-
//
|
|
184
|
+
// Successful connect proves the DB is reachable — reset the failure clock.
|
|
185
|
+
poolErrors.markHealthy();
|
|
186
|
+
// Register a per-client error handler; pg can emit errors on idle clients.
|
|
175
187
|
// https://github.com/brianc/node-postgres/issues/2499#issuecomment-805477725
|
|
176
|
-
_client.on("error", (err: Error) =>
|
|
177
|
-
log.remote(
|
|
178
|
-
ComponentNames.EFFECTSTREAM_PGLITE,
|
|
179
|
-
["connect"],
|
|
180
|
-
SeverityNumber.ERROR,
|
|
181
|
-
(log) => log(err),
|
|
182
|
-
);
|
|
183
|
-
});
|
|
188
|
+
_client.on("error", (err: Error) => poolErrors.log(err, ["connect"]));
|
|
184
189
|
});
|
|
185
190
|
|
|
186
191
|
if (readonly) {
|
|
187
192
|
const ensureReadOnly =
|
|
188
193
|
`SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;`;
|
|
189
|
-
void pool.query(ensureReadOnly)
|
|
194
|
+
void pool.query(ensureReadOnly).catch((err: unknown) =>
|
|
195
|
+
poolErrors.log(err, ["readonly-setup"]),
|
|
196
|
+
);
|
|
190
197
|
readonlyDBConn = pool;
|
|
191
198
|
}
|
|
192
199
|
|
|
@@ -198,14 +205,6 @@ export const getPersistentConnection = (creds: PoolConfig): Client => {
|
|
|
198
205
|
const client = new pg.Client(creds);
|
|
199
206
|
client.connect(() => {});
|
|
200
207
|
// https://github.com/brianc/node-postgres/issues/2499#issuecomment-805477725
|
|
201
|
-
|
|
202
|
-
client.on("error", (err: Error) => {
|
|
203
|
-
log.remote(
|
|
204
|
-
ComponentNames.EFFECTSTREAM_PGLITE,
|
|
205
|
-
["query"],
|
|
206
|
-
SeverityNumber.ERROR,
|
|
207
|
-
(log) => log(err),
|
|
208
|
-
);
|
|
209
|
-
});
|
|
208
|
+
client.on("error", (err: Error) => poolErrors.log(err, ["persistent"]));
|
|
210
209
|
return client;
|
|
211
210
|
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// Thresholds are read from env at module load (and `envMs` rejects 0, so use
|
|
4
|
+
// small positive values). SUSTAINED=5ms → a failure older than 5ms is
|
|
5
|
+
// "sustained"; FATAL=30ms → older than 30ms triggers the process.exit
|
|
6
|
+
// escalation. Dynamic import so these are picked up before the module
|
|
7
|
+
// computes its constants.
|
|
8
|
+
process.env.POOL_SUSTAINED_THRESHOLD_MS = "5";
|
|
9
|
+
process.env.POOL_FATAL_THRESHOLD_MS = "30";
|
|
10
|
+
const { PoolErrorTracker } = await import("./pool-error-tracker.ts");
|
|
11
|
+
|
|
12
|
+
const transient = () => ({
|
|
13
|
+
code: "ECONNRESET",
|
|
14
|
+
message: "Client network socket disconnected before secure TLS connection",
|
|
15
|
+
stack: " at node_modules/pg-pool/index.js:45:11",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const realExit = process.exit;
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
process.exit = realExit;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("ignores non-transient errors (auth/schema bugs)", () => {
|
|
24
|
+
const t = new PoolErrorTracker();
|
|
25
|
+
t.log(new Error("column foo does not exist"), ["sync-write"]);
|
|
26
|
+
expect(t.state().consecutive).toBe(0);
|
|
27
|
+
expect(t.state().sustained).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("counts consecutive transient failures and reports sustained", async () => {
|
|
31
|
+
const t = new PoolErrorTracker();
|
|
32
|
+
t.log(transient(), ["sync-write"]);
|
|
33
|
+
expect(t.state().sustained).toBe(false); // fresh failure, < 5ms old
|
|
34
|
+
await Bun.sleep(15); // exceed SUSTAINED_THRESHOLD_MS=5, stay under FATAL=30
|
|
35
|
+
t.log(transient(), ["sync-write"]);
|
|
36
|
+
const s = t.state();
|
|
37
|
+
expect(s.consecutive).toBe(2);
|
|
38
|
+
expect(s.sustained).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("markHealthy resets the failure clock", () => {
|
|
42
|
+
const t = new PoolErrorTracker();
|
|
43
|
+
t.log(transient(), ["sync-write"]);
|
|
44
|
+
t.markHealthy();
|
|
45
|
+
const s = t.state();
|
|
46
|
+
expect(s.consecutive).toBe(0);
|
|
47
|
+
expect(s.firstFailureAt).toBe(0);
|
|
48
|
+
expect(s.sustained).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("exits the process once failures persist past the fatal threshold", async () => {
|
|
52
|
+
let exitCode: number | undefined;
|
|
53
|
+
process.exit = ((code?: number) => {
|
|
54
|
+
exitCode = code;
|
|
55
|
+
}) as typeof process.exit;
|
|
56
|
+
|
|
57
|
+
const t = new PoolErrorTracker();
|
|
58
|
+
t.log(transient(), ["sync-write"]); // starts the clock; not yet fatal
|
|
59
|
+
await Bun.sleep(40); // exceed FATAL_THRESHOLD_MS=20
|
|
60
|
+
t.log(transient(), ["sync-write"]); // now duration >= fatal → schedules exit
|
|
61
|
+
|
|
62
|
+
// exit is deferred via setImmediate so the fatal log line can flush first.
|
|
63
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
64
|
+
expect(exitCode).toBe(1);
|
|
65
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ComponentNames, log, SeverityNumber } from "@effectstream/log";
|
|
2
|
+
import { isTransientPgError } from "./transient-pg-errors.ts";
|
|
3
|
+
|
|
4
|
+
function envMs(name: string, fallback: number): number {
|
|
5
|
+
const raw = process.env[name];
|
|
6
|
+
const parsed = raw != null ? Number(raw) : NaN;
|
|
7
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** After this long, treat continuous transient failures as a real outage. */
|
|
11
|
+
const SUSTAINED_THRESHOLD_MS = envMs("POOL_SUSTAINED_THRESHOLD_MS", 60_000);
|
|
12
|
+
/** After this long, exit so the orchestrator restarts the process. */
|
|
13
|
+
const FATAL_THRESHOLD_MS = envMs("POOL_FATAL_THRESHOLD_MS", 5 * 60_000);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Tracks consecutive transient pool errors. Severity escalates WARN → ERROR
|
|
17
|
+
* once failures persist past `SUSTAINED_THRESHOLD_MS`, and the process exits
|
|
18
|
+
* past `FATAL_THRESHOLD_MS` (relying on the orchestrator's restart policy).
|
|
19
|
+
* Call `markHealthy()` on any positive signal to reset.
|
|
20
|
+
*/
|
|
21
|
+
export class PoolErrorTracker {
|
|
22
|
+
private consecutive = 0;
|
|
23
|
+
private firstFailureAt = 0;
|
|
24
|
+
|
|
25
|
+
log(err: unknown, tags: string[]): void {
|
|
26
|
+
// Non-transient errors are a different failure mode (auth, schema, etc.).
|
|
27
|
+
if (!isTransientPgError(err)) {
|
|
28
|
+
log.remote(
|
|
29
|
+
ComponentNames.EFFECTSTREAM_PGLITE,
|
|
30
|
+
tags,
|
|
31
|
+
SeverityNumber.ERROR,
|
|
32
|
+
(l) => l(err),
|
|
33
|
+
);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (this.consecutive === 0) this.firstFailureAt = Date.now();
|
|
38
|
+
this.consecutive++;
|
|
39
|
+
|
|
40
|
+
const durationMs = Date.now() - this.firstFailureAt;
|
|
41
|
+
const sustained = durationMs >= SUSTAINED_THRESHOLD_MS;
|
|
42
|
+
const fatal = durationMs >= FATAL_THRESHOLD_MS;
|
|
43
|
+
|
|
44
|
+
log.remote(
|
|
45
|
+
ComponentNames.EFFECTSTREAM_PGLITE,
|
|
46
|
+
fatal
|
|
47
|
+
? [...tags, "sustained-failure", "fatal"]
|
|
48
|
+
: sustained
|
|
49
|
+
? [...tags, "sustained-failure"]
|
|
50
|
+
: tags,
|
|
51
|
+
sustained ? SeverityNumber.ERROR : SeverityNumber.WARN,
|
|
52
|
+
(l) => l({ err, consecutive: this.consecutive, durationMs }),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (fatal) {
|
|
56
|
+
// Defer to the next tick so the log line above has a chance to flush.
|
|
57
|
+
setImmediate(() => process.exit(1));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Call when the DB has proven reachable (e.g. successful pool connect). */
|
|
62
|
+
markHealthy(): void {
|
|
63
|
+
this.consecutive = 0;
|
|
64
|
+
this.firstFailureAt = 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Read-only snapshot for health checks, heartbeats, or external probes. */
|
|
68
|
+
state(): {
|
|
69
|
+
consecutive: number;
|
|
70
|
+
firstFailureAt: number;
|
|
71
|
+
sustainedDurationMs: number;
|
|
72
|
+
sustained: boolean;
|
|
73
|
+
} {
|
|
74
|
+
const durationMs = this.firstFailureAt === 0
|
|
75
|
+
? 0
|
|
76
|
+
: Date.now() - this.firstFailureAt;
|
|
77
|
+
return {
|
|
78
|
+
consecutive: this.consecutive,
|
|
79
|
+
firstFailureAt: this.firstFailureAt,
|
|
80
|
+
sustainedDurationMs: durationMs,
|
|
81
|
+
sustained: durationMs >= SUSTAINED_THRESHOLD_MS,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Process-wide singleton — failures aggregate at DB-health granularity. */
|
|
87
|
+
export const poolErrors = new PoolErrorTracker();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { isTransientPgError } from "./transient-pg-errors.ts";
|
|
3
|
+
|
|
4
|
+
test("isTransientPgError - Neon pg-pool TLS reset", () => {
|
|
5
|
+
expect(
|
|
6
|
+
isTransientPgError({
|
|
7
|
+
code: "ECONNRESET",
|
|
8
|
+
message:
|
|
9
|
+
"Client network socket disconnected before secure TLS connection was established",
|
|
10
|
+
stack: " at /app/node_modules/.bun/pg-pool@3.14.0/node_modules/pg-pool/index.js:45:11",
|
|
11
|
+
}),
|
|
12
|
+
).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("isTransientPgError - rejects non-pg ETIMEDOUT", () => {
|
|
16
|
+
expect(
|
|
17
|
+
isTransientPgError({
|
|
18
|
+
code: "ETIMEDOUT",
|
|
19
|
+
message: "fetch failed",
|
|
20
|
+
stack: " at node:internal/deps/undici/undici.js:1234:5",
|
|
21
|
+
}),
|
|
22
|
+
).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("isTransientPgError - walks cause chain", () => {
|
|
26
|
+
const inner = {
|
|
27
|
+
code: "ECONNRESET",
|
|
28
|
+
message:
|
|
29
|
+
"Client network socket disconnected before secure TLS connection was established",
|
|
30
|
+
stack: " at node_modules/pg-pool/index.js:45:11",
|
|
31
|
+
};
|
|
32
|
+
expect(isTransientPgError({ message: "wrapper", cause: inner })).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("isTransientPgError - rejects logic errors", () => {
|
|
36
|
+
expect(isTransientPgError(new Error("column foo does not exist"))).toBe(
|
|
37
|
+
false,
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("isTransientPgError - idle pool message without stack", () => {
|
|
42
|
+
expect(
|
|
43
|
+
isTransientPgError({
|
|
44
|
+
message: "Connection terminated unexpectedly",
|
|
45
|
+
}),
|
|
46
|
+
).toBe(true);
|
|
47
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const PG_STACK_RE = /node_modules\/(pg-pool|pg)\//;
|
|
2
|
+
|
|
3
|
+
const TRANSIENT_PG_MESSAGES = [
|
|
4
|
+
"Connection terminated unexpectedly",
|
|
5
|
+
"Client has encountered a connection error",
|
|
6
|
+
"Client network socket disconnected before secure TLS connection",
|
|
7
|
+
"Query read timeout",
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
10
|
+
function* walkErrors(reason: unknown): Generator<unknown> {
|
|
11
|
+
let current: unknown = reason;
|
|
12
|
+
const seen = new Set<unknown>();
|
|
13
|
+
while (current != null && typeof current === "object" && !seen.has(current)) {
|
|
14
|
+
seen.add(current);
|
|
15
|
+
yield current;
|
|
16
|
+
current = (current as { cause?: unknown }).cause;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isFromPgStack(err: unknown): boolean {
|
|
21
|
+
if (err == null || typeof err !== "object" || !("stack" in err)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return PG_STACK_RE.test(String((err as Error).stack));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function hasTransientPgMessage(err: unknown): boolean {
|
|
28
|
+
if (err == null || typeof err !== "object") return false;
|
|
29
|
+
const msg = (err as { message?: string }).message;
|
|
30
|
+
if (typeof msg !== "string") return false;
|
|
31
|
+
return TRANSIENT_PG_MESSAGES.some((fragment) => msg.includes(fragment));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isTransientPgErrorSingle(err: unknown): boolean {
|
|
35
|
+
if (err == null || typeof err !== "object") return false;
|
|
36
|
+
if (hasTransientPgMessage(err)) return true;
|
|
37
|
+
if (!isFromPgStack(err)) return false;
|
|
38
|
+
const code = (err as { code?: string }).code;
|
|
39
|
+
return code === "ECONNRESET" || code === "EPIPE" ||
|
|
40
|
+
code === "ECONNREFUSED";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns true for pg / network errors that are normal on serverless Postgres
|
|
45
|
+
* (Neon, etc.): cold-start RSTs, idle-socket evictions, transient TLS blips.
|
|
46
|
+
* The pool recovers on the next query — use with `installUnhandledRejectionLogger`
|
|
47
|
+
* from `@effectstream/log` so these do not crash the process.
|
|
48
|
+
*
|
|
49
|
+
* Intentionally narrow: known-recoverable conditions only; logic/SQL bugs still crash.
|
|
50
|
+
*/
|
|
51
|
+
export function isTransientPgError(reason: unknown): boolean {
|
|
52
|
+
for (const err of walkErrors(reason)) {
|
|
53
|
+
if (isTransientPgErrorSingle(err)) return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|