mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-31 09:41:46 +00:00
aae889a285
* fix(datasource/go): cache the publication time of each version The Go proxy defines that the `.info` API call is for a specific release version of a module, this file should never be modified after it's been created. However, because we previously had a 30m TTL on these entries, a repository that runs infrequently can lead to re-fetching data that doesn't ever update. We can fix this by introducing a separate cache namespace for the module's creation timestamp (`Time` in the `.info` API call). We can make sure that: - we always merge, not replace, the entries, as the `major < 2` filter in `_getReleases` means a run may only see a subset of a module's versions, and writing back only what we saw would evict the rest - we re-read immediately before writing, as `foo` and `foo/v2` are separate lookups which both walk the `v2` module, so one can overwrite the other's timestamps - the key is the proxy which served the data, minus any credentials, since a private Artifactory `GOPROXY` is a different universe of timestamps to `proxy.golang.org` - only the timestamps we successfully fetched are stored, so a proxy which serves no `Time` doesn't have that recorded for 100 days - it uses its own namespace, so that a `cacheTtlOverride` on the release cache doesn't drag the immutable data down with it Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io> * fixup! fix(datasource/go): cache the publication time of each version Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io> * fixup! fixup! fix(datasource/go): cache the publication time of each version Co-Authored-By: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io> --------- Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
99 lines
3.5 KiB
TypeScript
99 lines
3.5 KiB
TypeScript
import { GlobalConfig } from '../../../config/global.ts';
|
|
import * as packageCache from '../../../util/cache/package/index.ts';
|
|
import { acquireLock } from '../../../util/mutex.ts';
|
|
import type { Timestamp } from '../../../util/timestamp.ts';
|
|
import { parseUrl, trimTrailingSlash } from '../../../util/url.ts';
|
|
import { isPublicGoPackage } from './common.ts';
|
|
import { VersionTimestamps } from './schema.ts';
|
|
|
|
const cacheNamespace = 'datasource-go-proxy-timestamps';
|
|
|
|
// a module's `go.mod` should /never/ change after it's published. If going via the Go Proxy and the Go Checksum Database, a change in this value will result in build failures.
|
|
const ttlMinutes = 100 * 24 * 60;
|
|
|
|
/**
|
|
* The publication times we already know for a module, so that a run only fetches the `.info` file of a version it has never seen before.
|
|
*
|
|
* This is intentionally separate from the Go module's releases cache, as the current releases list changes (somewhat frequently), whereas the release timestamp for a given Go module shouldn't change after publication.
|
|
*/
|
|
export class GoVersionTimestampCache {
|
|
private isChanged = false;
|
|
private cacheKey: string | null;
|
|
private timestamps: Record<string, Timestamp>;
|
|
|
|
private constructor(
|
|
cacheKey: string | null,
|
|
timestamps: Record<string, Timestamp>,
|
|
) {
|
|
this.cacheKey = cacheKey;
|
|
this.timestamps = timestamps;
|
|
}
|
|
|
|
static async init(
|
|
baseUrl: string,
|
|
packageName: string,
|
|
): Promise<GoVersionTimestampCache> {
|
|
const cacheKey = getCacheKey(baseUrl, packageName);
|
|
if (!cacheKey) {
|
|
return new GoVersionTimestampCache(null, {});
|
|
}
|
|
|
|
return new GoVersionTimestampCache(cacheKey, await read(cacheKey));
|
|
}
|
|
|
|
get(version: string): Timestamp | undefined {
|
|
return this.timestamps[version];
|
|
}
|
|
|
|
set(version: string, timestamp: Timestamp): void {
|
|
this.timestamps[version] = timestamp;
|
|
this.isChanged = true;
|
|
}
|
|
|
|
async save(): Promise<void> {
|
|
if (!this.cacheKey || !this.isChanged) {
|
|
return;
|
|
}
|
|
|
|
// `foo` and `foo/v2` are separate lookups which both walk the `v2` module, so another one of them may have written its own timestamps while we were fetching ours. The lock only protects against concurrent writes within this process, not across separate Renovate processes/machines sharing the same cache backend.
|
|
const releaseLock = await acquireLock(this.cacheKey, cacheNamespace);
|
|
try {
|
|
const current = await read(this.cacheKey);
|
|
|
|
await packageCache.set(
|
|
cacheNamespace,
|
|
this.cacheKey,
|
|
{ ...current, ...this.timestamps },
|
|
ttlMinutes,
|
|
);
|
|
} finally {
|
|
releaseLock();
|
|
}
|
|
}
|
|
}
|
|
|
|
async function read(cacheKey: string): Promise<Record<string, Timestamp>> {
|
|
const cached = await packageCache.get<unknown>(cacheNamespace, cacheKey);
|
|
return cached ? VersionTimestamps.parse(cached) : {};
|
|
}
|
|
|
|
/**
|
|
* Timestamps are only shared between modules served by the same proxy, and only stored at all for modules we may cache.
|
|
*
|
|
* Any credentials in the proxy URL are left out, as the key is stored as-is by the cache backends.
|
|
*/
|
|
function getCacheKey(baseUrl: string, packageName: string): string | null {
|
|
const cachePrivatePackages = GlobalConfig.get('cachePrivatePackages');
|
|
if (!cachePrivatePackages && !isPublicGoPackage(packageName)) {
|
|
return null;
|
|
}
|
|
|
|
const parsedUrl = parseUrl(baseUrl);
|
|
if (!parsedUrl) {
|
|
return null;
|
|
}
|
|
|
|
const proxy = `${parsedUrl.origin}${trimTrailingSlash(parsedUrl.pathname)}`;
|
|
return `${proxy}@@${packageName}`;
|
|
}
|