@rocicorp/zero 1.6.0-canary.1 → 1.6.0-canary.3
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/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slots.js +33 -33
- package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/package.json +1 -1
package/out/zero/package.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
var package_default = {
|
|
2
2
|
name: "@rocicorp/zero",
|
|
3
|
-
version: "1.6.0-canary.
|
|
3
|
+
version: "1.6.0-canary.3",
|
|
4
4
|
description: "Zero is a web framework for serverless web development.",
|
|
5
5
|
homepage: "https://zero.rocicorp.dev",
|
|
6
6
|
bugs: { "url": "https://bugs.rocicorp.dev" },
|
package/out/zero/package.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"package.js","names":[],"sources":["../../package.json"],"sourcesContent":["{\n \"name\": \"@rocicorp/zero\",\n \"version\": \"1.6.0-canary.
|
|
1
|
+
{"version":3,"file":"package.js","names":[],"sources":["../../package.json"],"sourcesContent":["{\n \"name\": \"@rocicorp/zero\",\n \"version\": \"1.6.0-canary.3\",\n \"description\": \"Zero is a web framework for serverless web development.\",\n \"homepage\": \"https://zero.rocicorp.dev\",\n \"bugs\": {\n \"url\": \"https://bugs.rocicorp.dev\"\n },\n \"license\": \"Apache-2.0\",\n \"author\": \"Rocicorp, Inc.\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/rocicorp/mono.git\",\n \"directory\": \"packages/zero\"\n },\n \"bin\": {\n \"analyze-query\": \"./out/zero/src/analyze-query.js\",\n \"ast-to-zql\": \"./out/zero/src/ast-to-zql.js\",\n \"transform-query\": \"./out/zero/src/transform-query.js\",\n \"zero-build-schema\": \"./out/zero/src/build-schema.js\",\n \"zero-cache\": \"./out/zero/src/cli.js\",\n \"zero-cache-dev\": \"./out/zero/src/zero-cache-dev.js\",\n \"zero-deploy-permissions\": \"./out/zero/src/deploy-permissions.js\",\n \"zero-out\": \"./out/zero/src/zero-out.js\"\n },\n \"files\": [\n \"out\",\n \"!*.tsbuildinfo\"\n ],\n \"type\": \"module\",\n \"main\": \"out/zero/src/zero.js\",\n \"module\": \"out/zero/src/zero.js\",\n \"types\": \"out/zero/src/zero.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./out/zero/src/zero.d.ts\",\n \"default\": \"./out/zero/src/zero.js\"\n },\n \"./analyze\": {\n \"types\": \"./out/zero/src/analyze.d.ts\",\n \"default\": \"./out/zero/src/analyze.js\"\n },\n \"./bindings\": {\n \"types\": \"./out/zero/src/bindings.d.ts\",\n \"default\": \"./out/zero/src/bindings.js\"\n },\n \"./change-protocol/v0\": {\n \"types\": \"./out/zero/src/change-protocol/v0.d.ts\",\n \"default\": \"./out/zero/src/change-protocol/v0.js\"\n },\n \"./expo-sqlite\": {\n \"types\": \"./out/zero/src/expo-sqlite.d.ts\",\n \"default\": \"./out/zero/src/expo-sqlite.js\"\n },\n \"./op-sqlite\": {\n \"types\": \"./out/zero/src/op-sqlite.d.ts\",\n \"default\": \"./out/zero/src/op-sqlite.js\"\n },\n \"./pg\": {\n \"types\": \"./out/zero/src/pg.d.ts\",\n \"default\": \"./out/zero/src/pg.js\"\n },\n \"./react\": {\n \"types\": \"./out/zero/src/react.d.ts\",\n \"default\": \"./out/zero/src/react.js\"\n },\n \"./react-native\": {\n \"types\": \"./out/zero/src/react-native.d.ts\",\n \"default\": \"./out/zero/src/react-native.js\"\n },\n \"./server\": {\n \"types\": \"./out/zero/src/server.d.ts\",\n \"default\": \"./out/zero/src/server.js\"\n },\n \"./server/adapters/drizzle\": {\n \"types\": \"./out/zero/src/adapters/drizzle.d.ts\",\n \"default\": \"./out/zero/src/adapters/drizzle.js\"\n },\n \"./server/adapters/kysely\": {\n \"types\": \"./out/zero/src/adapters/kysely.d.ts\",\n \"default\": \"./out/zero/src/adapters/kysely.js\"\n },\n \"./server/adapters/prisma\": {\n \"types\": \"./out/zero/src/adapters/prisma.d.ts\",\n \"default\": \"./out/zero/src/adapters/prisma.js\"\n },\n \"./server/adapters/pg\": {\n \"types\": \"./out/zero/src/adapters/pg.d.ts\",\n \"default\": \"./out/zero/src/adapters/pg.js\"\n },\n \"./server/adapters/postgresjs\": {\n \"types\": \"./out/zero/src/adapters/postgresjs.d.ts\",\n \"default\": \"./out/zero/src/adapters/postgresjs.js\"\n },\n \"./solid\": {\n \"types\": \"./out/zero/src/solid.d.ts\",\n \"default\": \"./out/zero/src/solid.js\"\n },\n \"./sqlite\": {\n \"types\": \"./out/zero/src/sqlite.d.ts\",\n \"default\": \"./out/zero/src/sqlite.js\"\n },\n \"./zqlite\": {\n \"types\": \"./out/zero/src/zqlite.d.ts\",\n \"default\": \"./out/zero/src/zqlite.js\"\n }\n },\n \"scripts\": {\n \"build\": \"node --experimental-strip-types --no-warnings tool/build.ts\",\n \"build:watch\": \"node --experimental-strip-types --no-warnings tool/build.ts --watch\",\n \"check-types\": \"tsc -p tsconfig.client.json && tsc -p tsconfig.server.json\",\n \"check-types:client:watch\": \"tsc -p tsconfig.client.json --watch\",\n \"check-types:server:watch\": \"tsc -p tsconfig.server.json --watch\",\n \"format\": \"oxfmt .\",\n \"check-format\": \"oxfmt --check .\",\n \"lint\": \"oxlint --quiet --config ../../oxlint.config.ts src/\",\n \"docs\": \"node --experimental-strip-types --no-warnings tool/generate-docs.ts\",\n \"docs:server\": \"node --watch --experimental-strip-types --no-warnings tool/generate-docs.ts --server\",\n \"fmt\": \"oxfmt .\",\n \"check-fmt\": \"oxfmt --check .\"\n },\n \"dependencies\": {\n \"@badrap/valita\": \"0.3.11\",\n \"@databases/escape-identifier\": \"^1.0.3\",\n \"@databases/sql\": \"^3.3.0\",\n \"@dotenvx/dotenvx\": \"^1.39.0\",\n \"@drdgvhbh/postgres-error-codes\": \"^0.0.6\",\n \"@fastify/cors\": \"^10.0.0\",\n \"@fastify/websocket\": \"^11.0.0\",\n \"@google-cloud/precise-date\": \"^4.0.0\",\n \"@opentelemetry/api\": \"^1.9.0\",\n \"@opentelemetry/api-logs\": \"^0.203.0\",\n \"@opentelemetry/auto-instrumentations-node\": \"^0.62.0\",\n \"@opentelemetry/exporter-metrics-otlp-http\": \"^0.203.0\",\n \"@opentelemetry/resources\": \"^2.0.1\",\n \"@opentelemetry/sdk-metrics\": \"^2.0.1\",\n \"@opentelemetry/sdk-node\": \"^0.203.0\",\n \"@opentelemetry/sdk-trace-node\": \"^2.0.1\",\n \"@postgresql-typed/oids\": \"^0.2.0\",\n \"@rocicorp/lock\": \"^1.0.4\",\n \"@rocicorp/logger\": \"^5.4.0\",\n \"@rocicorp/resolver\": \"^1.0.2\",\n \"@rocicorp/zero-sqlite3\": \"^1.0.18\",\n \"@standard-schema/spec\": \"^1.0.0\",\n \"@types/basic-auth\": \"^1.1.8\",\n \"@types/ws\": \"^8.5.12\",\n \"basic-auth\": \"^2.0.1\",\n \"chalk-template\": \"^1.1.0\",\n \"chokidar\": \"^4.0.1\",\n \"cloudevents\": \"^10.0.0\",\n \"command-line-args\": \"^6.0.1\",\n \"command-line-usage\": \"^7.0.3\",\n \"compare-utf8\": \"^0.2.0\",\n \"defu\": \"^6.1.4\",\n \"eventemitter3\": \"^5.0.1\",\n \"fastify\": \"^5.0.0\",\n \"is-in-subnet\": \"^4.0.1\",\n \"jose\": \"^5.9.3\",\n \"js-xxhash\": \"^4.0.0\",\n \"json-custom-numbers\": \"^3.1.1\",\n \"kasi\": \"^1.1.0\",\n \"nanoid\": \"^5.1.2\",\n \"oxfmt\": \"^0.45.0\",\n \"parse-prometheus-text-format\": \"^1.1.1\",\n \"pg-format\": \"npm:pg-format-fix@^1.0.5\",\n \"postgres\": \"3.4.7\",\n \"semver\": \"^7.5.4\",\n \"tsx\": \"^4.21.0\",\n \"url-pattern\": \"^1.0.3\",\n \"urlpattern-polyfill\": \"^10.1.0\",\n \"ws\": \"^8.18.1\"\n },\n \"devDependencies\": {\n \"@op-engineering/op-sqlite\": \">=15\",\n \"@vitest/runner\": \"^4.1.5\",\n \"analyze-query\": \"0.0.0\",\n \"ast-to-zql\": \"0.0.0\",\n \"expo-sqlite\": \">=15\",\n \"replicache\": \"15.2.1\",\n \"shared\": \"0.0.0\",\n \"syncpack\": \"^14.3.0\",\n \"typedoc\": \"^0.28.17\",\n \"typedoc-plugin-markdown\": \"^4.10.0\",\n \"typescript\": \"~6.0.2\",\n \"vite\": \"8.0.3\",\n \"vitest\": \"^4.1.5\",\n \"zero-cache\": \"0.0.0\",\n \"zero-client\": \"0.0.0\",\n \"zero-pg\": \"0.0.0\",\n \"zero-react\": \"0.0.0\",\n \"zero-server\": \"0.0.0\",\n \"zero-solid\": \"0.0.0\",\n \"zqlite\": \"0.0.0\"\n },\n \"peerDependencies\": {\n \"@op-engineering/op-sqlite\": \">=15\",\n \"expo-sqlite\": \">=15\",\n \"kysely\": \"^0.28.16\"\n },\n \"peerDependenciesMeta\": {\n \"kysely\": {\n \"optional\": true\n },\n \"expo-sqlite\": {\n \"optional\": true\n },\n \"@op-engineering/op-sqlite\": {\n \"optional\": true\n }\n },\n \"engines\": {\n \"node\": \">=22\"\n }\n}"],"mappings":""}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LogContext } from '@rocicorp/logger';
|
|
2
|
-
import postgres from 'postgres';
|
|
2
|
+
import type postgres from 'postgres';
|
|
3
3
|
import { type PostgresDB } from '../../../types/pg';
|
|
4
4
|
import { type ShardID } from '../../../types/shards';
|
|
5
5
|
export type ReplicationSlot = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replication-slots.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/change-source/pg/replication-slots.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,QAAQ,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"replication-slots.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/change-source/pg/replication-slots.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,QAAQ,MAAM,UAAU,CAAC;AAErC,OAAO,EAAkB,KAAK,UAAU,EAAC,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAiB,KAAK,OAAO,EAAC,MAAM,uBAAuB,CAAC;AAUnE,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,MAAM,CAAC;IAGjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAGnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAoBF,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,QAAQ,CAAC,GAAG,EACrB,EAAC,QAAQ,EAAE,QAAQ,EAAE,WAAoC,EAAC,EAAE,cAAc,GACzE,OAAO,CAAC,eAAe,CAAC,CA0C1B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,UAAU,EACf,kBAAkB,EAAE,QAAQ,CAAC,GAAG,EAChC,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,OAAO,GAChB,OAAO,CAAC,eAAe,CAAC,CAmE1B;AAED;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,OAAO,EACd,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,CAAC,CAY9D;AA+DD,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,UAUvC"}
|
|
@@ -4,7 +4,6 @@ import { toStateVersionString } from "./lsn.js";
|
|
|
4
4
|
import { runTx } from "../../../db/run-transaction.js";
|
|
5
5
|
import { createReplica, replicationSlotExpression, replicationSlotPrefix } from "./schema/shard.js";
|
|
6
6
|
import { orTimeout } from "../../../types/timeout.js";
|
|
7
|
-
import postgres from "postgres";
|
|
8
7
|
import { PG_CONFIGURATION_LIMIT_EXCEEDED, PG_INSUFFICIENT_PRIVILEGE } from "@drdgvhbh/postgres-error-codes";
|
|
9
8
|
//#region ../zero-cache/src/services/change-source/pg/replication-slots.ts
|
|
10
9
|
var CREATE_REPLICATION_SLOT_TIMEOUT_MS = 3e4;
|
|
@@ -46,49 +45,46 @@ async function createReplicationSlot(lc, session, { slotName, failover, lockTime
|
|
|
46
45
|
async function createReplicaAndSlot(lc, sql, replicationSession, shard, replicaID, failover) {
|
|
47
46
|
const lockName = replicationSlotManagementLock(shard);
|
|
48
47
|
const slotPoolPrefix = replicationSlotPrefix(shard);
|
|
49
|
-
for (let first = true;; first = false)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
for (let first = true;; first = false) {
|
|
49
|
+
await dropUnclaimedSlots(lc, sql, shard);
|
|
50
|
+
try {
|
|
51
|
+
return await runTx(sql, async (tx) => {
|
|
52
|
+
await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;
|
|
53
|
+
let slotName;
|
|
54
|
+
const names = await tx`
|
|
54
55
|
SELECT slot_name as name FROM pg_replication_slots
|
|
55
56
|
WHERE slot_name LIKE ${slotPoolPrefix + "%"};
|
|
56
57
|
`.values();
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
const inUse = new Set(names.flat());
|
|
59
|
+
for (let next = 0;; next++) {
|
|
60
|
+
const candidateName = `${slotPoolPrefix}${slotPoolSuffix(next)}`;
|
|
61
|
+
if (!inUse.has(candidateName)) {
|
|
62
|
+
slotName = candidateName;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
63
65
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
const slot = await createReplicationSlot(lc, replicationSession, {
|
|
67
|
+
slotName,
|
|
68
|
+
failover
|
|
69
|
+
});
|
|
70
|
+
await createReplica(tx, shard, replicaID, slot.slot_name, toStateVersionString(slot.consistent_point));
|
|
71
|
+
return slot;
|
|
68
72
|
});
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
});
|
|
72
|
-
} catch (e) {
|
|
73
|
-
if (first && e instanceof postgres.PostgresError) {
|
|
74
|
-
if (isPostgresError(e, PG_INSUFFICIENT_PRIVILEGE)) {
|
|
73
|
+
} catch (e) {
|
|
74
|
+
if (first && isPostgresError(e, PG_INSUFFICIENT_PRIVILEGE)) {
|
|
75
75
|
await sql`ALTER ROLE current_user WITH REPLICATION`;
|
|
76
76
|
lc.info?.(`Added the REPLICATION role to database user`);
|
|
77
77
|
continue;
|
|
78
78
|
}
|
|
79
|
-
if (isPostgresError(e, PG_CONFIGURATION_LIMIT_EXCEEDED)) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
lc.warn?.(`Dropped inactive replication slots: ${dropped.map(({ slot }) => slot)}`, e);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
lc.error?.(`Unable to drop replication slots`, e);
|
|
79
|
+
if (first && isPostgresError(e, PG_CONFIGURATION_LIMIT_EXCEEDED)) {
|
|
80
|
+
lc.warn?.(`Reached max replication slots. Attempting to clean up unused slots`, e);
|
|
81
|
+
await sql`
|
|
82
|
+
DELETE FROM ${sql(`${upstreamSchema(shard)}.replicas`)} USING pg_replication_slots slots
|
|
83
|
+
WHERE replicas.slot = slots.slot_name AND NOT slots.active`;
|
|
84
|
+
continue;
|
|
89
85
|
}
|
|
86
|
+
throw e;
|
|
90
87
|
}
|
|
91
|
-
throw e;
|
|
92
88
|
}
|
|
93
89
|
}
|
|
94
90
|
/**
|
|
@@ -111,8 +107,12 @@ async function dropOldReplicasAndSlots(lc, sql, shard, beforeRank) {
|
|
|
111
107
|
lc.info?.(`Deleting ${oldReplicas.length} old replica(s)`, { oldReplicas });
|
|
112
108
|
await sql`DELETE FROM ${sql(replicasTable)} WHERE rank < ${beforeRank}`;
|
|
113
109
|
}
|
|
110
|
+
return dropUnclaimedSlots(lc, sql, shard);
|
|
111
|
+
}
|
|
112
|
+
function dropUnclaimedSlots(lc, sql, shard) {
|
|
114
113
|
const lockName = replicationSlotManagementLock(shard);
|
|
115
114
|
const slotExpression = replicationSlotExpression(shard);
|
|
115
|
+
const replicasTable = `${upstreamSchema(shard)}.replicas`;
|
|
116
116
|
return runTx(sql, async (tx) => {
|
|
117
117
|
await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;
|
|
118
118
|
const dropped = await tx`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replication-slots.js","names":[],"sources":["../../../../../../../zero-cache/src/services/change-source/pg/replication-slots.ts"],"sourcesContent":["import {\n PG_CONFIGURATION_LIMIT_EXCEEDED,\n PG_INSUFFICIENT_PRIVILEGE,\n} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport postgres from 'postgres';\nimport {runTx} from '../../../db/run-transaction';\nimport {isPostgresError, type PostgresDB} from '../../../types/pg';\nimport {upstreamSchema, type ShardID} from '../../../types/shards';\nimport {orTimeout} from '../../../types/timeout';\nimport {toStateVersionString} from './lsn';\nimport {\n createReplica,\n replicationSlotExpression,\n replicationSlotPrefix,\n} from './schema/shard';\n\n// Record returned by `CREATE_REPLICATION_SLOT`\nexport type ReplicationSlot = {\n slot_name: string;\n consistent_point: string;\n snapshot_name: string;\n output_plugin: string;\n};\n\nexport type CreateSlotSpec = {\n slotName: string;\n\n // Note: must be false if pgVersion < PG_17. Caller must verify.\n failover?: boolean;\n\n // For overriding in tests.\n lockTimeout?: number;\n};\n\n// When creating a replication slot, Postgres waits for open transactions\n// to complete before reserving a consistent_point (LSN) in the WAL and creating\n// a matching transaction snapshot. As such, it can technically take an arbitrary\n// amount of time (e.g. DDL operations, table-wide operations, etc.).\n//\n// However, to detect pathological situations, bound the amount of time that\n// the server waits for replication slot creation, so that a continual failure to\n// create a replication slot is surfaced by errors / alerts.\nconst CREATE_REPLICATION_SLOT_TIMEOUT_MS = 30_000;\n\n// The lock_timeout is set 1s before the client-side orTimeout so that\n// Postgres reliably aborts first and tears down the walsender cleanly.\n// The client-side timeout remains as a fallback for network-level failures.\nconst SERVER_LOCK_TIMEOUT_MS = CREATE_REPLICATION_SLOT_TIMEOUT_MS - 1_000;\n\n// Note: The replication connection does not support the extended query protocol,\n// so all commands must be sent using sql.unsafe(). This is technically safe\n// because all placeholder values are under our control (i.e. \"slotName\").\nexport async function createReplicationSlot(\n lc: LogContext,\n session: postgres.Sql,\n {slotName, failover, lockTimeout = SERVER_LOCK_TIMEOUT_MS}: CreateSlotSpec,\n): Promise<ReplicationSlot> {\n // CREATE_REPLICATION_SLOT can hang indefinitely waiting for long-running\n // transactions to finish: internally it calls SnapBuildWaitSnapshot →\n // XactLockTableWait → LockAcquire on each running XID. statement_timeout\n // does NOT apply to replication commands, but lock_timeout does (it governs\n // the heavyweight lock wait inside LockAcquire). Setting it here causes\n // Postgres to raise ERRCODE_LOCK_NOT_AVAILABLE and cleanly tear down the\n // walsender, rather than relying solely on the client-side orTimeout\n // which can leave an orphaned backend.\n //\n // An orphaned walsender is actively harmful: by this point the replication\n // slot has already been created and is pinning WAL retention and catalog_xmin.\n // Worse, the slot is marked `active` (the walsender PID is still alive), so\n // the existing cleanup code (which drops inactive slots on retry) can't\n // reclaim it. Without lock_timeout the orphan persists until TCP keepalive\n // fires (~2h default) or the blocking transaction finishes.\n await session.unsafe(`SET lock_timeout = ${lockTimeout}`);\n\n const createSlot = failover\n ? session.unsafe<ReplicationSlot[]>(\n /*sql*/ `CREATE_REPLICATION_SLOT \"${slotName}\" LOGICAL pgoutput (FAILOVER)`,\n )\n : session.unsafe<ReplicationSlot[]>(\n /*sql*/ `CREATE_REPLICATION_SLOT \"${slotName}\" LOGICAL pgoutput`,\n );\n const raced = await orTimeout(createSlot, CREATE_REPLICATION_SLOT_TIMEOUT_MS);\n if (raced === 'timed-out') {\n // Create slot can block indefinitely waiting for old transactions. End\n // this connection in the background and fail fast so the process restarts.\n void session\n .end()\n .catch(e =>\n lc.warn?.(`Error closing timed out replication slot session`, e),\n );\n throw new Error(\n `Timed out after ${CREATE_REPLICATION_SLOT_TIMEOUT_MS} ms creating replication slot ${slotName}. ` +\n `Crashing to force a clean restart.`,\n );\n }\n const [slot] = raced;\n lc.info?.(`Created replication slot ${slotName}`, slot);\n return slot;\n}\n\n/**\n * Replica and slot creation involves two sessions for proper coordination\n * with other replica management logic:\n *\n * * A normal transaction is started and acquires an advisory lock for\n * replica slot management. This is the same lock that cleanup logic\n * acquires before cleaning up replication slots.\n * * With the lock held, a new replication slot is created in a\n * replication session. The API of CREATE_REPLICATION_SLOT is such\n * that it cannot be done in a transaction, and cannot be followed by\n * any writes, or else its snapshot (which is needed for initial sync)\n * would be invalidated.\n * * Once the slot is created, the slot and replica information are recorded\n * in the `replicas` table before releasing the lock.\n *\n * This locking ensures that:\n * 1. multiple replication managers attempting to create a replication slot\n * will not use the same name for the replication slot (which is selected\n * from a pool of reused names).\n * 2. Running replication managers (which use an earlier replica of a lower\n * rank) will not delete the new slot during their cleanup logic, since\n * the slot will belong to a replica of a higher rank.\n */\nexport async function createReplicaAndSlot(\n lc: LogContext,\n sql: PostgresDB,\n replicationSession: postgres.Sql,\n shard: ShardID,\n replicaID: string,\n failover: boolean,\n): Promise<ReplicationSlot> {\n const lockName = replicationSlotManagementLock(shard);\n const slotPoolPrefix = replicationSlotPrefix(shard);\n for (let first = true; ; first = false) {\n try {\n return runTx(sql, async tx => {\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n // Pick an available slotName from the slotPoolPrefix pool.\n let slotName: string;\n const names = await tx<{name: string}[]> /*sql*/ `\n SELECT slot_name as name FROM pg_replication_slots\n WHERE slot_name LIKE ${slotPoolPrefix + '%'};\n `.values();\n const inUse = new Set(names.flat());\n for (let next = 0; ; next++) {\n const candidateName = `${slotPoolPrefix}${slotPoolSuffix(next)}`;\n if (!inUse.has(candidateName)) {\n slotName = candidateName;\n break;\n }\n }\n\n const slot = await createReplicationSlot(lc, replicationSession, {\n slotName,\n failover,\n });\n\n await createReplica(\n tx,\n shard,\n replicaID,\n slot.slot_name,\n toStateVersionString(slot.consistent_point),\n );\n\n return slot;\n });\n } catch (e) {\n if (first && e instanceof postgres.PostgresError) {\n if (isPostgresError(e, PG_INSUFFICIENT_PRIVILEGE)) {\n // Some Postgres variants (e.g. Google Cloud SQL) require that\n // the user have the REPLICATION role in order to create a slot.\n // Note that this must be done by the upstreamDB connection, and\n // does not work in the replicationSession itself.\n await sql`ALTER ROLE current_user WITH REPLICATION`;\n lc.info?.(`Added the REPLICATION role to database user`);\n continue;\n }\n if (isPostgresError(e, PG_CONFIGURATION_LIMIT_EXCEEDED)) {\n const slotExpression = replicationSlotExpression(shard);\n\n const dropped = await sql<{slot: string}[]>`\n SELECT slot_name as slot, pg_drop_replication_slot(slot_name)\n FROM pg_replication_slots\n WHERE slot_name LIKE ${slotExpression} AND NOT active`;\n if (dropped.length) {\n lc.warn?.(\n `Dropped inactive replication slots: ${dropped.map(({slot}) => slot)}`,\n e,\n );\n continue;\n }\n lc.error?.(`Unable to drop replication slots`, e);\n }\n }\n throw e;\n }\n }\n}\n\n/**\n * Deletes \"old\" replicas (i.e. those with a lower rank than the current)\n * and attempts to drop replication slots that are not associated with any\n * replica.\n *\n * If a slot could not be dropped because there is still an active subscriber,\n * it will be reflected in the `draining` count that is returned. When there\n * are draining slots, the method should be retried until all orphaned slots\n * have been dropped.\n */\nexport async function dropOldReplicasAndSlots(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n beforeRank: bigint,\n): Promise<{dropped: number; active: number; draining: number}> {\n const replicasTable = `${upstreamSchema(shard)}.replicas`;\n const oldReplicas = await sql`\n SELECT id, rank::float8, slot, version, \"initialSyncContext\", \"subscriberContext\"\n FROM ${sql(replicasTable)} WHERE rank < ${beforeRank};\n `;\n if (oldReplicas.length) {\n lc.info?.(`Deleting ${oldReplicas.length} old replica(s)`, {oldReplicas});\n await sql`DELETE FROM ${sql(replicasTable)} WHERE rank < ${beforeRank}`;\n }\n\n // The slot / replica cleanup happens within a transaction while holding\n // the replication slot management lock for this shard, to ensure that no\n // slot that belongs to a newer replica is dropped.\n const lockName = replicationSlotManagementLock(shard);\n const slotExpression = replicationSlotExpression(shard);\n return runTx(sql, async tx => {\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n const dropped = await tx /*sql*/ `\n SELECT slot_name as slot, pg_drop_replication_slot(slot_name) \n FROM pg_replication_slots\n LEFT JOIN ${tx(replicasTable)} replica on slot_name = slot\n WHERE slot_name LIKE ${slotExpression} \n AND NOT active\n AND replica.id IS NULL;\n `;\n if (dropped.length) {\n lc.info?.(`dropped inactive replication slots`, {dropped});\n }\n\n const remaining = await tx<\n {slot: string; pid: number | null; id: string | null}[]\n > /*sql*/ `\n SELECT slot_name as slot, active_pid as pid, replica.id as id\n FROM pg_replication_slots\n LEFT JOIN ${tx(replicasTable)} replica on slot_name = slot\n WHERE slot_name LIKE ${slotExpression};\n `;\n if (remaining.length) {\n lc.info?.(`remaining replication slots`, {remaining});\n }\n\n let active = 0;\n let draining = 0;\n for (const {id} of remaining) {\n if (id === null) {\n draining++;\n } else {\n active++;\n }\n }\n\n return {\n dropped: dropped.length,\n active,\n draining,\n };\n });\n}\n\nconst ALPHABET = 'abcdefghijklmnopqrstuvwxyz';\n\n// Alphabetic notation is used as the slot pool suffix to distinguish\n// it from the (numeric) shard num that's also encoded in the slot name.\nexport function slotPoolSuffix(n: number) {\n n++; // Adjust for 0-based indexing\n\n let suffix = '';\n while (n > 0) {\n n--;\n suffix = ALPHABET[n % 26] + suffix;\n n = Math.floor(n / 26);\n }\n return suffix;\n}\n\nfunction replicationSlotManagementLock(shard: ShardID) {\n return `replication-slot-management:${shard.appID}_${shard.shardNum}`;\n}\n"],"mappings":";;;;;;;;;AA2CA,IAAM,qCAAqC;AAK3C,IAAM,yBAAyB,qCAAqC;AAKpE,eAAsB,sBACpB,IACA,SACA,EAAC,UAAU,UAAU,cAAc,0BACT;AAgB1B,OAAM,QAAQ,OAAO,sBAAsB,cAAc;CASzD,MAAM,QAAQ,MAAM,UAPD,WACf,QAAQ,OACE,4BAA4B,SAAS,+BAC9C,GACD,QAAQ,OACE,4BAA4B,SAAS,oBAC9C,EACqC,mCAAmC;AAC7E,KAAI,UAAU,aAAa;AAGpB,UACF,KAAK,CACL,OAAM,MACL,GAAG,OAAO,oDAAoD,EAAE,CACjE;AACH,QAAM,IAAI,MACR,mBAAmB,mCAAmC,gCAAgC,SAAS,sCAEhG;;CAEH,MAAM,CAAC,QAAQ;AACf,IAAG,OAAO,4BAA4B,YAAY,KAAK;AACvD,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;AA0BT,eAAsB,qBACpB,IACA,KACA,oBACA,OACA,WACA,UAC0B;CAC1B,MAAM,WAAW,8BAA8B,MAAM;CACrD,MAAM,iBAAiB,sBAAsB,MAAM;AACnD,MAAK,IAAI,QAAQ,OAAQ,QAAQ,MAC/B,KAAI;AACF,SAAO,MAAM,KAAK,OAAM,OAAM;AAC5B,SAAM,EAAE,yCAAyC,SAAS;GAG1D,IAAI;GACJ,MAAM,QAAQ,MAAM,EAA6B;;mCAEtB,iBAAiB,IAAI;UAC9C,QAAQ;GACV,MAAM,QAAQ,IAAI,IAAI,MAAM,MAAM,CAAC;AACnC,QAAK,IAAI,OAAO,IAAK,QAAQ;IAC3B,MAAM,gBAAgB,GAAG,iBAAiB,eAAe,KAAK;AAC9D,QAAI,CAAC,MAAM,IAAI,cAAc,EAAE;AAC7B,gBAAW;AACX;;;GAIJ,MAAM,OAAO,MAAM,sBAAsB,IAAI,oBAAoB;IAC/D;IACA;IACD,CAAC;AAEF,SAAM,cACJ,IACA,OACA,WACA,KAAK,WACL,qBAAqB,KAAK,iBAAiB,CAC5C;AAED,UAAO;IACP;UACK,GAAG;AACV,MAAI,SAAS,aAAa,SAAS,eAAe;AAChD,OAAI,gBAAgB,GAAG,0BAA0B,EAAE;AAKjD,UAAM,GAAG;AACT,OAAG,OAAO,8CAA8C;AACxD;;AAEF,OAAI,gBAAgB,GAAG,gCAAgC,EAAE;IAGvD,MAAM,UAAU,MAAM,GAAqB;;;yCAFpB,0BAA0B,MAAM,CAKT;AAC9C,QAAI,QAAQ,QAAQ;AAClB,QAAG,OACD,uCAAuC,QAAQ,KAAK,EAAC,WAAU,KAAK,IACpE,EACD;AACD;;AAEF,OAAG,QAAQ,oCAAoC,EAAE;;;AAGrD,QAAM;;;;;;;;;;;;;AAeZ,eAAsB,wBACpB,IACA,KACA,OACA,YAC8D;CAC9D,MAAM,gBAAgB,GAAG,eAAe,MAAM,CAAC;CAC/C,MAAM,cAAc,MAAM,GAAG;;YAEnB,IAAI,cAAc,CAAC,gBAAgB,WAAW;;AAExD,KAAI,YAAY,QAAQ;AACtB,KAAG,OAAO,YAAY,YAAY,OAAO,kBAAkB,EAAC,aAAY,CAAC;AACzE,QAAM,GAAG,eAAe,IAAI,cAAc,CAAC,gBAAgB;;CAM7D,MAAM,WAAW,8BAA8B,MAAM;CACrD,MAAM,iBAAiB,0BAA0B,MAAM;AACvD,QAAO,MAAM,KAAK,OAAM,OAAM;AAC5B,QAAM,EAAE,yCAAyC,SAAS;EAE1D,MAAM,UAAU,MAAM,EAAW;;;oBAGjB,GAAG,cAAc,CAAC;+BACP,eAAe;;;;AAI1C,MAAI,QAAQ,OACV,IAAG,OAAO,sCAAsC,EAAC,SAAQ,CAAC;EAG5D,MAAM,YAAY,MAAM,EAEd;;;oBAGM,GAAG,cAAc,CAAC;+BACP,eAAe;;AAE1C,MAAI,UAAU,OACZ,IAAG,OAAO,+BAA+B,EAAC,WAAU,CAAC;EAGvD,IAAI,SAAS;EACb,IAAI,WAAW;AACf,OAAK,MAAM,EAAC,QAAO,UACjB,KAAI,OAAO,KACT;MAEA;AAIJ,SAAO;GACL,SAAS,QAAQ;GACjB;GACA;GACD;GACD;;AAGJ,IAAM,WAAW;AAIjB,SAAgB,eAAe,GAAW;AACxC;CAEA,IAAI,SAAS;AACb,QAAO,IAAI,GAAG;AACZ;AACA,WAAS,SAAS,IAAI,MAAM;AAC5B,MAAI,KAAK,MAAM,IAAI,GAAG;;AAExB,QAAO;;AAGT,SAAS,8BAA8B,OAAgB;AACrD,QAAO,+BAA+B,MAAM,MAAM,GAAG,MAAM"}
|
|
1
|
+
{"version":3,"file":"replication-slots.js","names":[],"sources":["../../../../../../../zero-cache/src/services/change-source/pg/replication-slots.ts"],"sourcesContent":["import {\n PG_CONFIGURATION_LIMIT_EXCEEDED,\n PG_INSUFFICIENT_PRIVILEGE,\n} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport type postgres from 'postgres';\nimport {runTx} from '../../../db/run-transaction';\nimport {isPostgresError, type PostgresDB} from '../../../types/pg';\nimport {upstreamSchema, type ShardID} from '../../../types/shards';\nimport {orTimeout} from '../../../types/timeout';\nimport {toStateVersionString} from './lsn';\nimport {\n createReplica,\n replicationSlotExpression,\n replicationSlotPrefix,\n} from './schema/shard';\n\n// Record returned by `CREATE_REPLICATION_SLOT`\nexport type ReplicationSlot = {\n slot_name: string;\n consistent_point: string;\n snapshot_name: string;\n output_plugin: string;\n};\n\nexport type CreateSlotSpec = {\n slotName: string;\n\n // Note: must be false if pgVersion < PG_17. Caller must verify.\n failover?: boolean;\n\n // For overriding in tests.\n lockTimeout?: number;\n};\n\n// When creating a replication slot, Postgres waits for open transactions\n// to complete before reserving a consistent_point (LSN) in the WAL and creating\n// a matching transaction snapshot. As such, it can technically take an arbitrary\n// amount of time (e.g. DDL operations, table-wide operations, etc.).\n//\n// However, to detect pathological situations, bound the amount of time that\n// the server waits for replication slot creation, so that a continual failure to\n// create a replication slot is surfaced by errors / alerts.\nconst CREATE_REPLICATION_SLOT_TIMEOUT_MS = 30_000;\n\n// The lock_timeout is set 1s before the client-side orTimeout so that\n// Postgres reliably aborts first and tears down the walsender cleanly.\n// The client-side timeout remains as a fallback for network-level failures.\nconst SERVER_LOCK_TIMEOUT_MS = CREATE_REPLICATION_SLOT_TIMEOUT_MS - 1_000;\n\n// Note: The replication connection does not support the extended query protocol,\n// so all commands must be sent using sql.unsafe(). This is technically safe\n// because all placeholder values are under our control (i.e. \"slotName\").\nexport async function createReplicationSlot(\n lc: LogContext,\n session: postgres.Sql,\n {slotName, failover, lockTimeout = SERVER_LOCK_TIMEOUT_MS}: CreateSlotSpec,\n): Promise<ReplicationSlot> {\n // CREATE_REPLICATION_SLOT can hang indefinitely waiting for long-running\n // transactions to finish: internally it calls SnapBuildWaitSnapshot →\n // XactLockTableWait → LockAcquire on each running XID. statement_timeout\n // does NOT apply to replication commands, but lock_timeout does (it governs\n // the heavyweight lock wait inside LockAcquire). Setting it here causes\n // Postgres to raise ERRCODE_LOCK_NOT_AVAILABLE and cleanly tear down the\n // walsender, rather than relying solely on the client-side orTimeout\n // which can leave an orphaned backend.\n //\n // An orphaned walsender is actively harmful: by this point the replication\n // slot has already been created and is pinning WAL retention and catalog_xmin.\n // Worse, the slot is marked `active` (the walsender PID is still alive), so\n // the existing cleanup code (which drops inactive slots on retry) can't\n // reclaim it. Without lock_timeout the orphan persists until TCP keepalive\n // fires (~2h default) or the blocking transaction finishes.\n await session.unsafe(`SET lock_timeout = ${lockTimeout}`);\n\n const createSlot = failover\n ? session.unsafe<ReplicationSlot[]>(\n /*sql*/ `CREATE_REPLICATION_SLOT \"${slotName}\" LOGICAL pgoutput (FAILOVER)`,\n )\n : session.unsafe<ReplicationSlot[]>(\n /*sql*/ `CREATE_REPLICATION_SLOT \"${slotName}\" LOGICAL pgoutput`,\n );\n const raced = await orTimeout(createSlot, CREATE_REPLICATION_SLOT_TIMEOUT_MS);\n if (raced === 'timed-out') {\n // Create slot can block indefinitely waiting for old transactions. End\n // this connection in the background and fail fast so the process restarts.\n void session\n .end()\n .catch(e =>\n lc.warn?.(`Error closing timed out replication slot session`, e),\n );\n throw new Error(\n `Timed out after ${CREATE_REPLICATION_SLOT_TIMEOUT_MS} ms creating replication slot ${slotName}. ` +\n `Crashing to force a clean restart.`,\n );\n }\n const [slot] = raced;\n lc.info?.(`Created replication slot ${slotName}`, slot);\n return slot;\n}\n\n/**\n * Replica and slot creation involves two sessions for proper coordination\n * with other replica management logic:\n *\n * * A normal transaction is started and acquires an advisory lock for\n * replica slot management. This is the same lock that cleanup logic\n * acquires before cleaning up replication slots.\n * * With the lock held, a new replication slot is created in a\n * replication session. The API of CREATE_REPLICATION_SLOT is such\n * that it cannot be done in a transaction, and cannot be followed by\n * any writes, or else its snapshot (which is needed for initial sync)\n * would be invalidated.\n * * Once the slot is created, the slot and replica information are recorded\n * in the `replicas` table before releasing the lock.\n *\n * This locking ensures that:\n * 1. multiple replication managers attempting to create a replication slot\n * will not use the same name for the replication slot (which is selected\n * from a pool of reused names).\n * 2. Running replication managers (which use an earlier replica of a lower\n * rank) will not delete the new slot during their cleanup logic, since\n * the slot will belong to a replica of a higher rank.\n */\nexport async function createReplicaAndSlot(\n lc: LogContext,\n sql: PostgresDB,\n replicationSession: postgres.Sql,\n shard: ShardID,\n replicaID: string,\n failover: boolean,\n): Promise<ReplicationSlot> {\n const lockName = replicationSlotManagementLock(shard);\n const slotPoolPrefix = replicationSlotPrefix(shard);\n for (let first = true; ; first = false) {\n await dropUnclaimedSlots(lc, sql, shard);\n try {\n return await runTx(sql, async tx => {\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n // Pick an available slotName from the slotPoolPrefix pool.\n let slotName: string;\n const names = await tx<{name: string}[]> /*sql*/ `\n SELECT slot_name as name FROM pg_replication_slots\n WHERE slot_name LIKE ${slotPoolPrefix + '%'};\n `.values();\n const inUse = new Set(names.flat());\n for (let next = 0; ; next++) {\n const candidateName = `${slotPoolPrefix}${slotPoolSuffix(next)}`;\n if (!inUse.has(candidateName)) {\n slotName = candidateName;\n break;\n }\n }\n\n const slot = await createReplicationSlot(lc, replicationSession, {\n slotName,\n failover,\n });\n\n await createReplica(\n tx,\n shard,\n replicaID,\n slot.slot_name,\n toStateVersionString(slot.consistent_point),\n );\n\n return slot;\n });\n } catch (e) {\n if (first && isPostgresError(e, PG_INSUFFICIENT_PRIVILEGE)) {\n // Some Postgres variants (e.g. Google Cloud SQL) require that\n // the user have the REPLICATION role in order to create a slot.\n // Note that this must be done by the upstreamDB connection, and\n // does not work in the replicationSession itself.\n await sql`ALTER ROLE current_user WITH REPLICATION`;\n lc.info?.(`Added the REPLICATION role to database user`);\n continue;\n }\n // Note: This is currently manually tested since max_replication_slots\n // is a PG startup parameter that other tests depend on.\n // TODO: Figure out a way to unit test this (with the full PG setup).\n if (first && isPostgresError(e, PG_CONFIGURATION_LIMIT_EXCEEDED)) {\n lc.warn?.(\n `Reached max replication slots. Attempting to clean up unused slots`,\n e,\n );\n // Drop any inactive replicas from failed initial syncs (e.g. inactive slots).\n const replicasTable = `${upstreamSchema(shard)}.replicas`;\n await sql`\n DELETE FROM ${sql(replicasTable)} USING pg_replication_slots slots\n WHERE replicas.slot = slots.slot_name AND NOT slots.active`;\n continue; // then let dropUnclaimedSlots() perform its cleanup\n }\n throw e;\n }\n }\n}\n\n/**\n * Deletes \"old\" replicas (i.e. those with a lower rank than the current)\n * and attempts to drop replication slots that are not associated with any\n * replica.\n *\n * If a slot could not be dropped because there is still an active subscriber,\n * it will be reflected in the `draining` count that is returned. When there\n * are draining slots, the method should be retried until all orphaned slots\n * have been dropped.\n */\nexport async function dropOldReplicasAndSlots(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n beforeRank: bigint,\n): Promise<{dropped: number; active: number; draining: number}> {\n const replicasTable = `${upstreamSchema(shard)}.replicas`;\n const oldReplicas = await sql`\n SELECT id, rank::float8, slot, version, \"initialSyncContext\", \"subscriberContext\"\n FROM ${sql(replicasTable)} WHERE rank < ${beforeRank};\n `;\n if (oldReplicas.length) {\n lc.info?.(`Deleting ${oldReplicas.length} old replica(s)`, {oldReplicas});\n await sql`DELETE FROM ${sql(replicasTable)} WHERE rank < ${beforeRank}`;\n }\n\n return dropUnclaimedSlots(lc, sql, shard);\n}\n\nfunction dropUnclaimedSlots(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n): Promise<{dropped: number; active: number; draining: number}> {\n // The slot / replica cleanup happens within a transaction while holding\n // the replication slot management lock for this shard, to ensure that no\n // slot that belongs to a newer replica is dropped.\n const lockName = replicationSlotManagementLock(shard);\n const slotExpression = replicationSlotExpression(shard);\n const replicasTable = `${upstreamSchema(shard)}.replicas`;\n\n return runTx(sql, async tx => {\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n const dropped = await tx /*sql*/ `\n SELECT slot_name as slot, pg_drop_replication_slot(slot_name) \n FROM pg_replication_slots\n LEFT JOIN ${tx(replicasTable)} replica on slot_name = slot\n WHERE slot_name LIKE ${slotExpression} \n AND NOT active\n AND replica.id IS NULL;\n `;\n if (dropped.length) {\n lc.info?.(`dropped inactive replication slots`, {dropped});\n }\n\n const remaining = await tx<\n {slot: string; pid: number | null; id: string | null}[]\n > /*sql*/ `\n SELECT slot_name as slot, active_pid as pid, replica.id as id\n FROM pg_replication_slots\n LEFT JOIN ${tx(replicasTable)} replica on slot_name = slot\n WHERE slot_name LIKE ${slotExpression};\n `;\n if (remaining.length) {\n lc.info?.(`remaining replication slots`, {remaining});\n }\n\n let active = 0;\n let draining = 0;\n for (const {id} of remaining) {\n if (id === null) {\n draining++;\n } else {\n active++;\n }\n }\n\n return {\n dropped: dropped.length,\n active,\n draining,\n };\n });\n}\n\nconst ALPHABET = 'abcdefghijklmnopqrstuvwxyz';\n\n// Alphabetic notation is used as the slot pool suffix to distinguish\n// it from the (numeric) shard num that's also encoded in the slot name.\nexport function slotPoolSuffix(n: number) {\n n++; // Adjust for 0-based indexing\n\n let suffix = '';\n while (n > 0) {\n n--;\n suffix = ALPHABET[n % 26] + suffix;\n n = Math.floor(n / 26);\n }\n return suffix;\n}\n\nfunction replicationSlotManagementLock(shard: ShardID) {\n return `replication-slot-management:${shard.appID}_${shard.shardNum}`;\n}\n"],"mappings":"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"}
|