mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-01 10:09:50 +00:00
f932d52fbd
* feat(platform): get all branch update dates in a more performant manner When Renovate is working on a repository with many /potential/ branches, as well as some actually created branches, it's possible for the calculation for `commitHourlyLimit` to end up taking significant time. This can lead to a less performant job generally, but can sometimes lead to the Renovate job failing to complete, and then timing out when executed by a job runner. To handle this, we can instead use use `git for-each-ref` to look up each Git ref (remotely) and request the commit date, which leads to a much more performant lookup. Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io> * fixup! feat(platform): get all branch update dates in a more performant manner Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io> * fixup! Merge remote-tracking branch 'origin/HEAD' into wip/commit-hourly-slow --------- Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>
188 lines
6.3 KiB
TypeScript
188 lines
6.3 KiB
TypeScript
import { DateTime } from 'luxon';
|
|
import type { RenovateConfig } from '~test/util.ts';
|
|
import { partial, platform, scm } from '~test/util.ts';
|
|
import type { Pr } from '../../../modules/platform/types.ts';
|
|
import * as _repositoryCache from '../../../util/cache/repository/index.ts';
|
|
import type {
|
|
BranchCache,
|
|
RepoCacheData,
|
|
} from '../../../util/cache/repository/types.ts';
|
|
import type { BranchConfig } from '../../types.ts';
|
|
import * as limits from './limits.ts';
|
|
|
|
vi.mock('../../../util/cache/repository/index.ts');
|
|
const repositoryCache = vi.mocked(_repositoryCache);
|
|
|
|
let config: RenovateConfig;
|
|
|
|
beforeEach(() => {
|
|
config = partial<RenovateConfig>({
|
|
branchPrefix: 'foo/',
|
|
onboardingBranch: 'bar/configure',
|
|
prHourlyLimit: 2,
|
|
prConcurrentLimit: 10,
|
|
branchConcurrentLimit: null,
|
|
});
|
|
repositoryCache.getCache.mockReturnValue({});
|
|
});
|
|
|
|
describe('workers/repository/process/limits', () => {
|
|
describe('getPrHourlyCount()', () => {
|
|
it('calculates hourly pr count', async () => {
|
|
const time = DateTime.local();
|
|
const createdAt = time.toISO();
|
|
platform.getPrList.mockResolvedValueOnce([
|
|
{ createdAt, sourceBranch: 'foo/test-1' },
|
|
{ createdAt, sourceBranch: 'foo/test-2' },
|
|
{ createdAt, sourceBranch: 'foo/test-3' },
|
|
{
|
|
createdAt: time.minus({ hours: 1 }).toISO(),
|
|
sourceBranch: 'foo/test-4',
|
|
},
|
|
{ createdAt, sourceBranch: 'bar/configure' },
|
|
{ createdAt, sourceBranch: 'baz/test' },
|
|
] as never);
|
|
const res = await limits.getPrHourlyCount(config);
|
|
expect(res).toBe(3);
|
|
});
|
|
|
|
it('returns zero if errored', async () => {
|
|
platform.getPrList.mockRejectedValue('Unknown error');
|
|
const res = await limits.getPrHourlyCount(config);
|
|
expect(res).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('getCommitHourlyCount()', () => {
|
|
it('calculates hourly commit count from SCM in a single batched call', async () => {
|
|
const time = DateTime.local();
|
|
scm.getAllBranchUpdateDates.mockResolvedValueOnce({
|
|
'foo/test-1': time,
|
|
'foo/test-2': time,
|
|
'foo/test-3': time.minus({ hours: 1 }),
|
|
'foo/test-4': time,
|
|
});
|
|
const res = await limits.getCommitsHourlyCount([
|
|
partial<BranchConfig>({ branchName: 'foo/test-1' }),
|
|
partial<BranchConfig>({ branchName: 'foo/test-2' }),
|
|
partial<BranchConfig>({ branchName: 'foo/test-3' }),
|
|
partial<BranchConfig>({ branchName: 'foo/test-4' }),
|
|
]);
|
|
expect(res).toBe(3);
|
|
expect(scm.getAllBranchUpdateDates).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('uses cache when available and falls back to a single batched SCM call when missing', async () => {
|
|
const currentTime = DateTime.utc();
|
|
const oldTime = currentTime.minus({ hours: 2 });
|
|
|
|
// Mock cache with mixed data: some cached, some missing
|
|
repositoryCache.getCache.mockReturnValue(
|
|
partial<RepoCacheData>({
|
|
branches: [
|
|
partial<BranchCache>({
|
|
branchName: 'foo/test-1',
|
|
commitTimestamp: currentTime.toISO(),
|
|
}),
|
|
partial<BranchCache>({
|
|
branchName: 'foo/test-2',
|
|
commitTimestamp: oldTime.toISO(),
|
|
}),
|
|
partial<BranchCache>({
|
|
branchName: 'foo/test-3',
|
|
// no commitTimestamp - will fall back to SCM
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
|
|
scm.getAllBranchUpdateDates.mockResolvedValueOnce({
|
|
'foo/test-3': currentTime,
|
|
});
|
|
|
|
const res = await limits.getCommitsHourlyCount([
|
|
partial<BranchConfig>({ branchName: 'foo/test-1' }),
|
|
partial<BranchConfig>({ branchName: 'foo/test-2' }),
|
|
partial<BranchConfig>({ branchName: 'foo/test-3' }),
|
|
]);
|
|
|
|
// Should count 2 (test-1 from cache and test-3 from SCM are in current hour)
|
|
expect(res).toBe(2);
|
|
// Should call the batched SCM lookup only once, regardless of how many branches are missing from the cache
|
|
expect(scm.getAllBranchUpdateDates).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('does not call SCM at all when every branch is already cached', async () => {
|
|
const currentTime = DateTime.utc();
|
|
|
|
repositoryCache.getCache.mockReturnValue(
|
|
partial<RepoCacheData>({
|
|
branches: [
|
|
partial<BranchCache>({
|
|
branchName: 'foo/test-1',
|
|
commitTimestamp: currentTime.toISO(),
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
|
|
const res = await limits.getCommitsHourlyCount([
|
|
partial<BranchConfig>({ branchName: 'foo/test-1' }),
|
|
]);
|
|
|
|
expect(res).toBe(1);
|
|
expect(scm.getAllBranchUpdateDates).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('treats a branch missing from the batched result as having no commit this hour', async () => {
|
|
scm.getAllBranchUpdateDates.mockResolvedValueOnce({});
|
|
const res = await limits.getCommitsHourlyCount([
|
|
partial<BranchConfig>({ branchName: 'foo/test-1' }),
|
|
]);
|
|
expect(res).toBe(0);
|
|
});
|
|
|
|
it('returns zero if errored', async () => {
|
|
scm.getAllBranchUpdateDates.mockRejectedValue('Unknown error');
|
|
const res = await limits.getCommitsHourlyCount([
|
|
{ branchName: 'foo/test-1' },
|
|
] as never);
|
|
expect(res).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('getConcurrentPrsCount()', () => {
|
|
it('calculates concurrent prs present', async () => {
|
|
platform.getBranchPr.mockImplementation((branchName) =>
|
|
branchName
|
|
? Promise.resolve(
|
|
partial<Pr>({
|
|
sourceBranch: branchName,
|
|
state: 'open',
|
|
}),
|
|
)
|
|
: Promise.reject(new Error('some error')),
|
|
);
|
|
const branches: BranchConfig[] = [
|
|
{ branchName: 'test' },
|
|
{ branchName: null },
|
|
] as never;
|
|
const res = await limits.getConcurrentPrsCount(config, branches);
|
|
expect(res).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('getConcurrentBranchesCount()', () => {
|
|
it('calculates concurrent branches present', async () => {
|
|
scm.branchExists.mockImplementation((branchName) =>
|
|
branchName ? Promise.resolve(true) : Promise.resolve(false),
|
|
);
|
|
const res = await limits.getConcurrentBranchesCount([
|
|
{ branchName: 'foo' },
|
|
{ branchName: null },
|
|
] as never);
|
|
expect(res).toBe(1);
|
|
});
|
|
});
|
|
});
|