2023-04-24 10:31:48 +02:00
|
|
|
import Redlock from "redlock"
|
2022-10-10 09:21:17 +02:00
|
|
|
import { getLockClient } from "./init"
|
|
|
|
import { LockOptions, LockType } from "@budibase/types"
|
2023-02-13 12:57:30 +01:00
|
|
|
import * as context from "../context"
|
|
|
|
import env from "../environment"
|
2023-11-21 11:52:50 +01:00
|
|
|
import { logWarn } from "../logging"
|
2023-11-29 20:50:50 +01:00
|
|
|
import { utils } from "@budibase/shared-core"
|
2022-10-10 09:21:17 +02:00
|
|
|
|
2023-05-30 18:41:20 +02:00
|
|
|
async function getClient(
|
2023-04-24 10:31:48 +02:00
|
|
|
type: LockType,
|
|
|
|
opts?: Redlock.Options
|
2023-05-30 18:41:20 +02:00
|
|
|
): Promise<Redlock> {
|
2023-04-24 10:31:48 +02:00
|
|
|
if (type === LockType.CUSTOM) {
|
|
|
|
return newRedlock(opts)
|
|
|
|
}
|
2023-11-29 20:50:50 +01:00
|
|
|
if (
|
|
|
|
env.isTest() &&
|
|
|
|
type !== LockType.TRY_ONCE &&
|
|
|
|
type !== LockType.AUTO_EXTEND
|
|
|
|
) {
|
2023-02-13 12:57:30 +01:00
|
|
|
return newRedlock(OPTIONS.TEST)
|
|
|
|
}
|
2022-10-10 09:21:17 +02:00
|
|
|
switch (type) {
|
|
|
|
case LockType.TRY_ONCE: {
|
2023-02-13 12:57:30 +01:00
|
|
|
return newRedlock(OPTIONS.TRY_ONCE)
|
2022-10-10 09:21:17 +02:00
|
|
|
}
|
2023-05-30 18:41:20 +02:00
|
|
|
case LockType.TRY_TWICE: {
|
|
|
|
return newRedlock(OPTIONS.TRY_TWICE)
|
|
|
|
}
|
2022-12-15 13:45:53 +01:00
|
|
|
case LockType.DEFAULT: {
|
2023-02-13 12:57:30 +01:00
|
|
|
return newRedlock(OPTIONS.DEFAULT)
|
2022-12-15 13:45:53 +01:00
|
|
|
}
|
|
|
|
case LockType.DELAY_500: {
|
2023-02-13 12:57:30 +01:00
|
|
|
return newRedlock(OPTIONS.DELAY_500)
|
2022-12-15 13:45:53 +01:00
|
|
|
}
|
2023-11-29 20:50:50 +01:00
|
|
|
case LockType.AUTO_EXTEND: {
|
|
|
|
return newRedlock(OPTIONS.AUTO_EXTEND)
|
|
|
|
}
|
2022-10-10 09:21:17 +02:00
|
|
|
default: {
|
2023-11-29 20:50:50 +01:00
|
|
|
throw utils.unreachable(type)
|
2022-10-10 09:21:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-29 20:50:50 +01:00
|
|
|
const OPTIONS: Record<keyof typeof LockType | "TEST", Redlock.Options> = {
|
2022-10-10 09:21:17 +02:00
|
|
|
TRY_ONCE: {
|
|
|
|
// immediately throws an error if the lock is already held
|
|
|
|
retryCount: 0,
|
|
|
|
},
|
2023-05-30 18:41:20 +02:00
|
|
|
TRY_TWICE: {
|
|
|
|
retryCount: 1,
|
|
|
|
},
|
2023-02-13 12:57:30 +01:00
|
|
|
TEST: {
|
|
|
|
// higher retry count in unit tests
|
|
|
|
// due to high contention.
|
|
|
|
retryCount: 100,
|
|
|
|
},
|
2022-10-10 09:21:17 +02:00
|
|
|
DEFAULT: {
|
2022-06-01 18:52:41 +02:00
|
|
|
// the expected clock drift; for more details
|
|
|
|
// see http://redis.io/topics/distlock
|
|
|
|
driftFactor: 0.01, // multiplied by lock ttl to determine drift time
|
|
|
|
|
|
|
|
// the max number of times Redlock will attempt
|
|
|
|
// to lock a resource before erroring
|
2022-10-10 09:21:17 +02:00
|
|
|
retryCount: 10,
|
2022-06-01 18:52:41 +02:00
|
|
|
|
|
|
|
// the time in ms between attempts
|
|
|
|
retryDelay: 200, // time in ms
|
|
|
|
|
|
|
|
// the max time in ms randomly added to retries
|
|
|
|
// to improve performance under high contention
|
|
|
|
// see https://www.awsarchitectureblog.com/2015/03/backoff.html
|
2022-10-10 09:21:17 +02:00
|
|
|
retryJitter: 100, // time in ms
|
|
|
|
},
|
2022-12-15 13:45:53 +01:00
|
|
|
DELAY_500: {
|
|
|
|
retryDelay: 500,
|
|
|
|
},
|
2023-11-29 20:50:50 +01:00
|
|
|
CUSTOM: {},
|
|
|
|
AUTO_EXTEND: {
|
|
|
|
retryCount: -1,
|
|
|
|
},
|
2022-10-10 09:21:17 +02:00
|
|
|
}
|
|
|
|
|
2023-05-30 18:41:20 +02:00
|
|
|
export async function newRedlock(opts: Redlock.Options = {}) {
|
2023-11-29 20:50:50 +01:00
|
|
|
let options = { ...OPTIONS, ...opts }
|
2022-10-10 09:21:17 +02:00
|
|
|
const redisWrapper = await getLockClient()
|
|
|
|
const client = redisWrapper.getClient()
|
|
|
|
return new Redlock([client], options)
|
|
|
|
}
|
|
|
|
|
2023-02-28 12:52:43 +01:00
|
|
|
type SuccessfulRedlockExecution<T> = {
|
|
|
|
executed: true
|
|
|
|
result: T
|
|
|
|
}
|
|
|
|
type UnsuccessfulRedlockExecution = {
|
|
|
|
executed: false
|
|
|
|
}
|
|
|
|
|
|
|
|
type RedlockExecution<T> =
|
|
|
|
| SuccessfulRedlockExecution<T>
|
|
|
|
| UnsuccessfulRedlockExecution
|
|
|
|
|
2023-05-30 18:41:20 +02:00
|
|
|
function getLockName(opts: LockOptions) {
|
|
|
|
// determine lock name
|
|
|
|
// by default use the tenantId for uniqueness, unless using a system lock
|
|
|
|
const prefix = opts.systemLock ? "system" : context.getTenantId()
|
|
|
|
let name: string = `lock:${prefix}_${opts.name}`
|
|
|
|
// add additional unique name if required
|
|
|
|
if (opts.resource) {
|
|
|
|
name = name + `_${opts.resource}`
|
|
|
|
}
|
|
|
|
return name
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function doWithLock<T>(
|
2023-02-28 12:47:28 +01:00
|
|
|
opts: LockOptions,
|
|
|
|
task: () => Promise<T>
|
2023-05-30 18:41:20 +02:00
|
|
|
): Promise<RedlockExecution<T>> {
|
2023-05-05 15:42:21 +02:00
|
|
|
const redlock = await getClient(opts.type, opts.customOptions)
|
2023-11-29 13:56:25 +01:00
|
|
|
let lock: Redlock.Lock | undefined
|
2023-11-29 20:50:50 +01:00
|
|
|
let timeout
|
2022-10-10 09:21:17 +02:00
|
|
|
try {
|
2023-05-30 18:41:20 +02:00
|
|
|
const name = getLockName(opts)
|
2023-02-13 12:57:30 +01:00
|
|
|
|
|
|
|
// create the lock
|
2023-11-29 20:50:50 +01:00
|
|
|
lock = await redlock.lock(name, opts.ttl)
|
2023-11-29 13:56:25 +01:00
|
|
|
|
2023-11-29 20:50:50 +01:00
|
|
|
if (opts.type === LockType.AUTO_EXTEND) {
|
2023-11-29 22:01:49 +01:00
|
|
|
// We keep extending the lock while the task is running
|
2023-11-29 20:50:50 +01:00
|
|
|
const extendInIntervals = (): void => {
|
|
|
|
timeout = setTimeout(async () => {
|
|
|
|
let isExpired = false
|
|
|
|
try {
|
2023-11-29 22:01:25 +01:00
|
|
|
lock = await lock!.extend(opts.ttl)
|
2023-11-29 20:50:50 +01:00
|
|
|
} catch (err: any) {
|
|
|
|
isExpired = err.message.includes("Cannot extend lock on resource")
|
|
|
|
if (isExpired) {
|
|
|
|
console.error("The lock expired", { name })
|
|
|
|
} else {
|
|
|
|
throw err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isExpired) {
|
|
|
|
extendInIntervals()
|
|
|
|
}
|
|
|
|
}, opts.ttl / 2)
|
|
|
|
}
|
|
|
|
|
|
|
|
extendInIntervals()
|
2023-11-29 13:56:25 +01:00
|
|
|
}
|
2023-02-13 12:57:30 +01:00
|
|
|
|
2022-10-10 09:21:17 +02:00
|
|
|
// perform locked task
|
2022-12-15 13:45:53 +01:00
|
|
|
// need to await to ensure completion before unlocking
|
|
|
|
const result = await task()
|
2023-02-28 12:52:43 +01:00
|
|
|
return { executed: true, result }
|
2022-10-10 09:21:17 +02:00
|
|
|
} catch (e: any) {
|
2023-11-21 11:52:50 +01:00
|
|
|
logWarn(`lock type: ${opts.type} error`, e)
|
2022-10-10 09:21:17 +02:00
|
|
|
// lock limit exceeded
|
|
|
|
if (e.name === "LockError") {
|
|
|
|
if (opts.type === LockType.TRY_ONCE) {
|
|
|
|
// don't throw for try-once locks, they will always error
|
|
|
|
// due to retry count (0) exceeded
|
2023-02-28 12:52:43 +01:00
|
|
|
return { executed: false }
|
2022-10-10 09:21:17 +02:00
|
|
|
} else {
|
|
|
|
throw e
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw e
|
|
|
|
}
|
|
|
|
} finally {
|
2023-11-29 22:09:09 +01:00
|
|
|
clearTimeout(timeout)
|
|
|
|
await lock?.unlock()
|
2022-10-10 09:21:17 +02:00
|
|
|
}
|
2022-06-01 18:52:41 +02:00
|
|
|
}
|