Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(github): long-term datasource caching #15653

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b5c4638
feat(github): Prolonged caching for tags and releases
zharinov May 19, 2022
4ffd3dc
WIP
zharinov May 19, 2022
a81da3e
WIP
zharinov May 19, 2022
bbf6ec0
Merge branch 'main' into feat/github-datasource-long-term-cache
zharinov May 19, 2022
17f3201
Fixes
zharinov May 19, 2022
6d827af
Integrate with changelog fetching
zharinov May 20, 2022
3301001
Add separate per-page parameter for cache update
zharinov May 20, 2022
ac3b304
WIP
zharinov May 20, 2022
458fc37
Add tests
zharinov May 22, 2022
bf08b84
Merge branch 'main' into feat/github-datasource-long-term-cache
zharinov May 22, 2022
7a6ceda
Fix tests
zharinov May 22, 2022
5071602
Merge branch 'main' of github.com:renovatebot/renovate into feat/gith…
zharinov May 23, 2022
c1490b5
Fix tests
zharinov May 23, 2022
b514b10
Fix coverage
zharinov May 23, 2022
58bffb4
Refactor/document
zharinov May 23, 2022
f1878f3
Merge branch 'main' of github.com:renovatebot/renovate into feat/gith…
zharinov May 23, 2022
5cb375f
Add more comments
zharinov May 24, 2022
5dd88e4
Fix
zharinov May 24, 2022
0918ec3
Change page limit from 10 to 1000
zharinov May 24, 2022
f273862
Merge branch 'main' of github.com:renovatebot/renovate into feat/gith…
zharinov May 24, 2022
4f0f900
Set max pages allowed to 100
zharinov May 25, 2022
d884c50
Merge branch 'main' into feat/github-datasource-long-term-cache
zharinov May 25, 2022
4060a22
Use spyOn for DateTime
zharinov May 25, 2022
7f9105e
Use spyOn
zharinov May 25, 2022
75e8a4e
Remove unused `updatedAt` field
zharinov May 25, 2022
34773fe
Move types
zharinov May 25, 2022
3e63209
Fix test
zharinov May 25, 2022
a9fcdd3
Merge branch 'main' into feat/github-datasource-long-term-cache
zharinov May 25, 2022
ba5e0e7
Add random delta for hard reset
zharinov May 25, 2022
e26de1d
Refactor a bit
zharinov May 25, 2022
607b28c
Simplify back-off logic
zharinov May 25, 2022
58e4ecd
Merge branch 'main' into feat/github-datasource-long-term-cache
rarkins May 26, 2022
baff301
Merge branch 'main' into feat/github-datasource-long-term-cache
zharinov May 26, 2022
587aa0d
Merge branch 'main' into feat/github-datasource-long-term-cache
zharinov May 31, 2022
4eb1195
Merge branch 'main' into feat/github-datasource-long-term-cache
zharinov Jun 2, 2022
b1b4a7b
Merge branch 'main' into feat/github-datasource-long-term-cache
rarkins Jun 3, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view

This file was deleted.

262 changes: 262 additions & 0 deletions lib/modules/datasource/github-releases/cache/cache-base.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import { DateTime } from 'luxon';
import { mocked } from '../../../../../test/util';
import * as _packageCache from '../../../../util/cache/package';
import {
GithubGraphqlResponse,
GithubHttp,
} from '../../../../util/http/github';
import { AbstractGithubDatasourceCache } from './cache-base';
import type { QueryResponse, StoredItemBase } from './types';

jest.mock('../../../../util/cache/package');
const packageCache = mocked(_packageCache);

interface FetchedItem {
name: string;
createdAt: string;
foo: string;
}

interface StoredItem extends StoredItemBase {
bar: string;
}

type GraphqlDataResponse = {
statusCode: 200;
headers: Record<string, string>;
body: GithubGraphqlResponse<QueryResponse<FetchedItem>>;
};

type GraphqlResponse = GraphqlDataResponse | Error;

