openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
408 lines (407 loc) • 16.3 kB
JavaScript
import { n as MAX_TIMER_TIMEOUT_MS } from "./number-coercion-CLj0HTDM.js";
import { n as loggingState } from "./state-UxwJeH7V.js";
import { n as computeBackoff, s as sleepWithAbort } from "./src-BQ327IOM.js";
import { c as isSqliteLockError, p as runWithSqliteBusyTimeout } from "./node-sqlite-BpQX3W0e.js";
import { t as runtimeProcessEntrypoints } from "./runtime-process-entrypoints-CEMkwAJ_.js";
import { n as resolveRuntimeWorkerArgv, r as resolveRuntimeWorkerUrl } from "./runtime-worker-url-CpdriB1D.js";
import { i as openOpenClawStateDatabase, s as runOpenClawStateWriteTransaction } from "./openclaw-state-db-BRTnL-D8.js";
import { t as createDeferredCore } from "./deferred-D0La5CRk.js";
import "./backoff-BkMI1WEL.js";
import { a as leaseHeartbeatState, i as renewOpenClawStateLeaseInTransaction, n as readOpenClawStateLeaseExpiry, r as releaseOpenClawStateLeaseInTransaction, t as acquireOpenClawStateLeaseInTransaction } from "./openclaw-state-lease-store-jGIk9tQ0.js";
import { randomUUID } from "node:crypto";
import { Worker } from "node:worker_threads";
//#region src/state/openclaw-state-lease-heartbeat.ts
const WORKER_START_TIMEOUT_MS = 5e3;
const WORKER_RESPONSE_TIMEOUT_MS = 1e3;
function startOpenClawStateLeaseHeartbeat(params) {
const shared = new BigInt64Array(new SharedArrayBuffer(3 * BigInt64Array.BYTES_PER_ELEMENT));
const url = resolveRuntimeWorkerUrl(runtimeProcessEntrypoints.stateLeaseHeartbeat);
const worker = new Worker(url, {
workerData: {
path: params.path,
identity: {
scope: params.identity.scope,
key: params.identity.key,
owner: params.identity.owner
},
leaseMs: params.leaseMs,
heartbeatMs: params.heartbeatMs,
shared: shared.buffer
},
env: {},
execArgv: resolveRuntimeWorkerArgv(url).slice(0, -1),
stdout: true,
stderr: true
});
worker.stdout.resume();
worker.stderr.resume();
const ready = createDeferredCore();
const fail = (error) => {
if (Atomics.load(shared, leaseHeartbeatState.status) === leaseHeartbeatState.closed) return;
Atomics.store(shared, leaseHeartbeatState.status, leaseHeartbeatState.lost);
Atomics.notify(shared, leaseHeartbeatState.ack);
clearTimeout(startTimer);
ready.reject(error);
params.onLost(error);
};
const settleStartup = () => {
clearTimeout(startTimer);
if (Atomics.compareExchange(shared, leaseHeartbeatState.status, leaseHeartbeatState.starting, leaseHeartbeatState.lost) === leaseHeartbeatState.ready) ready.resolve();
else fail(/* @__PURE__ */ new Error("state lease heartbeat did not become ready"));
};
const startTimer = setTimeout(settleStartup, Math.max(1, Math.min(WORKER_START_TIMEOUT_MS, params.expiresAt - Date.now())));
worker.once("error", fail);
worker.once("exit", () => fail(/* @__PURE__ */ new Error("state lease heartbeat exited")));
worker.once("message", settleStartup);
let stopping;
const close = () => {
Atomics.store(shared, leaseHeartbeatState.status, leaseHeartbeatState.closed);
Atomics.notify(shared, leaseHeartbeatState.ack);
clearTimeout(startTimer);
ready.reject(/* @__PURE__ */ new Error("state lease heartbeat closed"));
};
return {
ready: ready.promise,
close,
stop() {
close();
return stopping ??= worker.terminate();
},
assertResponsive(expiresAt) {
const deadline = performance.now() + Math.min(WORKER_RESPONSE_TIMEOUT_MS, expiresAt - Date.now());
const request = Atomics.add(shared, leaseHeartbeatState.request, 1n) + 1n;
worker.postMessage(null, []);
while (Atomics.load(shared, leaseHeartbeatState.status) === leaseHeartbeatState.ready) {
const ack = Atomics.load(shared, leaseHeartbeatState.ack);
if (ack === request && Atomics.load(shared, leaseHeartbeatState.status) === leaseHeartbeatState.ready) return;
const remainingMs = deadline - performance.now();
if (remainingMs <= 0) break;
Atomics.wait(shared, leaseHeartbeatState.ack, ack, remainingMs);
}
const error = /* @__PURE__ */ new Error("state lease heartbeat is not responsive");
fail(error);
throw error;
}
};
}
//#endregion
//#region src/state/openclaw-state-lease.ts
var OpenClawStateLeaseError = class extends Error {
constructor(message, options) {
super(message, { cause: options.cause });
this.name = "OpenClawStateLeaseError";
this.code = options.code;
}
};
const ACQUIRE_BACKOFF = {
initialMs: 25,
maxMs: 250,
factor: 1.5,
jitter: .25
};
const MIN_LEASE_MS = 1e3;
const LEASE_DB_BUSY_TIMEOUT_MS = 0;
const RELEASE_RETRY_TIMEOUT_MS = 2e3;
const processExitLeaseCleanups = /* @__PURE__ */ new Set();
let processExitListenerInstalled = false;
function runProcessExitLeaseCleanups() {
processExitListenerInstalled = false;
const previousForceConsoleToStderr = loggingState.forceConsoleToStderr;
loggingState.forceConsoleToStderr = true;
try {
for (const cleanup of processExitLeaseCleanups) try {
cleanup();
} catch {}
processExitLeaseCleanups.clear();
} finally {
loggingState.forceConsoleToStderr = previousForceConsoleToStderr;
}
}
function registerProcessExitLeaseCleanup(cleanup) {
processExitLeaseCleanups.add(cleanup);
if (!processExitListenerInstalled) {
process.once("exit", runProcessExitLeaseCleanups);
processExitListenerInstalled = true;
}
return () => {
processExitLeaseCleanups.delete(cleanup);
if (processExitLeaseCleanups.size === 0 && processExitListenerInstalled) {
process.removeListener("exit", runProcessExitLeaseCleanups);
processExitListenerInstalled = false;
}
};
}
function leaseError(code, message, cause) {
return new OpenClawStateLeaseError(message, {
code,
...cause === void 0 ? {} : { cause }
});
}
function invalidInput(message) {
return leaseError("OPENCLAW_STATE_LEASE_INVALID_INPUT", message);
}
function validateDuration(value, label, minimum, maximum) {
if (!Number.isInteger(value) || value < minimum || value > maximum) throw invalidInput(`${label} must be an integer between ${minimum} and ${maximum}`);
return value;
}
function validateNonEmptyString(value, label) {
if (typeof value !== "string" || !value.trim() || value.includes("\0")) throw invalidInput(`${label} must be a non-empty string without NUL bytes`);
return value;
}
function validateOptions(options) {
if (typeof options !== "object" || options === null || Array.isArray(options)) throw invalidInput("state lease options must be an object");
if (options.signal !== void 0 && !(options.signal instanceof AbortSignal)) throw invalidInput("state lease signal must be an AbortSignal");
const database = options.database;
if (typeof database !== "object" || database === null || Array.isArray(database)) throw invalidInput("state lease database must be an object");
if (database.scope !== "shared") throw invalidInput("state lease database scope must be shared");
const leaseLabel = options.leaseLabel === void 0 ? "state lease" : validateNonEmptyString(options.leaseLabel, "state lease label");
const operationLabel = options.operationLabel === void 0 ? "state.lease" : validateNonEmptyString(options.operationLabel, "state lease operationLabel");
return {
scope: validateNonEmptyString(options.scope, `${leaseLabel} scope`),
key: validateNonEmptyString(options.key, `${leaseLabel} key`),
database,
leaseMs: validateDuration(options.leaseMs, `${leaseLabel} leaseMs`, MIN_LEASE_MS, MAX_TIMER_TIMEOUT_MS),
waitMs: validateDuration(options.waitMs, `${leaseLabel} waitMs`, 0, MAX_TIMER_TIMEOUT_MS),
signal: options.signal,
heartbeat: options.heartbeat,
leaseLabel,
operationLabel
};
}
function withLeaseWriteTransaction(database, operationLabel, operation, busyTimeoutMs = LEASE_DB_BUSY_TIMEOUT_MS) {
const stateDatabase = openOpenClawStateDatabase(database.options);
const run = () => runOpenClawStateWriteTransaction(({ db }) => operation(db), {
...database.options,
database: stateDatabase
}, {
operationLabel,
busyTimeoutMs
});
return runWithSqliteBusyTimeout(stateDatabase.db, busyTimeoutMs, run);
}
function tryAcquire(params) {
return withLeaseWriteTransaction(params.database, params.operationLabel, (db) => acquireOpenClawStateLeaseInTransaction(db, params, params.leaseMs));
}
function renew(params) {
return withLeaseWriteTransaction(params.database, params.operationLabel, (db) => {
const expiresAt = renewOpenClawStateLeaseInTransaction(db, params, params.leaseMs);
if (expiresAt === void 0) throw leaseError("OPENCLAW_STATE_LEASE_LOST", `${params.leaseLabel} ${params.scope}/${params.key} was lost`);
return expiresAt;
});
}
function assertLeaseOwnedInDatabase(database, params) {
const expiresAt = readOpenClawStateLeaseExpiry(database, params);
if (expiresAt === void 0) throw leaseError("OPENCLAW_STATE_LEASE_LOST", `${params.leaseLabel} ${params.scope}/${params.key} was lost`);
return expiresAt;
}
function verifyLeaseOwnership(params) {
try {
if (params.transaction) return assertLeaseOwnedInDatabase(params.transaction, params);
if (!params.database) throw new Error("state lease ownership check requires a database");
return assertLeaseOwnedInDatabase(openOpenClawStateDatabase(params.database.options).db, params);
} catch (error) {
if (error instanceof OpenClawStateLeaseError) throw error;
throw leaseError("OPENCLAW_STATE_LEASE_STORAGE_FAILED", `failed to verify ${params.leaseLabel} ${params.scope}/${params.key}`, error);
}
}
function release(params) {
withLeaseWriteTransaction(params.database, params.operationLabel, (db) => releaseOpenClawStateLeaseInTransaction(db, params));
}
async function releaseBestEffort(params) {
const deadline = performance.now() + RELEASE_RETRY_TIMEOUT_MS;
let attempt = 0;
while (true) try {
release(params);
return;
} catch (error) {
const now = performance.now();
if (!isSqliteLockError(error) || now >= deadline) return;
attempt += 1;
await sleepWithAbort(Math.min(deadline - now, computeBackoff(ACQUIRE_BACKOFF, attempt)));
}
}
function abortError(signal, label, leaseLabel) {
return leaseError("OPENCLAW_STATE_LEASE_ABORTED", `${leaseLabel} ${label} was aborted`, signal.reason);
}
/** Run one trusted operation under a host-owned SQLite lease. */
async function withOpenClawStateLease(options, run) {
const validated = validateOptions(options);
if (validated.signal?.aborted) throw abortError(validated.signal, "acquisition", validated.leaseLabel);
const owner = randomUUID();
const deadline = performance.now() + validated.waitMs;
let attempt = 0;
let confirmedExpiresAt;
while (confirmedExpiresAt === void 0) {
if (validated.signal?.aborted) throw abortError(validated.signal, "acquisition", validated.leaseLabel);
try {
confirmedExpiresAt = tryAcquire({
database: validated.database,
operationLabel: validated.operationLabel,
scope: validated.scope,
key: validated.key,
owner,
leaseMs: validated.leaseMs,
leaseLabel: validated.leaseLabel
});
} catch (error) {
if (error instanceof OpenClawStateLeaseError) throw error;
if (!isSqliteLockError(error)) throw leaseError("OPENCLAW_STATE_LEASE_STORAGE_FAILED", `failed to acquire ${validated.leaseLabel} ${validated.scope}/${validated.key}`, error);
}
const now = performance.now();
if (confirmedExpiresAt !== void 0) {
if (validated.signal?.aborted || validated.waitMs > 0 && now >= deadline) {
await releaseBestEffort({
database: validated.database,
operationLabel: validated.operationLabel,
scope: validated.scope,
key: validated.key,
owner,
leaseLabel: validated.leaseLabel
});
if (validated.signal?.aborted) throw abortError(validated.signal, "acquisition", validated.leaseLabel);
throw leaseError("OPENCLAW_STATE_LEASE_TIMEOUT", `timed out waiting for ${validated.leaseLabel} ${validated.scope}/${validated.key}`);
}
break;
}
if (now >= deadline) throw leaseError("OPENCLAW_STATE_LEASE_TIMEOUT", `timed out waiting for ${validated.leaseLabel} ${validated.scope}/${validated.key}`);
attempt += 1;
const delayMs = Math.min(deadline - now, computeBackoff(ACQUIRE_BACKOFF, attempt));
try {
await sleepWithAbort(delayMs, validated.signal);
} catch (error) {
if (validated.signal?.aborted) throw abortError(validated.signal, "acquisition", validated.leaseLabel);
throw error;
}
}
const identity = {
scope: validated.scope,
key: validated.key,
owner,
leaseLabel: validated.leaseLabel
};
let closed = false;
let workerHeartbeat;
const unregisterProcessExitCleanup = registerProcessExitLeaseCleanup(() => {
closed = true;
workerHeartbeat?.close();
release({
...identity,
database: validated.database,
operationLabel: validated.operationLabel
});
});
const leaseLost = new AbortController();
const operationSignal = validated.signal ? AbortSignal.any([validated.signal, leaseLost.signal]) : leaseLost.signal;
const heartbeatMs = Math.max(250, Math.min(3e4, Math.floor(validated.leaseMs / 3)));
let expiryTimer;
let heartbeat;
const abortLost = (cause) => {
if (!leaseLost.signal.aborted) leaseLost.abort(cause instanceof OpenClawStateLeaseError ? cause : leaseError("OPENCLAW_STATE_LEASE_LOST", `${validated.leaseLabel} ${validated.scope}/${validated.key} was lost`, cause));
};
const scheduleExpiry = () => {
if (expiryTimer) clearTimeout(expiryTimer);
expiryTimer = setTimeout(() => abortLost(), Math.max(1, (confirmedExpiresAt ?? Date.now()) - Date.now()));
expiryTimer.unref?.();
};
const renewAndSchedule = () => {
confirmedExpiresAt = renew({
...identity,
database: validated.database,
operationLabel: validated.operationLabel,
leaseMs: validated.leaseMs
});
scheduleExpiry();
};
const renewOperation = () => {
assertActive();
if (workerHeartbeat) assertOperationOwned();
else renewAndSchedule();
};
const renewFromTimer = () => {
try {
renewAndSchedule();
} catch (error) {
if (error instanceof OpenClawStateLeaseError && error.code === "OPENCLAW_STATE_LEASE_LOST") abortLost(error);
else if (confirmedExpiresAt !== void 0 && Date.now() >= confirmedExpiresAt) abortLost(error);
}
};
const assertActive = () => {
if (leaseLost.signal.aborted) throw leaseLost.signal.reason;
if (validated.signal?.aborted) throw abortError(validated.signal, "operation", validated.leaseLabel);
if (closed) {
abortLost();
throw leaseLost.signal.reason;
}
};
const assertOperationOwned = (transaction) => {
assertActive();
const params = {
...identity,
database: validated.database,
transaction
};
const expiresAt = verifyLeaseOwnership(params);
if (workerHeartbeat) {
try {
workerHeartbeat.assertResponsive(expiresAt);
} catch (error) {
abortLost(error);
throw leaseLost.signal.reason;
}
assertActive();
verifyLeaseOwnership(params);
}
};
const stopWorker = () => {
workerHeartbeat?.stop();
};
try {
let result;
try {
if (validated.heartbeat === "worker") {
workerHeartbeat = startOpenClawStateLeaseHeartbeat({
path: openOpenClawStateDatabase(validated.database.options).path,
identity,
leaseMs: validated.leaseMs,
heartbeatMs,
expiresAt: confirmedExpiresAt,
onLost: abortLost
});
validated.signal?.addEventListener("abort", stopWorker, { once: true });
if (validated.signal?.aborted) stopWorker();
await workerHeartbeat.ready;
} else {
scheduleExpiry();
heartbeat = setInterval(renewFromTimer, heartbeatMs);
heartbeat.unref?.();
}
assertOperationOwned();
result = await run({
signal: operationSignal,
renew: renewOperation,
assertOwned: assertOperationOwned,
assertOwnedInTransaction: assertOperationOwned
});
} catch (error) {
if (leaseLost.signal.aborted) throw leaseLost.signal.reason;
if (validated.signal?.aborted) throw abortError(validated.signal, "operation", validated.leaseLabel);
throw error;
}
assertOperationOwned();
return result;
} finally {
closed = true;
unregisterProcessExitCleanup();
validated.signal?.removeEventListener("abort", stopWorker);
clearInterval(heartbeat);
if (expiryTimer) clearTimeout(expiryTimer);
await workerHeartbeat?.stop();
await releaseBestEffort({
...identity,
database: validated.database,
operationLabel: validated.operationLabel
});
}
}
//#endregion
export { withOpenClawStateLease as n, OpenClawStateLeaseError as t };