/** * Lease timing policy for runtime receipts and Runtime Sheet attempts. * * Expiry is a liveness signal, not a logical ordering token. B2 moves write * ordering to coordinator run-attempt epochs; these values only decide how * long an owner may hold a lease without renewal. */ const DEFAULT_PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS = 10 * 60_000; const DEFAULT_PLAY_RUNTIME_SHEET_ATTEMPT_LEASE_TTL_MS = 10 * 60_000; function readRuntimeLeaseEnv(name: string): string | undefined { if (typeof process === 'undefined') return undefined; return process.env?.[name]; } function resolveRuntimeLeaseTtlMs( envName: string, defaultValueMs: number, ): number { const raw = readRuntimeLeaseEnv(envName); if (raw === undefined || raw.trim() === '') return defaultValueMs; if (!/^\d+$/.test(raw.trim())) { throw new Error( `${envName} must be a positive integer number of milliseconds.`, ); } const value = Number(raw.trim()); if (!Number.isSafeInteger(value) || value <= 0) { throw new Error( `${envName} must be a positive integer number of milliseconds.`, ); } return value; } export const PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS = resolveRuntimeLeaseTtlMs( 'DEEPLINE_WORK_RECEIPT_LEASE_TTL_MS', DEFAULT_PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS, ); export const PLAY_RUNTIME_SHEET_ATTEMPT_LEASE_TTL_MS = resolveRuntimeLeaseTtlMs( 'DEEPLINE_SHEET_ATTEMPT_LEASE_TTL_MS', DEFAULT_PLAY_RUNTIME_SHEET_ATTEMPT_LEASE_TTL_MS, ); export const PLAY_RUNTIME_LEASE_HEARTBEAT_DIVISOR = 3; export function runtimeLeaseHeartbeatIntervalMs(ttlMs: number): number { if (!Number.isFinite(ttlMs) || ttlMs <= 0) { throw new Error('Runtime lease heartbeat interval needs a positive TTL.'); } return Math.max(1, Math.floor(ttlMs / PLAY_RUNTIME_LEASE_HEARTBEAT_DIVISOR)); } /** * The receipt store is authoritative for lease duration. Executors may not * inherit the coordinator's environment, so derive renewal cadence from the * expiry returned with the actual claim instead of assuming a local TTL. */ export function runtimeLeaseHeartbeatIntervalFromExpiry(input: { leaseExpiresAt?: string | null; fallbackTtlMs: number; nowMs?: number; }): number { const expiresAtMs = input.leaseExpiresAt ? Date.parse(input.leaseExpiresAt) : Number.NaN; const nowMs = input.nowMs ?? Date.now(); const remainingMs = expiresAtMs - nowMs; return runtimeLeaseHeartbeatIntervalMs( Number.isFinite(remainingMs) && remainingMs > 0 ? remainingMs : input.fallbackTtlMs, ); }