@koralabs/kora-labs-common 6.15.0 → 6.15.1

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.
@@ -38,6 +38,11 @@ export declare const resolveMode: (offline: string[], topology: CronLockTopology
38
38
  export declare const isPeerUnavailableError: (error: unknown) => boolean;
39
39
  /** A write timeout leaves an LWT's outcome unknown: the proposal may or may not have been applied. */
40
40
  export declare const isWriteTimeoutError: (error: unknown) => boolean;
41
+ /**
42
+ * An LWT whose outcome is unknown: the coordinator reported a write timeout, or it never replied within
43
+ * the driver's readTimeout ("The host … did not reply before timeout") — either way it may have committed.
44
+ */
45
+ export declare const isUnknownOutcomeError: (error: unknown) => boolean;
41
46
  /**
42
47
  * The directly testable LWT operation. Each invocation owns a unique token, so
43
48
  * concurrent ticks on one node cannot both treat the node identity as ownership.
@@ -46,6 +51,18 @@ export declare const acquireWithExecutor: (executor: CqlExecutor, name: string,
46
51
  export declare const acquire: (name: string, offline?: string | null, options?: {
47
52
  leaseMs?: number;
48
53
  }) => Promise<AcquireResult>;
49
- /** Release only the exact acquisition token owned by this invocation. */
54
+ export type ReleaseResult = {
55
+ status: 'released';
56
+ } | {
57
+ status: 'unknown';
58
+ message: string;
59
+ };
60
+ /**
61
+ * Release only the exact acquisition token owned by this invocation. The conditional DELETE is an LWT
62
+ * too: when it fails its outcome is unknown, and a surviving row would idle the job on every box for
63
+ * the whole lease. A serial read completes any in-flight Paxos round and tells whether the row is still
64
+ * ours; if it is, the (token-conditional) delete is issued once more.
65
+ */
66
+ export declare const releaseWithExecutor: (executor: CqlExecutor, name: string, token: string, offline: string | null | undefined, topology: CronLockTopology) => Promise<ReleaseResult>;
50
67
  export declare const release: (name: string, token: string, offline?: string | null) => Promise<void>;
51
68
  export {};
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.release = exports.acquire = exports.acquireWithExecutor = exports.isWriteTimeoutError = exports.isPeerUnavailableError = exports.resolveMode = exports.parseOffline = exports.leaseMs = exports.resolveTopology = void 0;
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;
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
@@ -117,6 +117,12 @@ exports.isPeerUnavailableError = isPeerUnavailableError;
117
117
  /** A write timeout leaves an LWT's outcome unknown: the proposal may or may not have been applied. */
118
118
  const isWriteTimeoutError = (error) => error instanceof cassandra_driver_1.errors.ResponseError && error.code === cassandra_driver_1.types.responseErrorCodes.writeTimeout;
119
119
  exports.isWriteTimeoutError = isWriteTimeoutError;
120
+ /**
121
+ * An LWT whose outcome is unknown: the coordinator reported a write timeout, or it never replied within
122
+ * the driver's readTimeout ("The host … did not reply before timeout") — either way it may have committed.
123
+ */
124
+ const isUnknownOutcomeError = (error) => (0, exports.isWriteTimeoutError)(error) || error instanceof cassandra_driver_1.errors.OperationTimedOutError;
125
+ exports.isUnknownOutcomeError = isUnknownOutcomeError;
120
126
  /**
121
127
  * The directly testable LWT operation. Each invocation owns a unique token, so
122
128
  * concurrent ticks on one node cannot both treat the node identity as ownership.
@@ -144,7 +150,7 @@ const acquireWithExecutor = async (executor, name, offline, topology, token = cr
144
150
  if (mode === 'serial' && (0, exports.isPeerUnavailableError)(error)) {
145
151
  return { status: 'peerUnavailable', token, message };
146
152
  }
147
- if ((0, exports.isWriteTimeoutError)(error)) {
153
+ if ((0, exports.isUnknownOutcomeError)(error)) {
148
154
  // The LWT timed out: it may have been applied WITH OUR TOKEN, in which case nobody (not even
149
155
  // us) could run until the lease expired. A read at the same serial consistency completes any
150
156
  // in-flight Paxos round, so it tells us the real owner.
@@ -217,22 +223,53 @@ const acquire = async (name, offline, options = {}) => {
217
223
  }
218
224
  };
219
225
  exports.acquire = acquire;
220
- /** Release only the exact acquisition token owned by this invocation. */
226
+ /**
227
+ * Release only the exact acquisition token owned by this invocation. The conditional DELETE is an LWT
228
+ * too: when it fails its outcome is unknown, and a surviving row would idle the job on every box for
229
+ * the whole lease. A serial read completes any in-flight Paxos round and tells whether the row is still
230
+ * ours; if it is, the (token-conditional) delete is issued once more.
231
+ */
232
+ const releaseWithExecutor = async (executor, name, token, offline, topology) => {
233
+ const mode = (0, exports.resolveMode)((0, exports.parseOffline)(offline), topology);
234
+ if (mode === 'standDown')
235
+ return { status: 'released' };
236
+ const serialConsistency = mode === 'localSerial' ? cassandra_driver_1.types.consistencies.localSerial : cassandra_driver_1.types.consistencies.serial;
237
+ const consistency = mode === 'localSerial' ? cassandra_driver_1.types.consistencies.localQuorum : cassandra_driver_1.types.consistencies.quorum;
238
+ const key = scopeName(name, topology);
239
+ try {
240
+ await executor.execute(`DELETE FROM ${KEYSPACE}.${LOCK_TABLE} WHERE name = ? IF owner = ?`, [key, token], { prepare: true, serialConsistency, consistency });
241
+ return { status: 'released' };
242
+ }
243
+ catch (error) {
244
+ const message = error instanceof Error ? error.message : String(error);
245
+ const read = await readOwner(executor, key, serialConsistency);
246
+ if (read.error)
247
+ return { status: 'unknown', message: `${message}; owner read failed (${read.error})` };
248
+ if (read.owner !== token)
249
+ return { status: 'released' };
250
+ return (await deleteIfOwner(executor, key, token, serialConsistency, consistency))
251
+ ? { status: 'released' }
252
+ : { status: 'unknown', message: `${message}; the row is still ours and the second delete failed` };
253
+ }
254
+ };
255
+ exports.releaseWithExecutor = releaseWithExecutor;
221
256
  const release = async (name, token, offline) => {
222
257
  try {
223
258
  const topology = (0, exports.resolveTopology)();
224
- const mode = (0, exports.resolveMode)((0, exports.parseOffline)(offline), topology);
225
- if (mode === 'standDown')
226
- return;
227
- const serialConsistency = mode === 'localSerial' ? cassandra_driver_1.types.consistencies.localSerial : cassandra_driver_1.types.consistencies.serial;
228
- const consistency = mode === 'localSerial' ? cassandra_driver_1.types.consistencies.localQuorum : cassandra_driver_1.types.consistencies.quorum;
229
- await getClient(topology).execute(`DELETE FROM ${KEYSPACE}.${LOCK_TABLE} WHERE name = ? IF owner = ?`, [scopeName(name, topology), token], { prepare: true, serialConsistency, consistency });
259
+ const result = await (0, exports.releaseWithExecutor)(getClient(topology), name, token, offline, topology);
260
+ if (result.status === 'unknown') {
261
+ logger_1.Logger.log({
262
+ message: `cron lock '${name}' release failed; if the row survived, '${name}' idles on every box until its lease expires: ${result.message}`,
263
+ event: 'cqlCronLock.release.unknown',
264
+ category: logger_1.LogCategory.WARN
265
+ });
266
+ }
230
267
  }
231
268
  catch (error) {
232
269
  logger_1.Logger.log({
233
- message: `cron lock '${name}' release best-effort failed (TTL will reclaim): ${error.message}`,
234
- event: 'cqlCronLock.release.softfail',
235
- category: logger_1.LogCategory.INFO
270
+ message: `cron lock '${name}' release failed (TTL will reclaim): ${error.message}`,
271
+ event: 'cqlCronLock.release.unknown',
272
+ category: logger_1.LogCategory.WARN
236
273
  });
237
274
  }
238
275
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koralabs/kora-labs-common",
3
- "version": "6.15.0",
3
+ "version": "6.15.1",
4
4
  "description": "Kora Labs Common Utilities",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",