class TestCache extends AbstractGithubDatasourceCache<StoredItem, FetchedItem> {
cacheNs = 'test-cache';
graphqlQuery = `query { ... }`;

coerceFetched({
name: version,
createdAt: releaseTimestamp,
foo: bar,
}: FetchedItem): StoredItem | null {
return version === 'invalid' ? null : { version, releaseTimestamp, bar };
}

isEquivalent({ bar: x }: StoredItem, { bar: y }: StoredItem): boolean {
return x === y;
}
}

function resp(items: FetchedItem[], hasNextPage = false): GraphqlDataResponse {
return {
statusCode: 200,
headers: {},
body: {
data: {
repository: {
payload: {
nodes: items,
pageInfo: {
hasNextPage,
endCursor: 'abc',
},
},
},
},
},
};
}

const sortItems = (items: StoredItem[]) =>
items.sort(({ releaseTimestamp: x }, { releaseTimestamp: y }) =>
x.localeCompare(y)
);

describe('modules/datasource/github-releases/cache/cache-base', () => {
const http = new GithubHttp();
const httpPostJson = jest.spyOn(GithubHttp.prototype, 'postJson');

const now = DateTime.local(2022, 6, 15, 18, 30, 30);
const t1 = now.minus({ days: 3 }).toISO();
const t2 = now.minus({ days: 2 }).toISO();
const t3 = now.minus({ days: 1 }).toISO();

let responses: GraphqlResponse[] = [];

beforeEach(() => {
responses = [];
jest.resetAllMocks();
jest.spyOn(DateTime, 'now').mockReturnValue(now);
httpPostJson.mockImplementation(() => {
const resp = responses.shift();
return resp instanceof Error
? Promise.reject(resp)
: Promise.resolve(resp);
});
});

it('performs pre-fetch', async () => {
responses = [
resp([{ name: 'v3', createdAt: t3, foo: 'ccc' }], true),
resp([{ name: 'v2', createdAt: t2, foo: 'bbb' }], true),
resp([{ name: 'v1', createdAt: t1, foo: 'aaa' }]),
];
const cache = new TestCache(http, { resetDeltaMinutes: 0 });

const res = await cache.getItems({ packageName: 'foo/bar' });

expect(sortItems(res)).toMatchObject([
{ version: 'v1', bar: 'aaa' },
{ version: 'v2', bar: 'bbb' },
{ version: 'v3', bar: 'ccc' },
]);
expect(packageCache.set).toHaveBeenCalledWith(
'test-cache',
'https://api.github.com/:foo:bar',
{
createdAt: now.toISO(),
updatedAt: now.toISO(),
items: {
v1: { bar: 'aaa', releaseTimestamp: t1, version: 'v1' },
v2: { bar: 'bbb', releaseTimestamp: t2, version: 'v2' },
v3: { bar: 'ccc', releaseTimestamp: t3, version: 'v3' },
},
},
7 * 24 * 60
);
});

it('filters out items being coerced to null', async () => {
responses = [
resp([{ name: 'v3', createdAt: t3, foo: 'ccc' }], true),
resp([{ name: 'invalid', createdAt: t3, foo: 'xxx' }], true),
resp([{ name: 'v2', createdAt: t2, foo: 'bbb' }], true),
resp([{ name: 'v1', createdAt: t1, foo: 'aaa' }]),
];
const cache = new TestCache(http, { resetDeltaMinutes: 0 });

const res = await cache.getItems({ packageName: 'foo/bar' });

expect(sortItems(res)).toMatchObject([
{ version: 'v1' },
{ version: 'v2' },
{ version: 'v3' },
]);
});

it('updates items', async () => {
packageCache.get.mockResolvedValueOnce({
items: {
v1: { version: 'v1', releaseTimestamp: t1, bar: 'aaa' },
v2: { version: 'v2', releaseTimestamp: t2, bar: 'bbb' },
v3: { version: 'v3', releaseTimestamp: t3, bar: 'ccc' },
},
createdAt: t3,
updatedAt: t3,
});

responses = [
resp([{ name: 'v3', createdAt: t3, foo: 'xxx' }], true),
resp([{ name: 'v2', createdAt: t2, foo: 'bbb' }], true),
resp([{ name: 'v1', createdAt: t1, foo: 'aaa' }]),
];
const cache = new TestCache(http, { resetDeltaMinutes: 0 });

const res = await cache.getItems({ packageName: 'foo/bar' });

expect(sortItems(res)).toMatchObject([
{ version: 'v1', bar: 'aaa' },
{ version: 'v2', bar: 'bbb' },
{ version: 'v3', bar: 'xxx' },
]);
expect(packageCache.set).toHaveBeenCalledWith(
'test-cache',
'https://api.github.com/:foo:bar',
{
createdAt: t3,
updatedAt: now.toISO(),
items: {
v1: { bar: 'aaa', releaseTimestamp: t1, version: 'v1' },
v2: { bar: 'bbb', releaseTimestamp: t2, version: 'v2' },
v3: { bar: 'xxx', releaseTimestamp: t3, version: 'v3' },
},
},
6 * 24 * 60
);
});

it('stops updating once stability period have passed', async () => {
packageCache.get.mockResolvedValueOnce({
items: {
v1: { version: 'v1', releaseTimestamp: t1, bar: 'aaa' },
v2: { version: 'v2', releaseTimestamp: t2, bar: 'bbb' },
v3: { version: 'v3', releaseTimestamp: t3, bar: 'ccc' },
},
createdAt: t3,
updatedAt: t3,
});
responses = [
resp([{ name: 'v3', createdAt: t3, foo: 'zzz' }], true),
resp([{ name: 'v2', createdAt: t2, foo: 'yyy' }], true),
resp([{ name: 'v1', createdAt: t1, foo: 'xxx' }]),
];
const cache = new TestCache(http, { unstableDays: 1.5 });

const res = await cache.getItems({ packageName: 'foo/bar' });

expect(sortItems(res)).toMatchObject([
{ version: 'v1', bar: 'aaa' },
{ version: 'v2', bar: 'bbb' },
{ version: 'v3', bar: 'zzz' },
]);
});

it('removes deleted items from cache', async () => {
packageCache.get.mockResolvedValueOnce({
items: {
v1: { version: 'v1', releaseTimestamp: t1, bar: 'aaa' },
v2: { version: 'v2', releaseTimestamp: t2, bar: 'bbb' },
v3: { version: 'v3', releaseTimestamp: t3, bar: 'ccc' },
},
createdAt: t3,
updatedAt: t3,
});
responses = [
resp([{ name: 'v3', createdAt: t3, foo: 'ccc' }], true),
resp([{ name: 'v1', createdAt: t1, foo: 'aaa' }]),
];
const cache = new TestCache(http, { resetDeltaMinutes: 0 });

const res = await cache.getItems({ packageName: 'foo/bar' });

expect(sortItems(res)).toMatchObject([
{ version: 'v1', bar: 'aaa' },
{ version: 'v3', bar: 'ccc' },
]);
});

it('returns cached values on server errors', async () => {
packageCache.get.mockResolvedValueOnce({
items: {
v1: { version: 'v1', releaseTimestamp: t1, bar: 'aaa' },
v2: { version: 'v2', releaseTimestamp: t2, bar: 'bbb' },
v3: { version: 'v3', releaseTimestamp: t3, bar: 'ccc' },
},
createdAt: t3,
updatedAt: t3,
});
responses = [
resp([{ name: 'v3', createdAt: t3, foo: 'zzz' }], true),
new Error('Unknown error'),
resp([{ name: 'v1', createdAt: t1, foo: 'xxx' }]),
];
const cache = new TestCache(http, { resetDeltaMinutes: 0 });

const res = await cache.getItems({ packageName: 'foo/bar' });

expect(sortItems(res)).toMatchObject([
{ version: 'v1', bar: 'aaa' },
{ version: 'v2', bar: 'bbb' },
{ version: 'v3', bar: 'ccc' },
]);
});
});