Skip to content

Commit 0df4021

Browse files
committed
test: add handling for 404 errors in scale-down tests and improve error logging
1 parent 92710de commit 0df4021

File tree

2 files changed

+138
-20
lines changed

2 files changed

+138
-20
lines changed

lambdas/functions/control-plane/src/scale-runners/scale-down.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,106 @@ describe('Scale down runners', () => {
403403
expect(mockTerminateRunners).toHaveBeenCalledWith(orphanRunner.instanceId);
404404
});
405405

406+
it('Should handle 404 error when checking orphaned runner (JIT) - treat as orphaned', async () => {
407+
// arrange
408+
const orphanRunner = createRunnerTestData(
409+
'orphan-jit-404',
410+
type,
411+
MINIMUM_BOOT_TIME + 1,
412+
false,
413+
true,
414+
true, // should be terminated when 404
415+
undefined,
416+
1234567890,
417+
);
418+
const runners = [orphanRunner];
419+
420+
mockGitHubRunners([]);
421+
mockAwsRunners(runners);
422+
423+
// Mock 404 error response
424+
const error404 = new Error('Runner not found');
425+
(error404 as any).status = 404;
426+
427+
if (type === 'Repo') {
428+
mockOctokit.actions.getSelfHostedRunnerForRepo.mockRejectedValueOnce(error404);
429+
} else {
430+
mockOctokit.actions.getSelfHostedRunnerForOrg.mockRejectedValueOnce(error404);
431+
}
432+
433+
// act
434+
await scaleDown();
435+
436+
// assert - should terminate since 404 means runner doesn't exist on GitHub
437+
expect(mockTerminateRunners).toHaveBeenCalledWith(orphanRunner.instanceId);
438+
});
439+
440+
it('Should handle 404 error when checking runner busy state - treat as not busy', async () => {
441+
// arrange
442+
const runner = createRunnerTestData(
443+
'runner-404',
444+
type,
445+
MINIMUM_TIME_RUNNING_IN_MINUTES + 1,
446+
true,
447+
false,
448+
true, // should be terminated since not busy due to 404
449+
);
450+
const runners = [runner];
451+
452+
mockGitHubRunners(runners);
453+
mockAwsRunners(runners);
454+
455+
// Mock 404 error response for busy state check
456+
const error404 = new Error('Runner not found');
457+
(error404 as any).status = 404;
458+
459+
if (type === 'Repo') {
460+
mockOctokit.actions.getSelfHostedRunnerForRepo.mockRejectedValueOnce(error404);
461+
} else {
462+
mockOctokit.actions.getSelfHostedRunnerForOrg.mockRejectedValueOnce(error404);
463+
}
464+
465+
// act
466+
await scaleDown();
467+
468+
// assert - should terminate since 404 means runner is not busy
469+
checkTerminated(runners);
470+
});
471+
472+
it('Should re-throw non-404 errors when checking runner state', async () => {
473+
// arrange
474+
const orphanRunner = createRunnerTestData(
475+
'orphan-error',
476+
type,
477+
MINIMUM_BOOT_TIME + 1,
478+
false,
479+
true,
480+
false,
481+
undefined,
482+
1234567890,
483+
);
484+
const runners = [orphanRunner];
485+
486+
mockGitHubRunners([]);
487+
mockAwsRunners(runners);
488+
489+
// Mock non-404 error response
490+
const error500 = new Error('Internal server error');
491+
(error500 as any).status = 500;
492+
493+
if (type === 'Repo') {
494+
mockOctokit.actions.getSelfHostedRunnerForRepo.mockRejectedValueOnce(error500);
495+
} else {
496+
mockOctokit.actions.getSelfHostedRunnerForOrg.mockRejectedValueOnce(error500);
497+
}
498+
499+
// act & assert - should not throw because error handling is in terminateOrphan
500+
await expect(scaleDown()).resolves.not.toThrow();
501+
502+
// Should not terminate since the error was not a 404
503+
expect(mockTerminateRunners).not.toHaveBeenCalledWith(orphanRunner.instanceId);
504+
});
505+
406506
it(`Should ignore errors when termination orphan fails.`, async () => {
407507
// setup
408508
const orphanRunner = createRunnerTestData('orphan-1', type, MINIMUM_BOOT_TIME + 1, false, true, true);

lambdas/functions/control-plane/src/scale-runners/scale-down.ts

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,25 +55,37 @@ async function getGitHubSelfHostedRunnerState(
5555
client: Octokit,
5656
ec2runner: RunnerInfo,
5757
runnerId: number,
58-
): Promise<RunnerState> {
59-
const state =
60-
ec2runner.type === 'Org'
61-
? await client.actions.getSelfHostedRunnerForOrg({
62-
runner_id: runnerId,
63-
org: ec2runner.owner,
64-
})
65-
: await client.actions.getSelfHostedRunnerForRepo({
66-
runner_id: runnerId,
67-
owner: ec2runner.owner.split('/')[0],
68-
repo: ec2runner.owner.split('/')[1],
69-
});
70-
metricGitHubAppRateLimit(state.headers);
71-
72-
return state.data;
58+
): Promise<RunnerState | null> {
59+
try {
60+
const state =
61+
ec2runner.type === 'Org'
62+
? await client.actions.getSelfHostedRunnerForOrg({
63+
runner_id: runnerId,
64+
org: ec2runner.owner,
65+
})
66+
: await client.actions.getSelfHostedRunnerForRepo({
67+
runner_id: runnerId,
68+
owner: ec2runner.owner.split('/')[0],
69+
repo: ec2runner.owner.split('/')[1],
70+
});
71+
metricGitHubAppRateLimit(state.headers);
72+
73+
return state.data;
74+
} catch (error: any) {
75+
if (error.status === 404) {
76+
logger.info(`Runner '${ec2runner.instanceId}' with GitHub Runner ID '${runnerId}' not found on GitHub (404)`);
77+
return null;
78+
}
79+
throw error;
80+
}
7381
}
7482

7583
async function getGitHubRunnerBusyState(client: Octokit, ec2runner: RunnerInfo, runnerId: number): Promise<boolean> {
7684
const state = await getGitHubSelfHostedRunnerState(client, ec2runner, runnerId);
85+
if (state === null) {
86+
logger.info(`Runner '${ec2runner.instanceId}' - GitHub Runner ID '${runnerId}' - Not found on GitHub, treating as not busy`);
87+
return false;
88+
}
7789
logger.info(`Runner '${ec2runner.instanceId}' - GitHub Runner ID '${runnerId}' - Busy: ${state.busy}`);
7890
return state.busy;
7991
}
@@ -227,12 +239,18 @@ async function lastChanceCheckOrphanRunner(runner: RunnerList): Promise<boolean>
227239
const ec2Instance = runner as RunnerInfo;
228240
const state = await getGitHubSelfHostedRunnerState(client, ec2Instance, runnerId);
229241
let isOrphan = false;
230-
logger.debug(
231-
`Runner '${runner.instanceId}' is '${state.status}' and is currently '${state.busy ? 'busy' : 'idle'}'.`,
232-
);
233-
const isOfflineAndBusy = state.status === 'offline' && state.busy;
234-
if (isOfflineAndBusy) {
242+
243+
if (state === null) {
244+
logger.debug(`Runner '${runner.instanceId}' not found on GitHub, treating as orphaned.`);
235245
isOrphan = true;
246+
} else {
247+
logger.debug(
248+
`Runner '${runner.instanceId}' is '${state.status}' and is currently '${state.busy ? 'busy' : 'idle'}'.`,
249+
);
250+
const isOfflineAndBusy = state.status === 'offline' && state.busy;
251+
if (isOfflineAndBusy) {
252+
isOrphan = true;
253+
}
236254
}
237255
logger.info(`Runner '${runner.instanceId}' is judged to ${isOrphan ? 'be' : 'not be'} orphaned.`);
238256
return isOrphan;

0 commit comments

Comments
 (0)