Skip to content

Commit

Permalink
feat(instance_launcher): basic tests for launcher logic
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronkvanmeerten committed Jan 3, 2025
1 parent b8e10a0 commit b60c41d
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 9 deletions.
2 changes: 1 addition & 1 deletion src/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ class Handlers {
// found the group, so find the instances and act upon them
// build the list of current instances
const currentInventory = await this.instanceTracker.trimCurrent(req.context, req.params.name);
const instances = this.instanceTracker.mapToInstanceDetails(currentInventory);
const instances = InstanceTracker.mapToInstanceDetails(currentInventory);
// set their reconfigure status to the current date
try {
await this.reconfigureManager.setReconfigureDate(req.context, instances);
Expand Down
12 changes: 5 additions & 7 deletions src/instance_launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ export default class InstanceLauncher {
if (options.maxThrottleThreshold) {
this.maxThrottleThreshold = options.maxThrottleThreshold;
}

this.launchOrShutdownInstancesByGroup = this.launchOrShutdownInstancesByGroup.bind(this);
}

async launchOrShutdownInstancesByGroup(ctx: Context, groupName: string): Promise<boolean> {
Expand Down Expand Up @@ -350,7 +348,7 @@ export default class InstanceLauncher {
instanceState.status.provisioning == true
);
});
return this.instanceTracker.mapToInstanceDetails(states);
return InstanceTracker.mapToInstanceDetails(states);
}

private getRunningInstances(instanceStates: InstanceState[]): InstanceDetails[] {
Expand All @@ -364,7 +362,7 @@ export default class InstanceLauncher {
instanceState.status.provisioning == false
);
});
return this.instanceTracker.mapToInstanceDetails(states);
return InstanceTracker.mapToInstanceDetails(states);
}

private getAvailableJibris(instanceStates: InstanceState[]): InstanceDetails[] {
Expand All @@ -373,7 +371,7 @@ export default class InstanceLauncher {
instanceState.status.jibriStatus && instanceState.status.jibriStatus.busyStatus == JibriStatusState.Idle
);
});
return this.instanceTracker.mapToInstanceDetails(states);
return InstanceTracker.mapToInstanceDetails(states);
}

private getExpiredJibris(instanceStates: InstanceState[]): InstanceDetails[] {
Expand All @@ -383,7 +381,7 @@ export default class InstanceLauncher {
instanceState.status.jibriStatus.busyStatus == JibriStatusState.Expired
);
});
return this.instanceTracker.mapToInstanceDetails(states);
return InstanceTracker.mapToInstanceDetails(states);
}

private getBusyJibris(instanceStates: InstanceState[]): InstanceDetails[] {
Expand All @@ -392,6 +390,6 @@ export default class InstanceLauncher {
instanceState.status.jibriStatus && instanceState.status.jibriStatus.busyStatus == JibriStatusState.Busy
);
});
return this.instanceTracker.mapToInstanceDetails(states);
return InstanceTracker.mapToInstanceDetails(states);
}
}
2 changes: 1 addition & 1 deletion src/instance_tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ export class InstanceTracker {
return states.filter((_, index) => !statesShutdownStatus[index] && !shutdownConfirmations[index]);
}

