mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
b494f64c4e
ResourceLockedError is emitted on every retry attempt during lock contention but was not handled, causing it to be logged as an unexpected error and reported to Sentry (OUTLINE-CLOUD-CAW). Fixes OUTLINE-CLOUD-CAW Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
110 lines
3.1 KiB
TypeScript
110 lines
3.1 KiB
TypeScript
import Redlock, {
|
|
ExecutionError,
|
|
ResourceLockedError,
|
|
type Lock,
|
|
type RedlockAbortSignal,
|
|
} from "redlock";
|
|
import Redis from "@server/storage/redis";
|
|
import Logger from "@server/logging/Logger";
|
|
import ShutdownHelper, { ShutdownOrder } from "./ShutdownHelper";
|
|
|
|
type AcquireOptions = {
|
|
/** Whether a lock should be automatically released on server shutdown */
|
|
releaseOnShutdown?: boolean;
|
|
};
|
|
|
|
export class MutexLock {
|
|
// Default expiry time for acquiring lock in milliseconds
|
|
public static defaultLockTimeout = 4000;
|
|
|
|
/**
|
|
* Returns the redlock instance
|
|
*/
|
|
public static get lock(): Redlock {
|
|
if (!this.redlock) {
|
|
this.redlock = new Redlock([Redis.defaultClient], {
|
|
retryJitter: 100,
|
|
retryCount: 120,
|
|
retryDelay: 1000,
|
|
});
|
|
this.redlock.on("error", (err) => {
|
|
if (err instanceof ResourceLockedError) {
|
|
// Expected during lock contention retries, not an error.
|
|
return;
|
|
} else if (err instanceof ExecutionError) {
|
|
Logger.warn("Failed to extend Redlock lock", {
|
|
message: err.message,
|
|
});
|
|
} else {
|
|
Logger.error("Unexpected Redlock error", err);
|
|
}
|
|
});
|
|
}
|
|
|
|
return this.redlock;
|
|
}
|
|
|
|
/**
|
|
* Acquire a Mutex lock
|
|
*
|
|
* @param resource The resource to lock
|
|
* @param timeout The duration to acquire the lock for if not released in milliseconds
|
|
* @returns A promise that resolves a to a Lock
|
|
*/
|
|
public static async acquire(
|
|
resource: string,
|
|
timeout: number,
|
|
options?: AcquireOptions
|
|
) {
|
|
const lock = await this.lock.acquire([resource], timeout);
|
|
if (options?.releaseOnShutdown) {
|
|
const key = `lock:${resource}`;
|
|
// @ts-expect-error Attach resource for use in shutdown
|
|
lock._key = key;
|
|
ShutdownHelper.add(key, ShutdownOrder.last, lock.release.bind(lock));
|
|
}
|
|
return lock;
|
|
}
|
|
|
|
/**
|
|
* Execute a routine in the context of an auto-extending lock. The lock is
|
|
* automatically acquired before the routine runs and released when it
|
|
* completes. If the lock cannot be extended, the provided AbortSignal will
|
|
* be triggered so the routine can bail out.
|
|
*
|
|
* @param resource The resource to lock.
|
|
* @param timeout The initial lock duration in milliseconds (auto-extended while running).
|
|
* @param routine The async routine to execute while holding the lock.
|
|
* @returns A promise that resolves with the routine's return value.
|
|
*/
|
|
public static async using<T>(
|
|
resource: string,
|
|
timeout: number,
|
|
routine: (signal: RedlockAbortSignal) => Promise<T>
|
|
): Promise<T> {
|
|
return this.lock.using([resource], timeout, routine);
|
|
}
|
|
|
|
/**
|
|
* Safely release a lock
|
|
*
|
|
* @param lock The lock to release
|
|
*/
|
|
public static release(lock: Lock) {
|
|
try {
|
|
if (lock && lock.expiration > new Date().getTime()) {
|
|
return lock.release();
|
|
}
|
|
return false;
|
|
} finally {
|
|
// @ts-expect-error Attach resource for use in shutdown
|
|
const key = lock._key;
|
|
if (key) {
|
|
ShutdownHelper.remove(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static redlock: Redlock;
|
|
}
|