mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-12 23:16:26 +00:00
feat(cache): etag caching for github GET (#26788)
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
This commit is contained in:
parent
d773b5aab5
commit
23a334c7e3
6 changed files with 88 additions and 6 deletions
|
@ -35,7 +35,10 @@ import type {
|
||||||
import * as hostRules from '../../../util/host-rules';
|
import * as hostRules from '../../../util/host-rules';
|
||||||
import * as githubHttp from '../../../util/http/github';
|
import * as githubHttp from '../../../util/http/github';
|
||||||
import type { GithubHttpOptions } from '../../../util/http/github';
|
import type { GithubHttpOptions } from '../../../util/http/github';
|
||||||
import type { HttpResponse } from '../../../util/http/types';
|
import type {
|
||||||
|
HttpResponse,
|
||||||
|
InternalHttpOptions,
|
||||||
|
} from '../../../util/http/types';
|
||||||
import { coerceObject } from '../../../util/object';
|
import { coerceObject } from '../../../util/object';
|
||||||
import { regEx } from '../../../util/regex';
|
import { regEx } from '../../../util/regex';
|
||||||
import { sanitize } from '../../../util/sanitize';
|
import { sanitize } from '../../../util/sanitize';
|
||||||
|
@ -316,11 +319,15 @@ export async function getRawFile(
|
||||||
branchOrTag?: string,
|
branchOrTag?: string,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const repo = repoName ?? config.repository;
|
const repo = repoName ?? config.repository;
|
||||||
|
const httpOptions: InternalHttpOptions = {
|
||||||
|
// Only cache response if it's from the same repo
|
||||||
|
repoCache: repo === config.repository,
|
||||||
|
};
|
||||||
let url = `repos/${repo}/contents/${fileName}`;
|
let url = `repos/${repo}/contents/${fileName}`;
|
||||||
if (branchOrTag) {
|
if (branchOrTag) {
|
||||||
url += `?ref=` + branchOrTag;
|
url += `?ref=` + branchOrTag;
|
||||||
}
|
}
|
||||||
const res = await githubApi.getJson<{ content: string }>(url);
|
const res = await githubApi.getJson<{ content: string }>(url, httpOptions);
|
||||||
const buf = res.body.content;
|
const buf = res.body.content;
|
||||||
const str = fromBase64(buf);
|
const str = fromBase64(buf);
|
||||||
return str;
|
return str;
|
||||||
|
@ -1220,7 +1227,7 @@ export async function getIssue(
|
||||||
const issueBody = (
|
const issueBody = (
|
||||||
await githubApi.getJson<{ body: string }>(
|
await githubApi.getJson<{ body: string }>(
|
||||||
`repos/${config.parentRepo ?? config.repository}/issues/${number}`,
|
`repos/${config.parentRepo ?? config.repository}/issues/${number}`,
|
||||||
{ memCache: useCache },
|
{ memCache: useCache, repoCache: true },
|
||||||
)
|
)
|
||||||
).body.body;
|
).body.body;
|
||||||
return {
|
return {
|
||||||
|
@ -1306,6 +1313,7 @@ export async function ensureIssue({
|
||||||
`repos/${config.parentRepo ?? config.repository}/issues/${
|
`repos/${config.parentRepo ?? config.repository}/issues/${
|
||||||
issue.number
|
issue.number
|
||||||
}`,
|
}`,
|
||||||
|
{ repoCache: true },
|
||||||
)
|
)
|
||||||
).body.body;
|
).body.body;
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -67,6 +67,7 @@ export async function getPrCache(
|
||||||
if (pageIdx === 1 && isInitial) {
|
if (pageIdx === 1 && isInitial) {
|
||||||
// Speed up initial fetch
|
// Speed up initial fetch
|
||||||
opts.paginate = true;
|
opts.paginate = true;
|
||||||
|
opts.repoCache = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const perPage = isInitial ? 100 : 20;
|
const perPage = isInitial ? 100 : 20;
|
||||||
|
|
8
lib/util/cache/repository/types.ts
vendored
8
lib/util/cache/repository/types.ts
vendored
|
@ -8,6 +8,7 @@ import type { BitbucketPrCacheData } from '../../../modules/platform/bitbucket/t
|
||||||
import type { GiteaPrCacheData } from '../../../modules/platform/gitea/types';
|
import type { GiteaPrCacheData } from '../../../modules/platform/gitea/types';
|
||||||
import type { RepoInitConfig } from '../../../workers/repository/init/types';
|
import type { RepoInitConfig } from '../../../workers/repository/init/types';
|
||||||
import type { PrBlockedBy } from '../../../workers/types';
|
import type { PrBlockedBy } from '../../../workers/types';
|
||||||
|
import type { HttpResponse } from '../../http/types';
|
||||||
|
|
||||||
export interface BaseBranchCache {
|
export interface BaseBranchCache {
|
||||||
sha: string; // branch commit sha
|
sha: string; // branch commit sha
|
||||||
|
@ -124,8 +125,15 @@ export interface BranchCache {
|
||||||
result?: string;
|
result?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HttpCache {
|
||||||
|
etag: string;
|
||||||
|
httpResponse: HttpResponse<unknown>;
|
||||||
|
timeStamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RepoCacheData {
|
export interface RepoCacheData {
|
||||||
configFileName?: string;
|
configFileName?: string;
|
||||||
|
httpCache?: Record<string, HttpCache>;
|
||||||
semanticCommits?: 'enabled' | 'disabled';
|
semanticCommits?: 'enabled' | 'disabled';
|
||||||
branches?: BranchCache[];
|
branches?: BranchCache[];
|
||||||
init?: RepoInitConfig;
|
init?: RepoInitConfig;
|
||||||
|
|
|
@ -76,13 +76,38 @@ describe('util/http/index', () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.get('/')
|
.get('/')
|
||||||
.reply(200, '{ "test": true }');
|
.reply(200, '{ "test": true }', { etag: 'abc123' });
|
||||||
expect(await http.getJson('http://renovate.com')).toEqual({
|
expect(
|
||||||
|
await http.getJson('http://renovate.com', { repoCache: true }),
|
||||||
|
).toEqual({
|
||||||
authorization: false,
|
authorization: false,
|
||||||
body: {
|
body: {
|
||||||
test: true,
|
test: true,
|
||||||
},
|
},
|
||||||
headers: {},
|
headers: {
|
||||||
|
etag: 'abc123',
|
||||||
|
},
|
||||||
|
statusCode: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
httpMock
|
||||||
|
.scope(baseUrl, {
|
||||||
|
reqheaders: {
|
||||||
|
accept: 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.get('/')
|
||||||
|
.reply(304, '', { etag: 'abc123' });
|
||||||
|
expect(
|
||||||
|
await http.getJson('http://renovate.com', { repoCache: true }),
|
||||||
|
).toEqual({
|
||||||
|
authorization: false,
|
||||||
|
body: {
|
||||||
|
test: true,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
etag: 'abc123',
|
||||||
|
},
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { pkg } from '../../expose.cjs';
|
||||||
import { logger } from '../../logger';
|
import { logger } from '../../logger';
|
||||||
import { ExternalHostError } from '../../types/errors/external-host-error';
|
import { ExternalHostError } from '../../types/errors/external-host-error';
|
||||||
import * as memCache from '../cache/memory';
|
import * as memCache from '../cache/memory';
|
||||||
|
import { getCache } from '../cache/repository';
|
||||||
import { clone } from '../clone';
|
import { clone } from '../clone';
|
||||||
import { hash } from '../hash';
|
import { hash } from '../hash';
|
||||||
import { type AsyncResult, Result } from '../result';
|
import { type AsyncResult, Result } from '../result';
|
||||||
|
@ -163,6 +164,8 @@ export class Http<Opts extends HttpOptions = HttpOptions> {
|
||||||
httpOptions,
|
httpOptions,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
logger.trace(`HTTP request: ${options.method.toUpperCase()} ${url}`);
|
||||||
|
|
||||||
const etagCache =
|
const etagCache =
|
||||||
httpOptions.etagCache && options.method === 'get'
|
httpOptions.etagCache && options.method === 'get'
|
||||||
? httpOptions.etagCache
|
? httpOptions.etagCache
|
||||||
|
@ -210,6 +213,16 @@ export class Http<Opts extends HttpOptions = HttpOptions> {
|
||||||
|
|
||||||
// istanbul ignore else: no cache tests
|
// istanbul ignore else: no cache tests
|
||||||
if (!resPromise) {
|
if (!resPromise) {
|
||||||
|
if (httpOptions.repoCache) {
|
||||||
|
const cachedEtag = getCache().httpCache?.[url]?.etag;
|
||||||
|
if (cachedEtag) {
|
||||||
|
logger.debug(`Using cached etag for ${url}`);
|
||||||
|
options.headers = {
|
||||||
|
...options.headers,
|
||||||
|
'If-None-Match': cachedEtag,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const httpTask: GotTask<T> = () => {
|
const httpTask: GotTask<T> = () => {
|
||||||
const queueDuration = Date.now() - startTime;
|
const queueDuration = Date.now() - startTime;
|
||||||
|
@ -243,6 +256,31 @@ export class Http<Opts extends HttpOptions = HttpOptions> {
|
||||||
const deepCopyNeeded = !!memCacheKey && res.statusCode !== 304;
|
const deepCopyNeeded = !!memCacheKey && res.statusCode !== 304;
|
||||||
const resCopy = copyResponse(res, deepCopyNeeded);
|
const resCopy = copyResponse(res, deepCopyNeeded);
|
||||||
resCopy.authorization = !!options?.headers?.authorization;
|
resCopy.authorization = !!options?.headers?.authorization;
|
||||||
|
if (httpOptions.repoCache) {
|
||||||
|
const cache = getCache();
|
||||||
|
cache.httpCache ??= {};
|
||||||
|
if (resCopy.statusCode === 200 && resCopy.headers?.etag) {
|
||||||
|
logger.debug(
|
||||||
|
`Saving response to cache: ${url} with etag ${resCopy.headers.etag}`,
|
||||||
|
);
|
||||||
|
cache.httpCache[url] = {
|
||||||
|
etag: resCopy.headers.etag,
|
||||||
|
httpResponse: copyResponse(res, deepCopyNeeded),
|
||||||
|
timeStamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (resCopy.statusCode === 304 && cache.httpCache[url]?.httpResponse) {
|
||||||
|
logger.debug(
|
||||||
|
`Using cached response: ${url} with etag ${resCopy.headers.etag} from ${cache.httpCache[url].timeStamp}`,
|
||||||
|
);
|
||||||
|
const cacheCopy = copyResponse(
|
||||||
|
cache.httpCache[url].httpResponse,
|
||||||
|
deepCopyNeeded,
|
||||||
|
);
|
||||||
|
cacheCopy.authorization = !!options?.headers?.authorization;
|
||||||
|
return cacheCopy as HttpResponse<T>;
|
||||||
|
}
|
||||||
|
}
|
||||||
return resCopy;
|
return resCopy;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const { abortOnError, abortIgnoreStatusCodes } = options;
|
const { abortOnError, abortIgnoreStatusCodes } = options;
|
||||||
|
|
|
@ -65,6 +65,7 @@ export interface HttpOptions {
|
||||||
|
|
||||||
token?: string;
|
token?: string;
|
||||||
memCache?: boolean;
|
memCache?: boolean;
|
||||||
|
repoCache?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EtagCache<T = any> {
|
export interface EtagCache<T = any> {
|
||||||
|
@ -81,6 +82,7 @@ export interface InternalHttpOptions extends HttpOptions {
|
||||||
responseType?: 'json' | 'buffer';
|
responseType?: 'json' | 'buffer';
|
||||||
method?: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head';
|
method?: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head';
|
||||||
parseJson?: ParseJsonFunction;
|
parseJson?: ParseJsonFunction;
|
||||||
|
repoCache?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HttpHeaders extends IncomingHttpHeaders {
|
export interface HttpHeaders extends IncomingHttpHeaders {
|
||||||
|
|
Loading…
Reference in a new issue