mapToInstanceDetails(states: InstanceState[]): InstanceDetails[] {
static mapToInstanceDetails(states: InstanceState[]): InstanceDetails[] {
return states.map((response) => {
return <InstanceDetails>{
instanceId: response.instanceId,
Expand Down
177 changes: 177 additions & 0 deletions src/test/instance_launcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck

import assert from 'node:assert';
import test, { afterEach, describe, mock } from 'node:test';

import InstanceTracker from '../instance_tracker';
import InstanceLauncher from '../instance_launcher';

function log(msg, obj) {
console.log(msg, JSON.stringify(obj));
}

function initContext() {
return {
logger: {
info: mock.fn(log),
debug: mock.fn(log),
error: mock.fn(log),
warn: mock.fn(log),
},
};
}

describe('InstanceLauncher', () => {
let context = initContext();

const shutdownManager = {
shutdown: mock.fn(),
areScaleDownProtected: mock.fn(() => []),
};

const audit = {
updateLastLauncherRun: mock.fn(),
saveLauncherActionItem: mock.fn(),
};

const groupName = 'group';
const groupDetails = {
name: groupName,
type: 'JVB',
region: 'default',
environment: 'test',
compartmentId: 'test',
instanceConfigurationId: 'test',
enableAutoScale: true,
enableLaunch: true,
gracePeriodTTLSec: 480,
protectedTTLSec: 600,
scalingOptions: {
minDesired: 1,
maxDesired: 1,
desiredCount: 1,
scaleUpQuantity: 1,
scaleDownQuantity: 1,
scaleUpThreshold: 0.8,
scaleDownThreshold: 0.3,
scalePeriod: 60,
scaleUpPeriodsCount: 2,
scaleDownPeriodsCount: 2,
},
};

const inventory = [{ instanceId: 'i-deadbeef007', status: { stats: { stress_level: 0.5, participants: 1 } } }];

const instanceGroupManager = {
getInstanceGroup: mock.fn(() => groupDetails),
isScaleDownProtected: mock.fn(() => false),
};

const cloudManager = {
scaleUp: mock.fn(() => groupDetails.scalingOptions.scaleUpQuantity),
scaleDown: mock.fn(),
};

const instanceTracker = {
trimCurrent: mock.fn(() => inventory),
mapToInstanceDetails: mock.fn((i) => InstanceTracker.mapToInstanceDetails(i)),
};

const metricsLoop = {
updateMetrics: mock.fn(),
getUnTrackedCount: mock.fn(() => 0),
};

// now we can create an instance of the class
const instanceLauncher = new InstanceLauncher({
instanceTracker,
instanceGroupManager,
cloudManager,
shutdownManager,
audit,
metricsLoop,
});

afterEach(() => {
audit.updateLastLauncherRun.mock.resetCalls();
instanceTracker.trimCurrent.mock.resetCalls();
shutdownManager.areScaleDownProtected.mock.resetCalls();
cloudManager.scaleDown.mock.resetCalls();
cloudManager.scaleUp.mock.resetCalls();
context = initContext();
});

describe('instanceLauncher basic tests', () => {
// first test if disabled group exits correctly
test('launchOrShutdownInstancesByGroup should return false if group is disabled', async () => {
const groupDetailsDisabled = { ...groupDetails, enableLaunch: false };
instanceGroupManager.getInstanceGroup.mock.mockImplementationOnce(() => groupDetailsDisabled);
const result = await instanceLauncher.launchOrShutdownInstancesByGroup(context, groupName);
assert.equal(result, false, 'skip disabled group');
});

// now test if enable group does nothing with desired of 1 and inventory of 1
test('launchOrShutdownInstancesByGroup should return true if desired is 1 and inventory is 1', async () => {
const result = await instanceLauncher.launchOrShutdownInstancesByGroup(context, groupName);
assert.equal(result, true, 'skip desired=1 and inventory=1');
assert.equal(cloudManager.scaleUp.mock.calls.length, 0, 'no scaleUp');
assert.equal(cloudManager.scaleDown.mock.calls.length, 0, 'no scaleDown');
assert.equal(instanceTracker.trimCurrent.mock.calls.length, 1, 'trimCurrent called');
assert.equal(audit.updateLastLauncherRun.mock.calls.length, 1, 'audit.updateLastLauncherRun called');
assert.equal(context.logger.info.mock.calls.length, 1, 'logger.info called');
assert.equal(
context.logger.info.mock.calls[0].arguments[0],
'[Launcher] No scaling activity needed for group group with 1 instances.',
);
});

// now test if scaleDown occurs with desired of 0 and inventory of 1
test('launchOrShutdownInstancesByGroup should return true if desired is 0 and inventory is 1', async () => {
const groupDetailsDesired0 = {
...groupDetails,
scalingOptions: { ...groupDetails.scalingOptions, desiredCount: 0, minDesired: 0 },
};
instanceGroupManager.getInstanceGroup.mock.mockImplementationOnce(() => groupDetailsDesired0);

const result = await instanceLauncher.launchOrShutdownInstancesByGroup(context, groupName);
assert.equal(result, true, 'scaleDown desired=0 and inventory=1');
assert.equal(audit.updateLastLauncherRun.mock.calls.length, 1, 'audit.updateLastLauncherRun called');
assert.equal(instanceTracker.trimCurrent.mock.calls.length, 1, 'trimCurrent called');
assert.equal(cloudManager.scaleUp.mock.calls.length, 0, 'no scaleUp');
assert.equal(cloudManager.scaleDown.mock.calls.length, 1, 'scaleDown called');
assert.equal(shutdownManager.areScaleDownProtected.mock.calls.length, 1, 'areScaleDownProtected called');
assert.equal(context.logger.info.mock.calls.length, 1, 'logger.info called');
assert.equal(
context.logger.info.mock.calls[0].arguments[0],
'[Launcher] Will scale down to the desired count',
);
});

// now test if scaleUp occurs with desired of 2 and inventory of 1
test('launchOrShutdownInstancesByGroup should return true if desired is 2 and inventory is 1', async () => {
const groupDetailsDesired2 = {
...groupDetails,
scalingOptions: { ...groupDetails.scalingOptions, desiredCount: 2, maxDesired: 2 },
};
instanceGroupManager.getInstanceGroup.mock.mockImplementationOnce(() => groupDetailsDesired2);

const result = await instanceLauncher.launchOrShutdownInstancesByGroup(context, groupName);
assert.equal(result, true, 'scaleDown desired=0 and inventory=1');
assert.equal(audit.updateLastLauncherRun.mock.calls.length, 1, 'audit.updateLastLauncherRun called');
assert.equal(instanceTracker.trimCurrent.mock.calls.length, 1, 'trimCurrent called');
assert.equal(cloudManager.scaleDown.mock.calls.length, 0, 'no scaleDown');
assert.equal(cloudManager.scaleUp.mock.calls.length, 1, 'scaleUp called');
assert.equal(
shutdownManager.areScaleDownProtected.mock.calls.length,
0,
'areScaleDownProtected not called',
);
assert.equal(context.logger.info.mock.calls.length, 1, 'logger.info called');
assert.equal(
context.logger.info.mock.calls[0].arguments[0],
'[Launcher] Will scale up to the desired count',
);
});
});
});

0 comments on commit b60c41d

Please sign in to comment.