import { createWritable } from './files.js' import { logger } from './logger.js' import * as redis from './redis.js' import { getRedisKey } from './util.js' import { Gauge } from 'prom-client' const cache = {} export const fileCacheSizeGauge = new Gauge({ name: 'file_cache_size', help: 'Number of entries in file cache' }) function logAndIgnoreError (err) { logger.error(err) } export function set (key, value, timeout) { logger.debug(`[Cache] Set ${key}`) if (cache[key] === value) return cache[key] = value if (timeout) setTimeout(expire, timeout * 1000, key) if (redis.isEnabled()) { if (timeout) return redis.client.set(key, value, 'EX', timeout).catch(logAndIgnoreError) return redis.client.set(key, value).catch(logAndIgnoreError) } } export async function clear () { for (const prop of Object.getOwnPropertyNames(cache)) { delete cache[prop] } fileCacheSizeGauge.reset() } export function get (key, fallback) { if (cache[key]) { logger.debug(`[Cache] Resolve from memory: ${key}`) return cache[key] } const promise = (async () => { if (redis.isEnabled()) { let result = await redis.client.get(key).catch(err => logger.error(err)) if (result) { logger.debug(`[Cache] Resolve from redis: ${key}`) result = JSON.parse(result) cache[key] = result redis.client.ttl(key).then(timeout => { if (timeout > 0) setTimeout(expire, timeout * 1000, key) }) return result } } if (!fallback) return const [fallbackResult, timeout] = await fallback() if (fallbackResult) { logger.debug(`[Cache] Found a getter for: ${key}`) set(key, JSON.stringify(fallbackResult), timeout) // overwrite local cache again, as set() will store the stringified version cache[key] = fallbackResult } return fallbackResult })() cache[key] = promise return promise } export function getFile ({ name, version }, fallback) { const key = getRedisKey({ version, name }) // try to get the file synchronously. const data = cache[key] if (data) { logger.debug(`[Cache] Resolve file from memory: ${key}`) return data } // if synchronously does not work, store the async promise for further requests const promise = (async () => { const bodyKey = getRedisKey({ version, name: `${name}:body` }) const metaKey = getRedisKey({ version, name: `${name}:meta` }) if (redis.isEnabled()) { const [body, meta = '{}'] = await Promise.all([ redis.client.getBuffer(bodyKey), redis.client.get(metaKey) ]).catch((err) => { logger.error(`[Cache] could not access redis: ${err}`) return [] }) if (body) { logger.debug(`[Cache] Resolve file from redis: ${key}`) fileCacheSizeGauge.inc() return (cache[key] = { body, ...JSON.parse(meta) }) } } const dataFromServer = await fallback({ version, name }).catch(err => { if (err.status !== 404) delete cache[key] throw err }) if (redis.isEnabled()) { logger.debug(`[Cache] Store file in redis: ${key}`) const { body, ...rest } = dataFromServer redis.client.set(bodyKey, createWritable(body)).catch(err => logger.error(`[Cache] could not store ${bodyKey}: ${err}`)) redis.client.set(metaKey, JSON.stringify(rest)).catch(err => logger.error(`[Cache] could not store ${metaKey}: ${err}`)) } // overwrite cache with synchronous data logger.debug(`[Cache] Store file in memory: ${key}`) fileCacheSizeGauge.inc() return (cache[key] = dataFromServer) })() // temporary set to promise cache[key] = promise return promise } function expire (key) { logger.debug(`[Cache] Key ${key} has expired.`) delete cache[key] }