@blamejs/core 0.7.1 → 0.7.18
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/CHANGELOG.md +423 -389
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -282
- package/lib/api-key.js +660 -672
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt.js +311 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +594 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -266
- package/lib/cache.js +1206 -1211
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/codepoint-class.js +196 -0
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -286
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -421
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/file-upload.js +1136 -0
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -202
- package/lib/framework-schema.js +717 -717
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -362
- package/lib/mail.js +981 -962
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -573
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/body-parser.js +1170 -1091
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -269
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +79 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -614
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -91
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -451
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -479
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -855
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -177
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -752
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -489
- package/lib/request-helpers.js +340 -336
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -630
- package/lib/session.js +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +879 -114
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -270
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -598
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
|
@@ -1,317 +1,317 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Default cluster-coordination provider — DB-row-based leader election.
|
|
4
|
-
*
|
|
5
|
-
* Uses an externalDb backend (already configured via b.externalDb.init)
|
|
6
|
-
* as the coordination point. A single row in `_blamejs_leader` holds
|
|
7
|
-
* the current lease; acquireLease is `INSERT ... ON CONFLICT ... DO
|
|
8
|
-
* UPDATE WHERE expiresAt < now() RETURNING ...`, which is atomic in
|
|
9
|
-
* Postgres and SQLite.
|
|
10
|
-
*
|
|
11
|
-
* Fencing tokens: every successful acquire bumps a monotonic integer.
|
|
12
|
-
* Leader-only DB writes include the current token; the audit-tip row's
|
|
13
|
-
* CHECK constraint rejects any incoming token below the stored one,
|
|
14
|
-
* which fences out a partitioned old leader even if its application-
|
|
15
|
-
* layer `_requireLeader()` gate somehow allowed the call through.
|
|
16
|
-
*
|
|
17
|
-
* Dialects: Postgres / SQLite use `INSERT ... ON CONFLICT ... DO
|
|
18
|
-
* UPDATE WHERE ... RETURNING` (atomic acquire-or-steal in one
|
|
19
|
-
* statement). MySQL takes a different shape because its
|
|
20
|
-
* `ON DUPLICATE KEY UPDATE` doesn't support a WHERE clause: each
|
|
21
|
-
* column update is gated by `IF(expiresAt < <nowMs>, VALUES(col), col)`
|
|
22
|
-
* so a non-expired lease is preserved untouched, and the followup
|
|
23
|
-
* SELECT reveals who currently holds the row. Both shapes are
|
|
24
|
-
* row-level atomic at the database — no client-side locking required.
|
|
25
|
-
*
|
|
26
|
-
* Public API:
|
|
27
|
-
* create({ externalDbBackend, dialect? }) → provider instance
|
|
28
|
-
*
|
|
29
|
-
* Provider instance:
|
|
30
|
-
* ensureSchema() async; idempotent CREATE
|
|
31
|
-
* TABLE + ALTER for
|
|
32
|
-
* endpoint migration
|
|
33
|
-
* acquireLease(nodeId, leaseTtlMs, opts?) async; → Lease | null.
|
|
34
|
-
* opts.endpoint persists
|
|
35
|
-
* in the row for discovery.
|
|
36
|
-
* renewLease(lease, opts?) async; → Lease (throws on
|
|
37
|
-
* takeover). opts.endpoint
|
|
38
|
-
* refreshes the row.
|
|
39
|
-
* releaseLease(lease) async; → void
|
|
40
|
-
* currentLeader() async; →
|
|
41
|
-
* { nodeId, leaseExpiresAt,
|
|
42
|
-
* fencingToken,
|
|
43
|
-
* endpoint } | null
|
|
44
|
-
*
|
|
45
|
-
* Lease object shape:
|
|
46
|
-
* { nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint }
|
|
47
|
-
*/
|
|
48
|
-
var C = require("./constants");
|
|
49
|
-
var { generateToken } = require("./crypto");
|
|
50
|
-
var externalDb = require("./external-db");
|
|
51
|
-
var { ClusterProviderError } = require("./framework-error");
|
|
52
|
-
|
|
53
|
-
var _err = ClusterProviderError.factory;
|
|
54
|
-
|
|
55
|
-
function create(config) {
|
|
56
|
-
if (!config || !config.externalDbBackend) {
|
|
57
|
-
throw _err("INVALID_CONFIG",
|
|
58
|
-
"cluster-provider-db requires { externalDbBackend: <name> }", true);
|
|
59
|
-
}
|
|
60
|
-
var backendName = config.externalDbBackend;
|
|
61
|
-
var dialect = (config.dialect || "postgres").toLowerCase();
|
|
62
|
-
if (dialect !== "postgres" && dialect !== "sqlite" && dialect !== "mysql") {
|
|
63
|
-
throw _err("UNSUPPORTED_DIALECT",
|
|
64
|
-
"cluster-provider-db dialect must be 'postgres', 'sqlite', or 'mysql' (got: " + dialect + ")",
|
|
65
|
-
true);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Postgres + SQLite use $1/$2 placeholders; MySQL uses ?. Keeping a
|
|
69
|
-
// single helper means the SQL builder doesn't have to care.
|
|
70
|
-
function _placeholder(n) {
|
|
71
|
-
return dialect === "mysql" ? "?" : "$" + n;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function _q(sql, params) {
|
|
75
|
-
return externalDb.query(sql, params || [], { backend: backendName });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function ensureSchema() {
|
|
79
|
-
// Postgres + MySQL: BIGINT for ms-precision timestamps. SQLite:
|
|
80
|
-
// INTEGER (which is wide enough to hold a 64-bit value).
|
|
81
|
-
var intType = dialect === "sqlite" ? "INTEGER" : "BIGINT";
|
|
82
|
-
// MySQL needs explicit lengths on TEXT columns when used as a
|
|
83
|
-
// PRIMARY KEY; VARCHAR(64) covers our scope/nodeId/leaseId values
|
|
84
|
-
// with room to spare. Postgres and SQLite happily PRIMARY-KEY a
|
|
85
|
-
// plain TEXT column.
|
|
86
|
-
var pkText = dialect === "mysql" ? "VARCHAR(64)" : "TEXT";
|
|
87
|
-
var bodyText = dialect === "mysql" ? "VARCHAR(255)" : "TEXT";
|
|
88
|
-
// MySQL doesn't enforce CHECK constraints in some MariaDB / older
|
|
89
|
-
// MySQL versions; the constant-scope invariant is a documentation
|
|
90
|
-
// belt-and-braces — application code only ever writes 'leader' /
|
|
91
|
-
// 'state' so the check is informational. Skip on MySQL to avoid
|
|
92
|
-
// CREATE TABLE failures on installations where CHECK is parsed
|
|
93
|
-
// but then dropped silently (which would cause version drift).
|
|
94
|
-
var leaderCheck = dialect === "mysql" ? "" : ", CHECK (scope = 'leader')";
|
|
95
|
-
var stateCheck = dialect === "mysql" ? "" : ", CHECK (scope = 'state')";
|
|
96
|
-
|
|
97
|
-
await _q(
|
|
98
|
-
"CREATE TABLE IF NOT EXISTS _blamejs_leader (" +
|
|
99
|
-
" scope " + pkText + " PRIMARY KEY," +
|
|
100
|
-
" nodeId " + bodyText + " NOT NULL," +
|
|
101
|
-
" leaseId " + bodyText + " NOT NULL," +
|
|
102
|
-
" acquiredAt " + intType + " NOT NULL," +
|
|
103
|
-
" expiresAt " + intType + " NOT NULL," +
|
|
104
|
-
" fencingToken " + intType + " NOT NULL," +
|
|
105
|
-
" endpoint " + bodyText + leaderCheck +
|
|
106
|
-
")"
|
|
107
|
-
);
|
|
108
|
-
// Migration for installs that pre-date the endpoint column. Both
|
|
109
|
-
// Postgres (≥9.6) and SQLite (≥3.35, March 2021) support ADD COLUMN
|
|
110
|
-
// IF NOT EXISTS; MySQL 8.0.29+ does as well. We go through try/catch
|
|
111
|
-
// to keep the path version-agnostic — the only "expected" failure
|
|
112
|
-
// here is "column already exists," which we swallow.
|
|
113
|
-
try {
|
|
114
|
-
await _q("ALTER TABLE _blamejs_leader ADD COLUMN endpoint " + bodyText);
|
|
115
|
-
} catch (_e) { /* column already exists — fine */ }
|
|
116
|
-
|
|
117
|
-
await _q(
|
|
118
|
-
"CREATE TABLE IF NOT EXISTS _blamejs_cluster_state (" +
|
|
119
|
-
" scope " + pkText + " PRIMARY KEY," +
|
|
120
|
-
" vaultKeyFp " + bodyText + " NOT NULL," +
|
|
121
|
-
" recordedAt " + intType + " NOT NULL," +
|
|
122
|
-
" recordedByNode " + bodyText + " NOT NULL" + stateCheck +
|
|
123
|
-
")"
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function acquireLease(nodeId, leaseTtlMs, opts) {
|
|
128
|
-
if (!nodeId) throw _err("INVALID_NODE_ID", "nodeId required", true);
|
|
129
|
-
if (typeof leaseTtlMs !== "number" || leaseTtlMs <= 0) {
|
|
130
|
-
throw _err("INVALID_TTL", "leaseTtlMs must be a positive number", true);
|
|
131
|
-
}
|
|
132
|
-
var endpoint = (opts && opts.endpoint) || null;
|
|
133
|
-
var leaseId = generateToken(C.BYTES.bytes(16));
|
|
134
|
-
var nowMs = Date.now();
|
|
135
|
-
var expiresAt = nowMs + leaseTtlMs;
|
|
136
|
-
|
|
137
|
-
var row;
|
|
138
|
-
if (dialect === "mysql") {
|
|
139
|
-
// MySQL has no `ON CONFLICT ... DO UPDATE WHERE` and no
|
|
140
|
-
// `RETURNING`. Atomicity comes from `INSERT ... ON DUPLICATE
|
|
141
|
-
// KEY UPDATE` evaluated as one statement; the WHERE-clause
|
|
142
|
-
// semantics are implemented per-column with `IF(expiresAt <
|
|
143
|
-
// nowMs, VALUES(col), col)` so a still-valid lease is preserved
|
|
144
|
-
// and an expired one is overwritten. The follow-up SELECT
|
|
145
|
-
// reveals who currently holds the row — same as Postgres'
|
|
146
|
-
// RETURNING but as a separate statement.
|
|
147
|
-
var insertSql =
|
|
148
|
-
"INSERT INTO _blamejs_leader " +
|
|
149
|
-
" (scope, nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint) " +
|
|
150
|
-
"VALUES " +
|
|
151
|
-
" ('leader', ?, ?, ?, ?, 1, ?) " +
|
|
152
|
-
"ON DUPLICATE KEY UPDATE " +
|
|
153
|
-
" nodeId = IF(expiresAt < ?, VALUES(nodeId), nodeId)," +
|
|
154
|
-
" leaseId = IF(expiresAt < ?, VALUES(leaseId), leaseId)," +
|
|
155
|
-
" acquiredAt = IF(expiresAt < ?, VALUES(acquiredAt), acquiredAt)," +
|
|
156
|
-
" fencingToken = IF(expiresAt < ?, fencingToken + 1, fencingToken)," +
|
|
157
|
-
" endpoint = IF(expiresAt < ?, VALUES(endpoint), endpoint)," +
|
|
158
|
-
// expiresAt MUST be the last assignment — IF() evaluates each
|
|
159
|
-
// column against the row state BEFORE that column's update is
|
|
160
|
-
// applied, so checking expiresAt for the other columns first
|
|
161
|
-
// and overwriting it last keeps the predicate consistent.
|
|
162
|
-
" expiresAt = IF(expiresAt < ?, VALUES(expiresAt), expiresAt)";
|
|
163
|
-
await _q(insertSql, [
|
|
164
|
-
nodeId, leaseId, nowMs, expiresAt, endpoint,
|
|
165
|
-
nowMs, nowMs, nowMs, nowMs, nowMs, nowMs,
|
|
166
|
-
]);
|
|
167
|
-
var sel = await _q(
|
|
168
|
-
"SELECT nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint " +
|
|
169
|
-
"FROM _blamejs_leader WHERE scope = 'leader'"
|
|
170
|
-
);
|
|
171
|
-
if (!sel.rows || sel.rows.length === 0) return null;
|
|
172
|
-
row = sel.rows[0];
|
|
173
|
-
} else {
|
|
174
|
-
// Postgres / SQLite — single-statement RETURNING.
|
|
175
|
-
var sql =
|
|
176
|
-
"INSERT INTO _blamejs_leader " +
|
|
177
|
-
" (scope, nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint) " +
|
|
178
|
-
"VALUES " +
|
|
179
|
-
" ('leader', " + _placeholder(1) + ", " + _placeholder(2) + ", " +
|
|
180
|
-
" " + _placeholder(3) + ", " + _placeholder(4) + ", 1, " + _placeholder(5) + ") " +
|
|
181
|
-
"ON CONFLICT (scope) DO UPDATE SET " +
|
|
182
|
-
" nodeId = EXCLUDED.nodeId," +
|
|
183
|
-
" leaseId = EXCLUDED.leaseId," +
|
|
184
|
-
" acquiredAt = EXCLUDED.acquiredAt," +
|
|
185
|
-
" expiresAt = EXCLUDED.expiresAt," +
|
|
186
|
-
" fencingToken = _blamejs_leader.fencingToken + 1," +
|
|
187
|
-
" endpoint = EXCLUDED.endpoint " +
|
|
188
|
-
"WHERE _blamejs_leader.expiresAt < " + _placeholder(6) + " " +
|
|
189
|
-
"RETURNING nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint";
|
|
190
|
-
var result = await _q(sql, [nodeId, leaseId, nowMs, expiresAt, endpoint, nowMs]);
|
|
191
|
-
if (!result.rows || result.rows.length === 0) return null;
|
|
192
|
-
row = result.rows[0];
|
|
193
|
-
}
|
|
194
|
-
if (row.nodeId !== nodeId || row.leaseId !== leaseId) {
|
|
195
|
-
// Another node won the race (the row reflects their values, not ours).
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
return {
|
|
199
|
-
nodeId: row.nodeId,
|
|
200
|
-
leaseId: row.leaseId,
|
|
201
|
-
acquiredAt: Number(row.acquiredAt),
|
|
202
|
-
expiresAt: Number(row.expiresAt),
|
|
203
|
-
fencingToken: Number(row.fencingToken),
|
|
204
|
-
endpoint: row.endpoint || null,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async function renewLease(lease, opts) {
|
|
209
|
-
if (!lease || !lease.leaseId) throw _err("INVALID_LEASE", "lease required", true);
|
|
210
|
-
var nowMs = Date.now();
|
|
211
|
-
var newExpiresAt = nowMs + (lease.expiresAt - lease.acquiredAt);
|
|
212
|
-
// opts.endpoint, when provided, refreshes the stored endpoint so
|
|
213
|
-
// operators who hot-update their config see the discovery row catch
|
|
214
|
-
// up. Default = preserve whatever was stored at acquire time.
|
|
215
|
-
var endpoint = (opts && opts.endpoint !== undefined) ? opts.endpoint : lease.endpoint || null;
|
|
216
|
-
|
|
217
|
-
// Match on (nodeId, leaseId) so a takeover is detectable: if our
|
|
218
|
-
// leaseId is no longer in the row, the SELECT-after-UPDATE
|
|
219
|
-
// returns either no row OR a row with a different leaseId, and
|
|
220
|
-
// we throw LEASE_LOST. Don't bump fencingToken on renewal — only
|
|
221
|
-
// on a fresh acquire.
|
|
222
|
-
var row;
|
|
223
|
-
if (dialect === "mysql") {
|
|
224
|
-
var rv = await _q(
|
|
225
|
-
"UPDATE _blamejs_leader SET " +
|
|
226
|
-
" expiresAt = ?, endpoint = ? " +
|
|
227
|
-
"WHERE scope = 'leader' AND nodeId = ? AND leaseId = ?",
|
|
228
|
-
[newExpiresAt, endpoint, lease.nodeId, lease.leaseId]
|
|
229
|
-
);
|
|
230
|
-
var affected = rv && (rv.affectedRows || rv.rowCount || 0);
|
|
231
|
-
if (!affected) {
|
|
232
|
-
throw _err("LEASE_LOST",
|
|
233
|
-
"lease for node '" + lease.nodeId + "' was taken over (renewal rejected)",
|
|
234
|
-
false);
|
|
235
|
-
}
|
|
236
|
-
var sel = await _q(
|
|
237
|
-
"SELECT nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint " +
|
|
238
|
-
"FROM _blamejs_leader WHERE scope = 'leader'"
|
|
239
|
-
);
|
|
240
|
-
if (!sel.rows || sel.rows.length === 0 ||
|
|
241
|
-
sel.rows[0].nodeId !== lease.nodeId ||
|
|
242
|
-
sel.rows[0].leaseId !== lease.leaseId) {
|
|
243
|
-
throw _err("LEASE_LOST",
|
|
244
|
-
"lease for node '" + lease.nodeId + "' was taken over after renewal",
|
|
245
|
-
false);
|
|
246
|
-
}
|
|
247
|
-
row = sel.rows[0];
|
|
248
|
-
} else {
|
|
249
|
-
var sql =
|
|
250
|
-
"UPDATE _blamejs_leader SET " +
|
|
251
|
-
" expiresAt = " + _placeholder(1) + "," +
|
|
252
|
-
" endpoint = " + _placeholder(2) + " " +
|
|
253
|
-
"WHERE scope = 'leader' AND nodeId = " + _placeholder(3) +
|
|
254
|
-
" AND leaseId = " + _placeholder(4) + " " +
|
|
255
|
-
"RETURNING nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint";
|
|
256
|
-
var result = await _q(sql, [newExpiresAt, endpoint, lease.nodeId, lease.leaseId]);
|
|
257
|
-
if (!result.rows || result.rows.length === 0) {
|
|
258
|
-
throw _err("LEASE_LOST",
|
|
259
|
-
"lease for node '" + lease.nodeId + "' was taken over (renewal rejected)",
|
|
260
|
-
false);
|
|
261
|
-
}
|
|
262
|
-
row = result.rows[0];
|
|
263
|
-
}
|
|
264
|
-
return {
|
|
265
|
-
nodeId: row.nodeId,
|
|
266
|
-
leaseId: row.leaseId,
|
|
267
|
-
acquiredAt: Number(row.acquiredAt),
|
|
268
|
-
expiresAt: Number(row.expiresAt),
|
|
269
|
-
fencingToken: Number(row.fencingToken),
|
|
270
|
-
endpoint: row.endpoint || null,
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async function releaseLease(lease) {
|
|
275
|
-
if (!lease || !lease.leaseId) return;
|
|
276
|
-
// Clear our row so the next acquire wins immediately. Match on
|
|
277
|
-
// leaseId so a takeover-then-release race doesn't clear someone
|
|
278
|
-
// else's lease.
|
|
279
|
-
var sql =
|
|
280
|
-
"UPDATE _blamejs_leader SET " +
|
|
281
|
-
" expiresAt = 0 " +
|
|
282
|
-
"WHERE scope = 'leader' AND nodeId = " + _placeholder(1) +
|
|
283
|
-
" AND leaseId = " + _placeholder(2);
|
|
284
|
-
await _q(sql, [lease.nodeId, lease.leaseId]);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async function currentLeader() {
|
|
288
|
-
var result = await _q(
|
|
289
|
-
"SELECT nodeId, expiresAt, fencingToken, endpoint FROM _blamejs_leader " +
|
|
290
|
-
"WHERE scope = 'leader'"
|
|
291
|
-
);
|
|
292
|
-
if (!result.rows || result.rows.length === 0) return null;
|
|
293
|
-
var row = result.rows[0];
|
|
294
|
-
if (Number(row.expiresAt) < Date.now()) return null;
|
|
295
|
-
return {
|
|
296
|
-
nodeId: row.nodeId,
|
|
297
|
-
leaseExpiresAt: Number(row.expiresAt),
|
|
298
|
-
fencingToken: Number(row.fencingToken),
|
|
299
|
-
endpoint: row.endpoint || null,
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return {
|
|
304
|
-
kind: "db",
|
|
305
|
-
backendName: backendName,
|
|
306
|
-
dialect: dialect,
|
|
307
|
-
ensureSchema: ensureSchema,
|
|
308
|
-
acquireLease: acquireLease,
|
|
309
|
-
renewLease: renewLease,
|
|
310
|
-
releaseLease: releaseLease,
|
|
311
|
-
currentLeader: currentLeader,
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
module.exports = {
|
|
316
|
-
create: create,
|
|
317
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Default cluster-coordination provider — DB-row-based leader election.
|
|
4
|
+
*
|
|
5
|
+
* Uses an externalDb backend (already configured via b.externalDb.init)
|
|
6
|
+
* as the coordination point. A single row in `_blamejs_leader` holds
|
|
7
|
+
* the current lease; acquireLease is `INSERT ... ON CONFLICT ... DO
|
|
8
|
+
* UPDATE WHERE expiresAt < now() RETURNING ...`, which is atomic in
|
|
9
|
+
* Postgres and SQLite.
|
|
10
|
+
*
|
|
11
|
+
* Fencing tokens: every successful acquire bumps a monotonic integer.
|
|
12
|
+
* Leader-only DB writes include the current token; the audit-tip row's
|
|
13
|
+
* CHECK constraint rejects any incoming token below the stored one,
|
|
14
|
+
* which fences out a partitioned old leader even if its application-
|
|
15
|
+
* layer `_requireLeader()` gate somehow allowed the call through.
|
|
16
|
+
*
|
|
17
|
+
* Dialects: Postgres / SQLite use `INSERT ... ON CONFLICT ... DO
|
|
18
|
+
* UPDATE WHERE ... RETURNING` (atomic acquire-or-steal in one
|
|
19
|
+
* statement). MySQL takes a different shape because its
|
|
20
|
+
* `ON DUPLICATE KEY UPDATE` doesn't support a WHERE clause: each
|
|
21
|
+
* column update is gated by `IF(expiresAt < <nowMs>, VALUES(col), col)`
|
|
22
|
+
* so a non-expired lease is preserved untouched, and the followup
|
|
23
|
+
* SELECT reveals who currently holds the row. Both shapes are
|
|
24
|
+
* row-level atomic at the database — no client-side locking required.
|
|
25
|
+
*
|
|
26
|
+
* Public API:
|
|
27
|
+
* create({ externalDbBackend, dialect? }) → provider instance
|
|
28
|
+
*
|
|
29
|
+
* Provider instance:
|
|
30
|
+
* ensureSchema() async; idempotent CREATE
|
|
31
|
+
* TABLE + ALTER for
|
|
32
|
+
* endpoint migration
|
|
33
|
+
* acquireLease(nodeId, leaseTtlMs, opts?) async; → Lease | null.
|
|
34
|
+
* opts.endpoint persists
|
|
35
|
+
* in the row for discovery.
|
|
36
|
+
* renewLease(lease, opts?) async; → Lease (throws on
|
|
37
|
+
* takeover). opts.endpoint
|
|
38
|
+
* refreshes the row.
|
|
39
|
+
* releaseLease(lease) async; → void
|
|
40
|
+
* currentLeader() async; →
|
|
41
|
+
* { nodeId, leaseExpiresAt,
|
|
42
|
+
* fencingToken,
|
|
43
|
+
* endpoint } | null
|
|
44
|
+
*
|
|
45
|
+
* Lease object shape:
|
|
46
|
+
* { nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint }
|
|
47
|
+
*/
|
|
48
|
+
var C = require("./constants");
|
|
49
|
+
var { generateToken } = require("./crypto");
|
|
50
|
+
var externalDb = require("./external-db");
|
|
51
|
+
var { ClusterProviderError } = require("./framework-error");
|
|
52
|
+
|
|
53
|
+
var _err = ClusterProviderError.factory;
|
|
54
|
+
|
|
55
|
+
function create(config) {
|
|
56
|
+
if (!config || !config.externalDbBackend) {
|
|
57
|
+
throw _err("INVALID_CONFIG",
|
|
58
|
+
"cluster-provider-db requires { externalDbBackend: <name> }", true);
|
|
59
|
+
}
|
|
60
|
+
var backendName = config.externalDbBackend;
|
|
61
|
+
var dialect = (config.dialect || "postgres").toLowerCase();
|
|
62
|
+
if (dialect !== "postgres" && dialect !== "sqlite" && dialect !== "mysql") {
|
|
63
|
+
throw _err("UNSUPPORTED_DIALECT",
|
|
64
|
+
"cluster-provider-db dialect must be 'postgres', 'sqlite', or 'mysql' (got: " + dialect + ")",
|
|
65
|
+
true);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Postgres + SQLite use $1/$2 placeholders; MySQL uses ?. Keeping a
|
|
69
|
+
// single helper means the SQL builder doesn't have to care.
|
|
70
|
+
function _placeholder(n) {
|
|
71
|
+
return dialect === "mysql" ? "?" : "$" + n;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _q(sql, params) {
|
|
75
|
+
return externalDb.query(sql, params || [], { backend: backendName });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function ensureSchema() {
|
|
79
|
+
// Postgres + MySQL: BIGINT for ms-precision timestamps. SQLite:
|
|
80
|
+
// INTEGER (which is wide enough to hold a 64-bit value).
|
|
81
|
+
var intType = dialect === "sqlite" ? "INTEGER" : "BIGINT";
|
|
82
|
+
// MySQL needs explicit lengths on TEXT columns when used as a
|
|
83
|
+
// PRIMARY KEY; VARCHAR(64) covers our scope/nodeId/leaseId values
|
|
84
|
+
// with room to spare. Postgres and SQLite happily PRIMARY-KEY a
|
|
85
|
+
// plain TEXT column.
|
|
86
|
+
var pkText = dialect === "mysql" ? "VARCHAR(64)" : "TEXT";
|
|
87
|
+
var bodyText = dialect === "mysql" ? "VARCHAR(255)" : "TEXT";
|
|
88
|
+
// MySQL doesn't enforce CHECK constraints in some MariaDB / older
|
|
89
|
+
// MySQL versions; the constant-scope invariant is a documentation
|
|
90
|
+
// belt-and-braces — application code only ever writes 'leader' /
|
|
91
|
+
// 'state' so the check is informational. Skip on MySQL to avoid
|
|
92
|
+
// CREATE TABLE failures on installations where CHECK is parsed
|
|
93
|
+
// but then dropped silently (which would cause version drift).
|
|
94
|
+
var leaderCheck = dialect === "mysql" ? "" : ", CHECK (scope = 'leader')";
|
|
95
|
+
var stateCheck = dialect === "mysql" ? "" : ", CHECK (scope = 'state')";
|
|
96
|
+
|
|
97
|
+
await _q(
|
|
98
|
+
"CREATE TABLE IF NOT EXISTS _blamejs_leader (" +
|
|
99
|
+
" scope " + pkText + " PRIMARY KEY," +
|
|
100
|
+
" nodeId " + bodyText + " NOT NULL," +
|
|
101
|
+
" leaseId " + bodyText + " NOT NULL," +
|
|
102
|
+
" acquiredAt " + intType + " NOT NULL," +
|
|
103
|
+
" expiresAt " + intType + " NOT NULL," +
|
|
104
|
+
" fencingToken " + intType + " NOT NULL," +
|
|
105
|
+
" endpoint " + bodyText + leaderCheck +
|
|
106
|
+
")"
|
|
107
|
+
);
|
|
108
|
+
// Migration for installs that pre-date the endpoint column. Both
|
|
109
|
+
// Postgres (≥9.6) and SQLite (≥3.35, March 2021) support ADD COLUMN
|
|
110
|
+
// IF NOT EXISTS; MySQL 8.0.29+ does as well. We go through try/catch
|
|
111
|
+
// to keep the path version-agnostic — the only "expected" failure
|
|
112
|
+
// here is "column already exists," which we swallow.
|
|
113
|
+
try {
|
|
114
|
+
await _q("ALTER TABLE _blamejs_leader ADD COLUMN endpoint " + bodyText);
|
|
115
|
+
} catch (_e) { /* column already exists — fine */ }
|
|
116
|
+
|
|
117
|
+
await _q(
|
|
118
|
+
"CREATE TABLE IF NOT EXISTS _blamejs_cluster_state (" +
|
|
119
|
+
" scope " + pkText + " PRIMARY KEY," +
|
|
120
|
+
" vaultKeyFp " + bodyText + " NOT NULL," +
|
|
121
|
+
" recordedAt " + intType + " NOT NULL," +
|
|
122
|
+
" recordedByNode " + bodyText + " NOT NULL" + stateCheck +
|
|
123
|
+
")"
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function acquireLease(nodeId, leaseTtlMs, opts) {
|
|
128
|
+
if (!nodeId) throw _err("INVALID_NODE_ID", "nodeId required", true);
|
|
129
|
+
if (typeof leaseTtlMs !== "number" || leaseTtlMs <= 0) {
|
|
130
|
+
throw _err("INVALID_TTL", "leaseTtlMs must be a positive number", true);
|
|
131
|
+
}
|
|
132
|
+
var endpoint = (opts && opts.endpoint) || null;
|
|
133
|
+
var leaseId = generateToken(C.BYTES.bytes(16));
|
|
134
|
+
var nowMs = Date.now();
|
|
135
|
+
var expiresAt = nowMs + leaseTtlMs;
|
|
136
|
+
|
|
137
|
+
var row;
|
|
138
|
+
if (dialect === "mysql") {
|
|
139
|
+
// MySQL has no `ON CONFLICT ... DO UPDATE WHERE` and no
|
|
140
|
+
// `RETURNING`. Atomicity comes from `INSERT ... ON DUPLICATE
|
|
141
|
+
// KEY UPDATE` evaluated as one statement; the WHERE-clause
|
|
142
|
+
// semantics are implemented per-column with `IF(expiresAt <
|
|
143
|
+
// nowMs, VALUES(col), col)` so a still-valid lease is preserved
|
|
144
|
+
// and an expired one is overwritten. The follow-up SELECT
|
|
145
|
+
// reveals who currently holds the row — same as Postgres'
|
|
146
|
+
// RETURNING but as a separate statement.
|
|
147
|
+
var insertSql =
|
|
148
|
+
"INSERT INTO _blamejs_leader " +
|
|
149
|
+
" (scope, nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint) " +
|
|
150
|
+
"VALUES " +
|
|
151
|
+
" ('leader', ?, ?, ?, ?, 1, ?) " +
|
|
152
|
+
"ON DUPLICATE KEY UPDATE " +
|
|
153
|
+
" nodeId = IF(expiresAt < ?, VALUES(nodeId), nodeId)," +
|
|
154
|
+
" leaseId = IF(expiresAt < ?, VALUES(leaseId), leaseId)," +
|
|
155
|
+
" acquiredAt = IF(expiresAt < ?, VALUES(acquiredAt), acquiredAt)," +
|
|
156
|
+
" fencingToken = IF(expiresAt < ?, fencingToken + 1, fencingToken)," +
|
|
157
|
+
" endpoint = IF(expiresAt < ?, VALUES(endpoint), endpoint)," +
|
|
158
|
+
// expiresAt MUST be the last assignment — IF() evaluates each
|
|
159
|
+
// column against the row state BEFORE that column's update is
|
|
160
|
+
// applied, so checking expiresAt for the other columns first
|
|
161
|
+
// and overwriting it last keeps the predicate consistent.
|
|
162
|
+
" expiresAt = IF(expiresAt < ?, VALUES(expiresAt), expiresAt)";
|
|
163
|
+
await _q(insertSql, [
|
|
164
|
+
nodeId, leaseId, nowMs, expiresAt, endpoint,
|
|
165
|
+
nowMs, nowMs, nowMs, nowMs, nowMs, nowMs,
|
|
166
|
+
]);
|
|
167
|
+
var sel = await _q(
|
|
168
|
+
"SELECT nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint " +
|
|
169
|
+
"FROM _blamejs_leader WHERE scope = 'leader'"
|
|
170
|
+
);
|
|
171
|
+
if (!sel.rows || sel.rows.length === 0) return null;
|
|
172
|
+
row = sel.rows[0];
|
|
173
|
+
} else {
|
|
174
|
+
// Postgres / SQLite — single-statement RETURNING.
|
|
175
|
+
var sql =
|
|
176
|
+
"INSERT INTO _blamejs_leader " +
|
|
177
|
+
" (scope, nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint) " +
|
|
178
|
+
"VALUES " +
|
|
179
|
+
" ('leader', " + _placeholder(1) + ", " + _placeholder(2) + ", " +
|
|
180
|
+
" " + _placeholder(3) + ", " + _placeholder(4) + ", 1, " + _placeholder(5) + ") " +
|
|
181
|
+
"ON CONFLICT (scope) DO UPDATE SET " +
|
|
182
|
+
" nodeId = EXCLUDED.nodeId," +
|
|
183
|
+
" leaseId = EXCLUDED.leaseId," +
|
|
184
|
+
" acquiredAt = EXCLUDED.acquiredAt," +
|
|
185
|
+
" expiresAt = EXCLUDED.expiresAt," +
|
|
186
|
+
" fencingToken = _blamejs_leader.fencingToken + 1," +
|
|
187
|
+
" endpoint = EXCLUDED.endpoint " +
|
|
188
|
+
"WHERE _blamejs_leader.expiresAt < " + _placeholder(6) + " " +
|
|
189
|
+
"RETURNING nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint";
|
|
190
|
+
var result = await _q(sql, [nodeId, leaseId, nowMs, expiresAt, endpoint, nowMs]);
|
|
191
|
+
if (!result.rows || result.rows.length === 0) return null;
|
|
192
|
+
row = result.rows[0];
|
|
193
|
+
}
|
|
194
|
+
if (row.nodeId !== nodeId || row.leaseId !== leaseId) {
|
|
195
|
+
// Another node won the race (the row reflects their values, not ours).
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
nodeId: row.nodeId,
|
|
200
|
+
leaseId: row.leaseId,
|
|
201
|
+
acquiredAt: Number(row.acquiredAt),
|
|
202
|
+
expiresAt: Number(row.expiresAt),
|
|
203
|
+
fencingToken: Number(row.fencingToken),
|
|
204
|
+
endpoint: row.endpoint || null,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function renewLease(lease, opts) {
|
|
209
|
+
if (!lease || !lease.leaseId) throw _err("INVALID_LEASE", "lease required", true);
|
|
210
|
+
var nowMs = Date.now();
|
|
211
|
+
var newExpiresAt = nowMs + (lease.expiresAt - lease.acquiredAt);
|
|
212
|
+
// opts.endpoint, when provided, refreshes the stored endpoint so
|
|
213
|
+
// operators who hot-update their config see the discovery row catch
|
|
214
|
+
// up. Default = preserve whatever was stored at acquire time.
|
|
215
|
+
var endpoint = (opts && opts.endpoint !== undefined) ? opts.endpoint : lease.endpoint || null;
|
|
216
|
+
|
|
217
|
+
// Match on (nodeId, leaseId) so a takeover is detectable: if our
|
|
218
|
+
// leaseId is no longer in the row, the SELECT-after-UPDATE
|
|
219
|
+
// returns either no row OR a row with a different leaseId, and
|
|
220
|
+
// we throw LEASE_LOST. Don't bump fencingToken on renewal — only
|
|
221
|
+
// on a fresh acquire.
|
|
222
|
+
var row;
|
|
223
|
+
if (dialect === "mysql") {
|
|
224
|
+
var rv = await _q(
|
|
225
|
+
"UPDATE _blamejs_leader SET " +
|
|
226
|
+
" expiresAt = ?, endpoint = ? " +
|
|
227
|
+
"WHERE scope = 'leader' AND nodeId = ? AND leaseId = ?",
|
|
228
|
+
[newExpiresAt, endpoint, lease.nodeId, lease.leaseId]
|
|
229
|
+
);
|
|
230
|
+
var affected = rv && (rv.affectedRows || rv.rowCount || 0);
|
|
231
|
+
if (!affected) {
|
|
232
|
+
throw _err("LEASE_LOST",
|
|
233
|
+
"lease for node '" + lease.nodeId + "' was taken over (renewal rejected)",
|
|
234
|
+
false);
|
|
235
|
+
}
|
|
236
|
+
var sel = await _q(
|
|
237
|
+
"SELECT nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint " +
|
|
238
|
+
"FROM _blamejs_leader WHERE scope = 'leader'"
|
|
239
|
+
);
|
|
240
|
+
if (!sel.rows || sel.rows.length === 0 ||
|
|
241
|
+
sel.rows[0].nodeId !== lease.nodeId ||
|
|
242
|
+
sel.rows[0].leaseId !== lease.leaseId) {
|
|
243
|
+
throw _err("LEASE_LOST",
|
|
244
|
+
"lease for node '" + lease.nodeId + "' was taken over after renewal",
|
|
245
|
+
false);
|
|
246
|
+
}
|
|
247
|
+
row = sel.rows[0];
|
|
248
|
+
} else {
|
|
249
|
+
var sql =
|
|
250
|
+
"UPDATE _blamejs_leader SET " +
|
|
251
|
+
" expiresAt = " + _placeholder(1) + "," +
|
|
252
|
+
" endpoint = " + _placeholder(2) + " " +
|
|
253
|
+
"WHERE scope = 'leader' AND nodeId = " + _placeholder(3) +
|
|
254
|
+
" AND leaseId = " + _placeholder(4) + " " +
|
|
255
|
+
"RETURNING nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint";
|
|
256
|
+
var result = await _q(sql, [newExpiresAt, endpoint, lease.nodeId, lease.leaseId]);
|
|
257
|
+
if (!result.rows || result.rows.length === 0) {
|
|
258
|
+
throw _err("LEASE_LOST",
|
|
259
|
+
"lease for node '" + lease.nodeId + "' was taken over (renewal rejected)",
|
|
260
|
+
false);
|
|
261
|
+
}
|
|
262
|
+
row = result.rows[0];
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
nodeId: row.nodeId,
|
|
266
|
+
leaseId: row.leaseId,
|
|
267
|
+
acquiredAt: Number(row.acquiredAt),
|
|
268
|
+
expiresAt: Number(row.expiresAt),
|
|
269
|
+
fencingToken: Number(row.fencingToken),
|
|
270
|
+
endpoint: row.endpoint || null,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function releaseLease(lease) {
|
|
275
|
+
if (!lease || !lease.leaseId) return;
|
|
276
|
+
// Clear our row so the next acquire wins immediately. Match on
|
|
277
|
+
// leaseId so a takeover-then-release race doesn't clear someone
|
|
278
|
+
// else's lease.
|
|
279
|
+
var sql =
|
|
280
|
+
"UPDATE _blamejs_leader SET " +
|
|
281
|
+
" expiresAt = 0 " +
|
|
282
|
+
"WHERE scope = 'leader' AND nodeId = " + _placeholder(1) +
|
|
283
|
+
" AND leaseId = " + _placeholder(2);
|
|
284
|
+
await _q(sql, [lease.nodeId, lease.leaseId]);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function currentLeader() {
|
|
288
|
+
var result = await _q(
|
|
289
|
+
"SELECT nodeId, expiresAt, fencingToken, endpoint FROM _blamejs_leader " +
|
|
290
|
+
"WHERE scope = 'leader'"
|
|
291
|
+
);
|
|
292
|
+
if (!result.rows || result.rows.length === 0) return null;
|
|
293
|
+
var row = result.rows[0];
|
|
294
|
+
if (Number(row.expiresAt) < Date.now()) return null;
|
|
295
|
+
return {
|
|
296
|
+
nodeId: row.nodeId,
|
|
297
|
+
leaseExpiresAt: Number(row.expiresAt),
|
|
298
|
+
fencingToken: Number(row.fencingToken),
|
|
299
|
+
endpoint: row.endpoint || null,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
kind: "db",
|
|
305
|
+
backendName: backendName,
|
|
306
|
+
dialect: dialect,
|
|
307
|
+
ensureSchema: ensureSchema,
|
|
308
|
+
acquireLease: acquireLease,
|
|
309
|
+
renewLease: renewLease,
|
|
310
|
+
releaseLease: releaseLease,
|
|
311
|
+
currentLeader: currentLeader,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
module.exports = {
|
|
316
|
+
create: create,
|
|
317
|
+
};
|