Skip to content

Commit 7d8c576

Browse files
fix(mac): handle RunInstances scale errors (github-aws-runners#5183)
## Description Adds scale-error handling for the dedicated-host `RunInstances` path. `RunInstances` does not return fleet-style `Errors`, so the catch block now checks the thrown AWS SDK error `name` against configured `scaleErrors` and throws `ScaleError` to trigger retry behavior. The successful response path is split into `processRunInstanceResult` to validate returned instance IDs. ## Test Plan - Tested in my AWS environment with a dedicated-host scale-up where no compatible host capacity was available. - Confirmed `RunInstances` failed with `InsufficientHostCapacity`, the error was matched through `error.name`, and `ScaleError` was thrown so the request is retried. ## Related Issues N/A
1 parent 58fd1c2 commit 7d8c576

2 files changed

Lines changed: 66 additions & 8 deletions

File tree

lambdas/functions/control-plane/src/aws/runners.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1229,7 +1229,7 @@ describe('create runner with useDedicatedHost', () => {
12291229
mockEC2Client.on(RunInstancesCommand).resolves({ Instances: [] });
12301230

12311231
await expect(createRunner(createRunnerConfig(dedicatedHostRunnerConfig))).rejects.toThrow(
1232-
'RunInstances returned no instances for dedicated host.',
1232+
'RunInstances failed, no instance created.',
12331233
);
12341234
});
12351235

@@ -1239,6 +1239,37 @@ describe('create runner with useDedicatedHost', () => {
12391239
await expect(createRunner(createRunnerConfig(dedicatedHostRunnerConfig))).rejects.toThrow('EC2 error');
12401240
});
12411241

1242+
it('throws ScaleError when RunInstances fails with configured scale error', async () => {
1243+
const error = Object.assign(new Error('Insufficient capacity'), { name: 'InsufficientInstanceCapacity' });
1244+
mockEC2Client.on(RunInstancesCommand).rejects(error);
1245+
1246+
await expect(
1247+
createRunner({
1248+
...createRunnerConfig({
1249+
...dedicatedHostRunnerConfig,
1250+
scaleErrors: ['InsufficientInstanceCapacity'],
1251+
}),
1252+
numberOfRunners: 2,
1253+
}),
1254+
).rejects.toMatchObject({
1255+
name: 'ScaleError',
1256+
failedInstanceCount: 2,
1257+
});
1258+
});
1259+
1260+
it('throws error when RunInstances returns fewer instances', async () => {
1261+
mockEC2Client.on(RunInstancesCommand).resolves({
1262+
Instances: [{ InstanceId: 'i-dedicated-1' }],
1263+
});
1264+
1265+
await expect(
1266+
createRunner({
1267+
...createRunnerConfig(dedicatedHostRunnerConfig),
1268+
numberOfRunners: 2,
1269+
}),
1270+
).rejects.toThrow('RunInstances failed, no instance created.');
1271+
});
1272+
12421273
it('uses ami id override from ssm parameter', async () => {
12431274
const paramValue: GetParameterResult = {
12441275
Parameter: {

lambdas/functions/control-plane/src/aws/runners.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
DescribeInstancesCommand,
77
DescribeInstancesResult,
88
RunInstancesCommand,
9+
RunInstancesCommandOutput,
910
EC2Client,
1011
FleetLaunchTemplateOverridesRequest,
1112
Tag,
@@ -232,6 +233,27 @@ function countScaleErrors(errors: string[], scaleErrors: string[]): number {
232233
return errors.reduce((acc, e) => (scaleErrors.includes(e) ? acc + 1 : acc), 0);
233234
}
234235

236+
function processRunInstanceResult(
237+
result: RunInstancesCommandOutput,
238+
runnerParameters: Runners.RunnerInputParameters,
239+
): string[] {
240+
const instances = result.Instances?.map((i) => i.InstanceId!).filter(Boolean) || [];
241+
242+
if (instances.length === runnerParameters.numberOfRunners) {
243+
return instances;
244+
}
245+
246+
logger.warn(
247+
`${
248+
instances.length === 0 ? 'No' : instances.length + ' off ' + runnerParameters.numberOfRunners
249+
} instances created.`,
250+
{ data: result },
251+
);
252+
253+
logger.warn('RunInstances failed, error not recognized as scaling error.', { data: result });
254+
throw Error('RunInstances failed, no instance created.');
255+
}
256+
235257
async function getAmiIdOverride(runnerParameters: Runners.RunnerInputParameters): Promise<string | undefined> {
236258
if (!runnerParameters.amiIdSsmParameterName) {
237259
return undefined;
@@ -333,6 +355,7 @@ async function createInstancesWithRunInstances(
333355
tags.push({ Key: 'ghr:trace_id', Value: traceId! });
334356
}
335357

358+
let result: RunInstancesCommandOutput;
336359
try {
337360
if (runnerParameters.ec2instanceCriteria.targetCapacityType === 'spot') {
338361
throw new Error(
@@ -364,18 +387,22 @@ async function createInstancesWithRunInstances(
364387
});
365388

366389
logger.debug('RunInstances request payload.', { payload: runInstancesCommand.input });
367-
const result = await ec2Client.send(runInstancesCommand);
368-
const instanceIds = result.Instances?.map((i) => i.InstanceId!).filter(Boolean) || [];
369-
370-
if (instanceIds.length === 0) {
371-
throw new Error('RunInstances returned no instances for dedicated host.');
390+
result = await ec2Client.send(runInstancesCommand);
391+
} catch (e) {
392+
const errorName = (e as Error).name;
393+
if (errorName && runnerParameters.scaleErrors.includes(errorName)) {
394+
logger.warn('RunInstances failed with a scale error, ScaleError will be thrown to trigger retry.', {
395+
error: e as Error,
396+
errorName,
397+
});
398+
throw new ScaleError(runnerParameters.numberOfRunners);
372399
}
373400

374-
return instanceIds;
375-
} catch (e) {
376401
logger.warn('RunInstances request failed for dedicated host.', { error: e as Error });
377402
throw e;
378403
}
404+
405+
return processRunInstanceResult(result, runnerParameters);
379406
}
380407

381408
// If launchTime is undefined, this will return false

0 commit comments

Comments
 (0)