Skip to content

Commit 1d42c41

Browse files
committed
Reactor logic, add tests for booting and orphan runners
1 parent d1c43df commit 1d42c41

File tree

2 files changed

+114
-55
lines changed

2 files changed

+114
-55
lines changed

modules/runners/lambdas/runners/src/pool/pool.test.ts

+84-47
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { Octokit } from '@octokit/rest';
22
import { mocked } from 'jest-mock';
3+
import moment from 'moment-timezone';
34
import nock from 'nock';
45

56
import { listEC2Runners } from '../aws/runners';
67
import * as ghAuth from '../gh-auth/gh-auth';
7-
import * as scale from '../scale-runners/scale-up';
8+
import { createRunners } from '../scale-runners/scale-up';
89
import { adjust } from './pool';
910

1011
const mockOctokit = {
@@ -24,6 +25,7 @@ jest.mock('@octokit/rest', () => ({
2425

2526
jest.mock('./../aws/runners');
2627
jest.mock('./../gh-auth/gh-auth');
28+
jest.mock('./../scale-runners/scale-up');
2729

2830
const mocktokit = Octokit as jest.MockedClass<typeof Octokit>;
2931
const mockedAppAuth = mocked(ghAuth.createGithubAppAuth, {
@@ -36,6 +38,28 @@ const mockListRunners = mocked(listEC2Runners);
3638
const cleanEnv = process.env;
3739

3840
const ORG = 'my-org';
41+
const MINIMUM_TIME_RUNNING = 15;
42+
43+
const ec2InstancesRegistered = [
44+
{
45+
instanceId: 'i-1',
46+
launchTime: new Date(),
47+
type: 'Org',
48+
owner: ORG,
49+
},
50+
{
51+
instanceId: 'i-2',
52+
launchTime: new Date(),
53+
type: 'Org',
54+
owner: ORG,
55+
},
56+
{
57+
instanceId: 'i-3',
58+
launchTime: new Date(),
59+
type: 'Org',
60+
owner: ORG,
61+
},
62+
];
3963

4064
beforeEach(() => {
4165
nock.disableNetConnect();
@@ -54,6 +78,7 @@ beforeEach(() => {
5478
process.env.INSTANCE_TYPES = 'm5.large';
5579
process.env.INSTANCE_TARGET_CAPACITY_TYPE = 'spot';
5680
process.env.RUNNER_OWNER = ORG;
81+
process.env.RUNNER_BOOT_TIME_IN_MINUTES = MINIMUM_TIME_RUNNING.toString();
5782

5883
const mockTokenReturnValue = {
5984
data: {
@@ -87,44 +112,9 @@ beforeEach(() => {
87112
busy: false,
88113
labels: [],
89114
},
90-
{
91-
id: 11,
92-
name: 'j-1', // some runner of another env
93-
os: 'linux',
94-
status: 'online',
95-
busy: false,
96-
labels: [],
97-
},
98-
{
99-
id: 12,
100-
name: 'j-2', // some runner of another env
101-
os: 'linux',
102-
status: 'online',
103-
busy: true,
104-
labels: [],
105-
},
106115
]);
107116

108-
mockListRunners.mockImplementation(async () => [
109-
{
110-
instanceId: 'i-1',
111-
launchTime: new Date(),
112-
type: 'Org',
113-
owner: ORG,
114-
},
115-
{
116-
instanceId: 'i-2',
117-
launchTime: new Date(),
118-
type: 'Org',
119-
owner: ORG,
120-
},
121-
{
122-
instanceId: 'i-3',
123-
launchTime: new Date(),
124-
type: 'Org',
125-
owner: ORG,
126-
},
127-
]);
117+
mockListRunners.mockImplementation(async () => ec2InstancesRegistered);
128118

129119
const mockInstallationIdReturnValueOrgs = {
130120
data: {
@@ -155,16 +145,64 @@ beforeEach(() => {
155145

156146
describe('Test simple pool.', () => {
157147
describe('With GitHub Cloud', () => {
158-
it('Top up pool with pool size 2.', async () => {
159-
const spy = jest.spyOn(scale, 'createRunners');
160-
await expect(adjust({ poolSize: 2 })).resolves;
161-
expect(spy).toBeCalled;
148+
it('Top up pool with pool size 2 registered.', async () => {
149+
await expect(await adjust({ poolSize: 3 })).resolves;
150+
expect(createRunners).toHaveBeenCalledTimes(1);
162151
});
163152

164153
it('Should not top up if pool size is reached.', async () => {
165-
const spy = jest.spyOn(scale, 'createRunners');
166-
await expect(adjust({ poolSize: 1 })).resolves;
167-
expect(spy).not.toHaveBeenCalled;
154+
await expect(await adjust({ poolSize: 1 })).resolves;
155+
expect(createRunners).not.toHaveBeenCalled();
156+
});
157+
158+
it('Should top up if pool size is not reached including a booting instance.', async () => {
159+
mockListRunners.mockImplementation(async () => [
160+
...ec2InstancesRegistered,
161+
{
162+
instanceId: 'i-4-still-booting',
163+
launchTime: moment(new Date())
164+
.subtract(MINIMUM_TIME_RUNNING - 3, 'minutes')
165+
.toDate(),
166+
type: 'Org',
167+
owner: ORG,
168+
},
169+
{
170+
instanceId: 'i-5-orphan',
171+
launchTime: moment(new Date())
172+
.subtract(MINIMUM_TIME_RUNNING + 3, 'minutes')
173+
.toDate(),
174+
type: 'Org',
175+
owner: ORG,
176+
},
177+
]);
178+
179+
await expect(await adjust({ poolSize: 5 })).resolves;
180+
expect(createRunners).toHaveBeenCalledTimes(1);
181+
});
182+
183+
it('Should not top up if pool size is reached including a booting instance.', async () => {
184+
mockListRunners.mockImplementation(async () => [
185+
...ec2InstancesRegistered,
186+
{
187+
instanceId: 'i-4-still-booting',
188+
launchTime: moment(new Date())
189+
.subtract(MINIMUM_TIME_RUNNING - 3, 'minutes')
190+
.toDate(),
191+
type: 'Org',
192+
owner: ORG,
193+
},
194+
{
195+
instanceId: 'i-5-orphan',
196+
launchTime: moment(new Date())
197+
.subtract(MINIMUM_TIME_RUNNING + 3, 'minutes')
198+
.toDate(),
199+
type: 'Org',
200+
owner: ORG,
201+
},
202+
]);
203+
204+
await expect(await adjust({ poolSize: 2 })).resolves;
205+
expect(createRunners).not.toHaveBeenCalled();
168206
});
169207
});
170208

@@ -174,9 +212,8 @@ describe('Test simple pool.', () => {
174212
});
175213

176214
it('Top up if the pool size is set to 5', async () => {
177-
const spy = jest.spyOn(scale, 'createRunners');
178-
await expect(adjust({ poolSize: 5 })).resolves;
179-
expect(spy).toBeCalled;
215+
await expect(await adjust({ poolSize: 3 })).resolves;
216+
expect(createRunners).toHaveBeenCalledTimes(1);
180217
});
181218
});
182219
});

modules/runners/lambdas/runners/src/pool/pool.ts

+30-8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ export interface PoolEvent {
1212
poolSize: number;
1313
}
1414

15+
interface RunnerStatus {
16+
busy: boolean;
17+
status: string;
18+
}
19+
1520
export async function adjust(event: PoolEvent): Promise<void> {
1621
logger.info(`Checking current pool size against pool of size: ${event.poolSize}`);
1722
const runnerExtraLabels = process.env.RUNNER_EXTRA_LABELS;
@@ -45,7 +50,10 @@ export async function adjust(event: PoolEvent): Promise<void> {
4550
per_page: 100,
4651
},
4752
);
48-
const githubIdleRunners = runners.filter((r) => !r.busy && r.status === 'online');
53+
const runnerStatus = new Map<string, RunnerStatus>();
54+
for (const runner of runners) {
55+
runnerStatus.set(runner.name, { busy: runner.busy, status: runner.status });
56+
}
4957

5058
// Look up the managed ec2 runners in AWS, but running does not mean idle
5159
const ec2runners = await listEC2Runners({
@@ -56,13 +64,27 @@ export async function adjust(event: PoolEvent): Promise<void> {
5664
});
5765

5866
// Runner should be considered idle if it is still booting, or is idle in GitHub
59-
const managedIdleRunners = ec2runners
60-
.filter((r) => {
61-
return githubIdleRunners.some((ghRunner) => ghRunner.name == r.instanceId) || !bootTimeExceeded(r);
62-
})
63-
.map((r) => r.instanceId);
67+
let numberOfRunnersInPool = 0;
68+
for (const ec2Instance of ec2runners) {
69+
if (
70+
runnerStatus.get(ec2Instance.instanceId)?.busy === false &&
71+
runnerStatus.get(ec2Instance.instanceId)?.status === 'online'
72+
) {
73+
numberOfRunnersInPool++;
74+
logger.debug(`Runner ${ec2Instance.instanceId} is idle in GitHub and counted as part of the pool`);
75+
} else if (runnerStatus.get(ec2Instance.instanceId) != null) {
76+
logger.debug(`Runner ${ec2Instance.instanceId} is not idle in GitHub and NOT counted as part of the pool`);
77+
} else if (!bootTimeExceeded(ec2Instance)) {
78+
numberOfRunnersInPool++;
79+
logger.info(`Runner ${ec2Instance.instanceId} is still booting and counted as part of the pool`);
80+
} else {
81+
logger.debug(
82+
`Runner ${ec2Instance.instanceId} is not idle in GitHub nor booting and not counted as part of the pool`,
83+
);
84+
}
85+
}
6486

65-
const topUp = event.poolSize - managedIdleRunners.length;
87+
const topUp = event.poolSize - numberOfRunnersInPool;
6688
if (topUp > 0) {
6789
logger.info(`The pool will be topped up with ${topUp} runners.`);
6890
await createRunners(
@@ -90,7 +112,7 @@ export async function adjust(event: PoolEvent): Promise<void> {
90112
githubInstallationClient,
91113
);
92114
} else {
93-
logger.info(`Pool will not be topped up. Found ${managedIdleRunners} managed idle runners.`);
115+
logger.info(`Pool will not be topped up. Found ${numberOfRunnersInPool} managed idle runners.`);
94116
}
95117
}
96118

0 commit comments

Comments
 (0)