mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-25 06:51:11 +00:00
f64ec3975a
* refactor(datasource): separate registry URLs in cache keys with a colon Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io> Co-Authored-By: Claude Sonnet 5.5 <michael.kriese+claude-code@mend.io> * fix(datasource/go): separate the remaining cache key parts with a colon Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io> * refactor(datasource): build cache keys with a shared colon-joining helper Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io> * refactor: build all cache keys with buildCacheKey Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io> * refactor(http): keep GET package cache keys as the bare URL Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io> * refactor(cache): extend cache key helper to memory cache keys Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io> * refactor(datasource/go): prefix the goproxy cache keys with their purpose Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io> --------- Co-authored-by: Claude Opus 5.5 <michael.kriese+claude-code@mend.io>
158 lines
4.1 KiB
TypeScript
158 lines
4.1 KiB
TypeScript
import { isUndefined } from '@sindresorhus/is';
|
|
import { DateTime } from 'luxon';
|
|
import { GlobalConfig } from '../../../config/global.ts';
|
|
import { logger } from '../../../logger/index.ts';
|
|
import { acquireLock } from '../../mutex.ts';
|
|
import * as packageCache from './index.ts';
|
|
import { buildCacheKey } from './key.ts';
|
|
import { resolveTtlValues } from './ttl.ts';
|
|
import type { CachedRecord, PackageCacheNamespace } from './types.ts';
|
|
|
|
export interface CachedOptions {
|
|
/**
|
|
* The cache namespace.
|
|
*/
|
|
namespace: PackageCacheNamespace;
|
|
|
|
/**
|
|
* The cache key.
|
|
*/
|
|
key: string;
|
|
|
|
/**
|
|
* The TTL (or expiry) of the key in minutes.
|
|
* @default 30
|
|
*/
|
|
ttlMinutes?: number;
|
|
|
|
/**
|
|
* Whether caching is enabled for this call.
|
|
* When false, the function is called directly without caching.
|
|
* @default true
|
|
*/
|
|
cacheable?: boolean;
|
|
|
|
/**
|
|
* Whether a cache result is safe to use and store for this call.
|
|
* @default value !== undefined
|
|
*/
|
|
shouldCacheResult?: (value: unknown) => boolean;
|
|
|
|
/**
|
|
* Enable extended hard TTL for graceful degradation.
|
|
* When true and upstream errors occur, stale cached data is returned.
|
|
* @default false
|
|
*/
|
|
fallback?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Caches the result of an async function.
|
|
*
|
|
* @param options - Cache options
|
|
* @param fn - The async function to cache
|
|
* @returns The cached or fresh result
|
|
*/
|
|
export async function withCache<T>(
|
|
options: CachedOptions,
|
|
fn: () => T | Promise<T>,
|
|
): Promise<T> {
|
|
const {
|
|
namespace,
|
|
key,
|
|
ttlMinutes = 30,
|
|
cacheable = true,
|
|
fallback = false,
|
|
} = options;
|
|
|
|
const cachePrivatePackages = GlobalConfig.get('cachePrivatePackages');
|
|
const isCacheable = cachePrivatePackages || cacheable;
|
|
if (!isCacheable) {
|
|
return fn();
|
|
}
|
|
|
|
function defaultShouldCacheResult(_value: unknown): boolean {
|
|
return true;
|
|
}
|
|
const shouldCacheResult =
|
|
options.shouldCacheResult ?? defaultShouldCacheResult;
|
|
|
|
// istanbul ignore if -- TODO: add test #40625
|
|
if (!namespace || !key) {
|
|
return fn();
|
|
}
|
|
|
|
const cacheKey = buildCacheKey('cache-decorator', key);
|
|
|
|
// prevent concurrent processing and cache writes
|
|
const releaseLock = await acquireLock(cacheKey, namespace);
|
|
|
|
try {
|
|
const cachedRecord = await packageCache.get<CachedRecord>(
|
|
namespace,
|
|
cacheKey,
|
|
);
|
|
|
|
const { softTtlMinutes, hardTtlMinutes: resolvedHardTtl } =
|
|
resolveTtlValues(namespace, ttlMinutes);
|
|
|
|
// The separation between "soft" and "hard" TTL allows us to treat
|
|
// data as obsolete according to the "soft" TTL while physically storing it
|
|
// according to the "hard" TTL.
|
|
//
|
|
// This helps us return obsolete data in case of upstream server errors,
|
|
// which is more useful than throwing exceptions ourselves.
|
|
//
|
|
// The `fallback` option controls whether this extended TTL behavior is used.
|
|
// When false, the "soft" just equals the "hard" ttl.
|
|
const hardTtlMinutes = fallback ? resolvedHardTtl : softTtlMinutes;
|
|
|
|
let fallbackValue: unknown;
|
|
if (cachedRecord && shouldCacheResult(cachedRecord.value)) {
|
|
const now = DateTime.local();
|
|
const cachedAt = DateTime.fromISO(cachedRecord.cachedAt);
|
|
|
|
const softDeadline = cachedAt.plus({ minutes: softTtlMinutes });
|
|
if (now < softDeadline) {
|
|
return cachedRecord.value as T;
|
|
}
|
|
|
|
const hardDeadline = cachedAt.plus({ minutes: hardTtlMinutes });
|
|
if (now < hardDeadline) {
|
|
fallbackValue = cachedRecord.value;
|
|
}
|
|
}
|
|
|
|
let newValue: T;
|
|
try {
|
|
newValue = await fn();
|
|
} catch (err) {
|
|
if (!isUndefined(fallbackValue)) {
|
|
logger.debug(
|
|
{ err },
|
|
'Package cache: callback error, returning stale data',
|
|
);
|
|
return fallbackValue as T;
|
|
}
|
|
throw err;
|
|
}
|
|
|
|
if (!isUndefined(newValue) && shouldCacheResult(newValue)) {
|
|
const newRecord: CachedRecord = {
|
|
cachedAt: DateTime.local().toISO(),
|
|
value: newValue,
|
|
};
|
|
await packageCache.setWithRawTtl(
|
|
namespace,
|
|
cacheKey,
|
|
newRecord,
|
|
hardTtlMinutes,
|
|
);
|
|
}
|
|
|
|
return newValue;
|
|
} finally {
|
|
releaseLock();
|
|
}
|
|
}
|