Merged in feat/redis-fix (pull request #3207)

Feat/redis fix

* feat(redis): delete multiple keys in one partition scan

* fix(BOOK-603): make it possible to do multiple deletes in redis at once using one partition scan


Approved-by: Linus Flood
This commit is contained in:
Joakim Jäderberg
2025-11-24 10:17:35 +00:00
parent 29e81d9995
commit 8ed16a0119
11 changed files with 260 additions and 61 deletions

View File

@@ -79,7 +79,11 @@ export function parseBookingWidgetSearchParams(
return result
} catch (error) {
logger.error("[URL] Error parsing search params for booking widget:", error)
logger.error(
"[URL] Error parsing search params for booking widget:",
error,
searchParams
)
return {}
}
}

View File

@@ -99,4 +99,12 @@ export type DataCache = {
* @returns
*/
deleteKey: (key: string, opts?: { fuzzy?: boolean }) => Promise<void>
/**
* Deletes a key from the cache
* @param keys CacheKeys to delete
* @param fuzzy If true, does a wildcard delete. *key*
* @returns
*/
deleteKeys: (keys: string[], opts?: { fuzzy?: boolean }) => Promise<void>
}

View File

@@ -1,5 +1,6 @@
import { cacheOrGet } from "./cacheOrGet"
import { deleteKey } from "./deleteKey"
import { deleteKeys } from "./deleteKeys"
import { get } from "./get"
import { set } from "./set"
@@ -12,5 +13,6 @@ export async function createDistributedCache(): Promise<DataCache> {
set,
cacheOrGet,
deleteKey,
deleteKeys,
} satisfies DataCache
}

View File

@@ -0,0 +1,45 @@
import * as Sentry from "@sentry/nextjs"
import { env } from "../../env/server"
import { safeTry } from "../../utils/safeTry"
import { cacheLogger } from "../logger"
import { getDeleteMultipleKeysEndpoint } from "./endpoints"
const API_KEY = env.REDIS_API_KEY ?? ""
export async function deleteKeys(keys: string[], opts?: { fuzzy?: boolean }) {
const perf = performance.now()
const endpoint = getDeleteMultipleKeysEndpoint()
const [response, error] = await safeTry(
fetch(endpoint, {
method: "DELETE",
cache: "no-cache",
headers: {
"x-api-key": API_KEY,
},
body: JSON.stringify({ keys, fuzzy: opts?.fuzzy ?? false }),
signal: AbortSignal.timeout(10_000),
})
)
if (!response || !response.ok || error) {
if (response?.status !== 404) {
Sentry.captureException(
error ?? new Error("Unable to DELETE cachekeys"),
{
extra: {
cacheKeys: keys,
statusCode: response?.status,
statusText: response?.statusText,
},
}
)
}
return undefined
}
cacheLogger.debug(
`Deleted '${keys.join(", ")}' took ${(performance.now() - perf).toFixed(2)}ms`
)
}

View File

@@ -10,3 +10,13 @@ export function getCacheEndpoint(key: string) {
return url
}
export function getDeleteMultipleKeysEndpoint() {
if (!env.REDIS_API_HOST) {
throw new Error("REDIS_API_HOST is not set")
}
const url = new URL(`/api/cache/multiple`, env.REDIS_API_HOST)
return url
}

View File

@@ -0,0 +1,18 @@
import { cacheLogger } from "../../logger"
import { cacheMap } from "./cacheMap"
export async function deleteKeys(keys: string[], opts?: { fuzzy?: boolean }) {
cacheLogger.debug("Deleting keys", keys)
keys.forEach((key) => {
if (opts?.fuzzy) {
cacheMap.forEach((_, k) => {
if (k.includes(key)) {
cacheMap.delete(k)
}
})
return
}
cacheMap.delete(key)
})
}

View File

@@ -1,10 +1,11 @@
import { cacheOrGet } from "./cacheOrGet"
import { deleteKey } from "./deleteKey"
import { deleteKeys } from "./deleteKeys"
import { get } from "./get"
import { set } from "./set"
import type { DataCache } from "../../Cache"
export async function createInMemoryCache(): Promise<DataCache> {
return { type: "in-memory", cacheOrGet, deleteKey, get, set }
return { type: "in-memory", cacheOrGet, deleteKey, get, set, deleteKeys }
}