Skip to content

Commit

Permalink
BC-8054 - Board-Loadtest - establish connections first (#5230)
Browse files Browse the repository at this point in the history
Refactor loadtests to create connections first and to start simulation of the users later.

* refactor: establish all connections upfront
* switch to redisIoAdapter
  • Loading branch information
hoeppner-dataport authored Sep 24, 2024
1 parent ff30cd9 commit 5d0f6d5
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 79 deletions.
8 changes: 3 additions & 5 deletions apps/server/src/apps/board-collaboration.app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { install as sourceMapInstall } from 'source-map-support';
// application imports
import { SwaggerDocumentOptions } from '@nestjs/swagger';
import { LegacyLogger, Logger } from '@src/core/logger';
import { MongoIoAdapter } from '@src/infra/socketio';
import { RedisIoAdapter } from '@src/infra/socketio';
import { BoardCollaborationModule } from '@src/modules/board/board-collaboration.module';
import { enableOpenApiDocs } from '@src/shared/controller/swagger';
import express from 'express';
Expand All @@ -17,7 +17,6 @@ import {
createAndStartPrometheusMetricsAppIfEnabled,
} from '@src/apps/helpers/prometheus-metrics';
import { ExpressAdapter } from '@nestjs/platform-express';
import { DB_URL } from '@src/config';

async function bootstrap() {
sourceMapInstall();
Expand All @@ -29,9 +28,8 @@ async function bootstrap() {
nestApp.useLogger(legacyLogger);
nestApp.enableCors({ exposedHeaders: ['Content-Disposition'] });

const mongoAdapter = new MongoIoAdapter(nestApp);
await mongoAdapter.connectToMongoDb(DB_URL);
const ioAdapter = mongoAdapter;
const ioAdapter = new RedisIoAdapter(nestApp);
ioAdapter.connectToRedis();
nestApp.useWebSocketAdapter(ioAdapter);

const options: SwaggerDocumentOptions = {
Expand Down
40 changes: 19 additions & 21 deletions apps/server/src/modules/board/loadtest/board-load-test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,6 @@ jest.mock('./loadtest-client', () => {
};
});

const testClass: ClassDefinition = {
name: 'viewersClass',
users: [{ ...fastEditor, amount: 5 }],
};

beforeEach(() => {
jest.resetAllMocks();
jest.useFakeTimers();
Expand All @@ -42,42 +37,45 @@ afterEach(() => {
jest.useRealTimers();
});

const classDefinitionExample = {
name: 'my-configuration',
users: [{ name: 'tempuserprofile', isActive: true, sleepMs: 100, amount: 1 }],
simulateUsersTimeMs: 2000,
};

describe('BoardLoadTest', () => {
const setup = () => {
const setup = (classDefinition: ClassDefinition) => {
const socketConfiguration = { baseUrl: '', path: '', token: '' };
const socketConnectionManager = createMock<SocketConnectionManager>();
socketConnectionManager.createConnections = jest
.fn()
.mockResolvedValue([new SocketConnection(socketConfiguration, console.log)]);
const socketConnection = new SocketConnection(socketConfiguration, console.log);

const boarLoadTest = new BoardLoadTest(socketConnectionManager, console.log);
const boarLoadTest = new BoardLoadTest(socketConnectionManager, classDefinition, console.log);
return { boarLoadTest, socketConnectionManager, socketConnection };
};

describe('runBoardTest', () => {
describe('if no userProfiles are provided', () => {
it('should do nothing', async () => {
const { boarLoadTest } = setup();
const { boarLoadTest } = setup(classDefinitionExample);
const boardId = 'board-id';
const configuration = {
name: 'my-configuration',
users: [{ name: 'tempuserprofile', isActive: true, sleepMs: 100, amount: 1 }],
simulateUsersTimeMs: 2000,
};

const response = await boarLoadTest.runBoardTest(boardId, configuration);
await boarLoadTest.initializeLoadtestClients(boardId);
const response = await boarLoadTest.runBoardTest();

expect(response).toBeUndefined();
});
});

describe('if userProfiles are provided', () => {
it('should create socketConnections for all users', async () => {
const { boarLoadTest, socketConnectionManager } = setup();
const { boarLoadTest, socketConnectionManager } = setup(classDefinitionExample);
const boardId = 'board-id';

await boarLoadTest.runBoardTest(boardId, testClass);
await boarLoadTest.initializeLoadtestClients(boardId);
await boarLoadTest.runBoardTest();

expect(socketConnectionManager.createConnections).toHaveBeenCalledTimes(1);
});
Expand All @@ -86,7 +84,7 @@ describe('BoardLoadTest', () => {

describe('simulateUsersActions', () => {
it('should simulate actions for all users', async () => {
const { boarLoadTest } = setup();
const { boarLoadTest } = setup(classDefinitionExample);
const loadtestClient = {
createColumn: jest.fn().mockResolvedValue({ id: 'some-id' }),
createCard: jest.fn().mockResolvedValue({ id: 'some-id' }),
Expand All @@ -99,7 +97,7 @@ describe('BoardLoadTest', () => {
} as unknown as LoadtestClient;
const userProfile = fastEditor;

await boarLoadTest.simulateUsersActions([loadtestClient], [userProfile]);
await boarLoadTest.simulateUserActions(loadtestClient, userProfile);

expect(loadtestClient.createColumn).toHaveBeenCalled();
expect(loadtestClient.createCard).toHaveBeenCalled();
Expand All @@ -108,7 +106,7 @@ describe('BoardLoadTest', () => {

describe('simulateUserActions', () => {
it('should create columns and cards', async () => {
const { boarLoadTest } = setup();
const { boarLoadTest } = setup(classDefinitionExample);
const loadtestClient = {
createColumn: jest.fn().mockResolvedValue({ id: 'some-id' }),
createCard: jest.fn().mockResolvedValue({ id: 'some-id' }),
Expand All @@ -130,7 +128,7 @@ describe('BoardLoadTest', () => {

describe('createColumn', () => {
it('should create a column', async () => {
const { boarLoadTest } = setup();
const { boarLoadTest } = setup(classDefinitionExample);

const loadtestClient = {
createColumn: jest.fn().mockResolvedValue({ id: 'some-id' }),
Expand All @@ -144,7 +142,7 @@ describe('BoardLoadTest', () => {

describe('createRandomCard', () => {
it('should create a card', async () => {
const { boarLoadTest } = setup();
const { boarLoadTest } = setup(classDefinitionExample);
boarLoadTest.trackColumn('some-id');

const loadtestClient = {
Expand Down
53 changes: 26 additions & 27 deletions apps/server/src/modules/board/loadtest/board-load-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,51 +12,50 @@ const SIMULATE_USER_TIME_MS = 120000;
export class BoardLoadTest {
private columns: { id: string; cards: { id: string }[] }[] = [];

constructor(private socketConnectionManager: SocketConnectionManager, private onError: Callback) {}
private userProfiles: UserProfile[] = [];

async runBoardTest(boardId: string, configuration: ClassDefinition): Promise<void> {
private loadtestClients: LoadtestClient[] = [];

constructor(
private socketConnectionManager: SocketConnectionManager,
classDefinition: ClassDefinition,
private onError: Callback
) {
this.userProfiles = duplicateUserProfiles(classDefinition.users);
}

async runBoardTest(): Promise<void> {
try {
const userProfiles = duplicateUserProfiles(configuration.users);
const userClients = await this.initializeLoadtestClients(userProfiles.length, boardId);
await this.simulateUsersActions(userClients, userProfiles);
await this.simulateUsersActions();
} catch (err) {
this.onError((err as Error).message);
}
}

async initializeLoadtestClients(amount: number, boardId: string): Promise<LoadtestClient[]> {
const connections = await this.socketConnectionManager.createConnections(amount);
const promises = connections.map((socketConnection: SocketConnection) =>
this.initializeLoadtestClient(socketConnection, boardId)
);
const results = await Promise.all(promises);
return results;
}

async initializeLoadtestClient(socketConnection: SocketConnection, boardId: string): Promise<LoadtestClient> {
/* istanbul ignore next */
const loadtestClient = createLoadtestClient(socketConnection, boardId);

await sleep(Math.ceil(Math.random() * 20000));
/* istanbul ignore next */
await loadtestClient.fetchBoard();
/* istanbul ignore next */
return loadtestClient;
async initializeLoadtestClients(boardId: string): Promise<void> {
const connections = await this.socketConnectionManager.createConnections(this.userProfiles.length);
this.loadtestClients = connections.map((socketConnection: SocketConnection) => {
const loadtestClient = createLoadtestClient(socketConnection, boardId);
return loadtestClient;
});
}

async simulateUsersActions(loadtestClients: LoadtestClient[], userProfiles: UserProfile[]) {
async simulateUsersActions() {
// eslint-disable-next-line arrow-body-style
const promises = loadtestClients.map((loadtestClient, index) => {
const promises = this.loadtestClients.map((loadtestClient, index) => {
/* istanbul ignore next */
return this.simulateUserActions(loadtestClient, userProfiles[index]);
return this.simulateUserActions(loadtestClient, this.userProfiles[index]);
});
await Promise.all(promises);
}

async simulateUserActions(loadtestClient: LoadtestClient, userProfile: UserProfile, actionsMax = 1000000) {
const startTime = performance.now();

await sleep(Math.ceil(Math.random() * 3000));
await sleep(Math.ceil(Math.random() * 20000));

/* istanbul ignore next */
await loadtestClient.fetchBoard();

let actionCount = 0;
while (performance.now() - startTime < SIMULATE_USER_TIME_MS && actionCount < actionsMax) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ describe('LoadtestRunner', () => {
});

const setup = () => {
const runBoardTest = jest.fn().mockResolvedValue({ responseTimes: [] });
const runBoardTest = jest.fn().mockResolvedValue({});
const initializeLoadtestClients = jest.fn();
const createBoardLoadTest = jest.fn().mockImplementation(() => {
return { runBoardTest };
return { runBoardTest, initializeLoadtestClients };
});
const socketConfiguration = { baseUrl: 'http://localhost', path: '', token: '' };
const socketConnectionManager = new SocketConnectionManager(socketConfiguration);
Expand Down
15 changes: 10 additions & 5 deletions apps/server/src/modules/board/loadtest/loadtest-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getUrlConfiguration } from './helper/get-url-configuration';
import { useResponseTimes } from './helper/responseTimes.composable';
import { SocketConnectionManager } from './socket-connection-manager';
import { Callback, ClassDefinitionWithAmount, CreateBoardLoadTest, SocketConfiguration } from './types';
import { BoardLoadTest } from './board-load-test';

const { getAvgByAction, getTotalAvg } = useResponseTimes();

Expand Down Expand Up @@ -125,11 +126,15 @@ export class LoadtestRunner {
throw new Error('Failed to create all boards');
}

const promises: Promise<unknown>[] = classes.flatMap(async (classDefinition, index) => {
const boardLoadTest = this.createBoardLoadTest(this.socketConnectionManager, this.onError);
const boardId = boardIds[index];
return boardLoadTest.runBoardTest(boardId, classDefinition);
});
const boardLoadTests: BoardLoadTest[] = [];
for (const classDefinition of classes) {
const boardLoadTest = this.createBoardLoadTest(this.socketConnectionManager, classDefinition, this.onError);
const boardId = boardIds[boardLoadTests.length];
await boardLoadTest.initializeLoadtestClients(boardId);
boardLoadTests.push(boardLoadTest);
}

const promises: Promise<unknown>[] = boardLoadTests.map((boardLoadTest) => boardLoadTest.runBoardTest());

await Promise.all(promises);

Expand Down
20 changes: 17 additions & 3 deletions apps/server/src/modules/board/loadtest/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ The tests can be run from your local environment or from any other place that ha

In order to run the load tests you need to provide three environment variables:

### target
### TARGET_URL

The Url of the server.

e.g. `export TARGET_URL=http://localhost:4450` <br>
e.g. `export TARGET_URL=https://bc-7830-board-loadtests-merge.brb.dbildungscloud.dev`

### courseId
### COURSE_ID

The id of the course that the user (see next variable "token") is allowed to create boards in.<br>
e.g. `export COURSE_ID=66c493f577499cc64bf9aab4`

### token
### TOKEN

A valid JWT-token of a user that is allowed to create boards in the given course. <br>
e.g. `export TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6...`
Expand Down Expand Up @@ -51,3 +51,17 @@ To run the test:
```bash
npx jest apps/server/src/modules/board/loadtest/board-collaboration.load.spec.ts
```

### VIEWER_CLASSES

Defines the number of viewer classes. Those classes consist of an active user (= the teacher) editing stuff on the board and thirty students.

e.g `export VIEWER_CLASSES=2`
e.g `export VIEWER_CLASSES=0`

### COLLAB_CLASSES

Defines the number of collaboration classes. Those classes consist of thirty active users.

e.g. `export COLLAB_CLASSES=2`
e.g. `export COLLAB_CLASSES=0`
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,9 @@ export class SocketConnectionManager {
const connections: SocketConnection[] = [];

while (connections.length < amount) {
const batchAmount = Math.min(10, amount - connections.length);
const promises = Array(batchAmount)
.fill(1)
.map(() => this.createConnection());

const allSettled = await Promise.allSettled(promises);
allSettled.forEach((res) => {
if (res.status === 'fulfilled') {
connections.push(res.value);
} else {
/* istanbul ignore next */
this.onErrorHandler('failed to create connection');
}
});
await sleep(1000);
const connection = await this.createConnection();
connections.push(connection);
await sleep(100);
}
return connections;
}
Expand Down
9 changes: 9 additions & 0 deletions apps/server/src/modules/board/loadtest/socket-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ export class SocketConnection {
handle = setTimeout(() => {
/* istanbul ignore next */
if (!this.connected) {
this.stopTimeoutChecks();
reject(new Error('Timeout: could not connect to socket server'));
}
}, this.socketConfiguration.connectTimeout ?? 10000);
this.stopTimeoutChecks();
});
}

Expand All @@ -102,6 +104,13 @@ export class SocketConnection {
}
}

stopTimeoutChecks() {
if (!this.checkerInterval) {
/* istanbul ignore next */
clearInterval(this.checkerInterval);
}
}

checkTimeouts() {
const now = performance.now();
while (this.timeoutuntilList.length > 0 && this.timeoutuntilList[0]?.timeoutUntil < now) {
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/modules/board/loadtest/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,9 @@ export type SocketConfiguration = {
export type Callback = (...args: unknown[]) => void;

export interface CreateBoardLoadTest {
(socketConnectionManager: SocketConnectionManager, onError: Callback): BoardLoadTest;
(
socketConnectionManager: SocketConnectionManager,
classDefinition: ClassDefinition,
onError: Callback
): BoardLoadTest;
}

0 comments on commit 5d0f6d5

Please sign in to comment.