@koralabs/kora-labs-common 6.18.4 → 6.19.0

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.
@@ -48,6 +48,8 @@ export declare const isUnknownOutcomeError: (error: unknown) => boolean;
48
48
  * concurrent ticks on one node cannot both treat the node identity as ownership.
49
49
  */
50
50
  export declare const acquireWithExecutor: (executor: CqlExecutor, name: string, offline: string | null | undefined, topology: CronLockTopology, token?: string, lease?: number) => Promise<AcquireResult>;
51
+ /** One fresh process's acquisition: make sure the lock table exists, then take the lock. */
52
+ export declare const acquireOnExecutor: (executor: CqlExecutor, name: string, offline: string | null | undefined, topology: CronLockTopology, lease?: number) => Promise<AcquireResult>;
51
53
  export declare const acquire: (name: string, offline?: string | null, options?: {
52
54
  leaseMs?: number;
53
55
  }) => Promise<AcquireResult>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.release = exports.releaseWithExecutor = exports.acquire = exports.acquireWithExecutor = exports.isUnknownOutcomeError = exports.isWriteTimeoutError = exports.isPeerUnavailableError = exports.resolveMode = exports.parseOffline = exports.leaseMs = exports.resolveTopology = void 0;
3
+ exports.release = exports.releaseWithExecutor = exports.acquire = exports.acquireOnExecutor = exports.acquireWithExecutor = exports.isUnknownOutcomeError = exports.isWriteTimeoutError = exports.isPeerUnavailableError = exports.resolveMode = exports.parseOffline = exports.leaseMs = exports.resolveTopology = void 0;
4
4
  /**
5
5
  * Cross-datacenter cron mutex for the self-hosted, multi-DC Scylla topology.
6
6
  * (Moved from minting.handle.me src/helpers/cronLock/cqlCronLock.ts @ 6ecb9c3 so every box cron
@@ -14,9 +14,9 @@ exports.release = exports.releaseWithExecutor = exports.acquire = exports.acquir
14
14
  * - all boxes online: SERIAL / QUORUM;
15
15
  * - this box offline: stand down;
16
16
  * - this box is the only declared-online box: LOCAL_SERIAL / LOCAL_QUORUM;
17
- * - an undeclared peer outage: the caller normally fails closed. StateData may
18
- * use its DC-local fallback only for mintPaidSessions and only after a definite
19
- * CQL UNAVAILABLE response (never after a timeout or unknown failure).
17
+ * - an undeclared peer outage: 'peerUnavailable'. Callers whose job is safe on one
18
+ * box (on-chain/idempotent) may take a DC-local fallback, and only after this
19
+ * definite CQL UNAVAILABLE response (never after a timeout or unknown failure).
20
20
  *
21
21
  * The row TTL recovers crashed holders. Unique per-invocation owner tokens prevent
22
22
  * overlapping ticks on one box from mistaking node identity for lock ownership.
@@ -70,7 +70,7 @@ const leaseSeconds = (override) => Math.ceil((0, exports.leaseMs)(override) / 10
70
70
  const scopeName = (name, topology) => `${topology.network}:${name}`;
71
71
  let client = null;
72
72
  let clientTopologyKey = '';
73
- let ensured = false;
73
+ const ensured = new WeakSet();
74
74
  const getClient = (topology) => {
75
75
  const topologyKey = JSON.stringify([topology.contactPoints, topology.localDataCenter]);
76
76
  if (!client || clientTopologyKey !== topologyKey) {
@@ -80,17 +80,25 @@ const getClient = (topology) => {
80
80
  socketOptions: { connectTimeout: 8000, readTimeout: 12000 }
81
81
  });
82
82
  clientTopologyKey = topologyKey;
83
- ensured = false;
84
83
  }
85
84
  return client;
86
85
  };
86
+ /**
87
+ * Schema changes need a majority of the whole cluster, so with a peer DC down a CREATE … IF NOT EXISTS
88
+ * hangs until the driver times out — masking the definite UNAVAILABLE the one-box solo path relies on
89
+ * (incident 2026-10-07). system_schema is replicated to every node (LocalStrategy), so reading it
90
+ * needs no peer; DDL is issued only on a cluster that really lacks the table.
91
+ */
87
92
  const ensureSchema = async (executor) => {
88
- if (ensured)
93
+ if (ensured.has(executor))
89
94
  return;
90
- await executor.execute(`CREATE KEYSPACE IF NOT EXISTS ${KEYSPACE} WITH replication = ` +
91
- `{'class':'NetworkTopologyStrategy','kora-dc1':1,'kora-dc2':1}`);
92
- await executor.execute(`CREATE TABLE IF NOT EXISTS ${KEYSPACE}.${LOCK_TABLE} (name text PRIMARY KEY, owner text, acquired_at timestamp)`);
93
- ensured = true;
95
+ const existing = await executor.execute('SELECT table_name FROM system_schema.tables WHERE keyspace_name = ? AND table_name = ?', [KEYSPACE, LOCK_TABLE], { prepare: true, consistency: cassandra_driver_1.types.consistencies.localOne });
96
+ if (existing.rows.length === 0) {
97
+ await executor.execute(`CREATE KEYSPACE IF NOT EXISTS ${KEYSPACE} WITH replication = ` +
98
+ `{'class':'NetworkTopologyStrategy','kora-dc1':1,'kora-dc2':1}`);
99
+ await executor.execute(`CREATE TABLE IF NOT EXISTS ${KEYSPACE}.${LOCK_TABLE} (name text PRIMARY KEY, owner text, acquired_at timestamp)`);
100
+ }
101
+ ensured.add(executor);
94
102
  };
95
103
  const parseOffline = (raw) => {
96
104
  const value = (raw !== null && raw !== void 0 ? raw : '').trim().toLowerCase();
@@ -191,12 +199,16 @@ const deleteIfOwner = async (executor, key, token, serialConsistency, consistenc
191
199
  return false;
192
200
  }
193
201
  };
202
+ /** One fresh process's acquisition: make sure the lock table exists, then take the lock. */
203
+ const acquireOnExecutor = async (executor, name, offline, topology, lease) => {
204
+ await ensureSchema(executor);
205
+ return (0, exports.acquireWithExecutor)(executor, name, offline, topology, undefined, lease);
206
+ };
207
+ exports.acquireOnExecutor = acquireOnExecutor;
194
208
  const acquire = async (name, offline, options = {}) => {
195
209
  try {
196
210
  const topology = (0, exports.resolveTopology)();
197
- const executor = getClient(topology);
198
- await ensureSchema(executor);
199
- const result = await (0, exports.acquireWithExecutor)(executor, name, offline, topology, undefined, options.leaseMs);
211
+ const result = await (0, exports.acquireOnExecutor)(getClient(topology), name, offline, topology, options.leaseMs);
200
212
  if (result.status === 'peerUnavailable') {
201
213
  logger_1.Logger.log({
202
214
  message: `cron lock '${name}' SERIAL unavailable because the configured peer quorum is unavailable: ${result.message}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koralabs/kora-labs-common",
3
- "version": "6.18.4",
3
+ "version": "6.19.0",
4
4
  "description": "Kora Labs Common Utilities",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",