Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC-7038 - advanced load testing #5125

Closed
wants to merge 82 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
7442380
initial commit
hoeppner-dataport Jul 19, 2024
02db6d0
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Jul 19, 2024
080d5bc
implementation of scenario-user-configurations
hoeppner-dataport Jul 19, 2024
cf03e65
chore: change bucket maxAgeSeconds from 600 to 30
hoeppner-dataport Jul 19, 2024
aa42034
fix: connecting when multiple collaboration pods
hoeppner-dataport Jul 22, 2024
e9f140c
fix: user gauge naming
hoeppner-dataport Jul 22, 2024
789db0d
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Jul 29, 2024
ac71936
chore: try redis again
hoeppner-dataport Jul 29, 2024
f3c5202
chore: add redis error handler
hoeppner-dataport Jul 29, 2024
08ab990
chore: get redis uri from configuration
hoeppner-dataport Jul 29, 2024
89d6aa4
fix: replace redis- with mongoIoAdapter again
hoeppner-dataport Jul 29, 2024
08ad4ad
chore: cleanup
hoeppner-dataport Aug 2, 2024
03c49b0
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Aug 2, 2024
f946320
chore: redisIoAdapter typing
hoeppner-dataport Aug 5, 2024
3124670
exclude load-tests from regular tests
hoeppner-dataport Aug 5, 2024
da84677
add loadtests to ignore list of jest config
hoeppner-dataport Aug 5, 2024
c16aabe
exclude load test from automatic tests
hoeppner-dataport Aug 5, 2024
8d0d7dd
fix tests
hoeppner-dataport Aug 5, 2024
93458f6
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Aug 5, 2024
c80862c
fix formatDate test timezone issue
hoeppner-dataport Aug 5, 2024
193cf05
fix formatData and tests
hoeppner-dataport Aug 5, 2024
c4abb3a
reset jest.config deny of .load.spec.ts
hoeppner-dataport Aug 7, 2024
e8c874f
adapt logging and amount of viewer classes
hoeppner-dataport Aug 7, 2024
f9373eb
implement rampUp
hoeppner-dataport Aug 8, 2024
36218f2
switchToRedis
hoeppner-dataport Aug 8, 2024
e68d2b1
add logging to redisIoAdapter
hoeppner-dataport Aug 8, 2024
96d2ca3
chore: fix test
hoeppner-dataport Aug 8, 2024
51bc665
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Aug 8, 2024
0d49157
chore: fix tests
hoeppner-dataport Aug 8, 2024
3ad80ad
chore: fix tests
hoeppner-dataport Aug 8, 2024
e7d3c8f
fix legacy logger
hoeppner-dataport Aug 8, 2024
5d33799
chore: replace legacyLogger with console.log
hoeppner-dataport Aug 8, 2024
5dfd7df
fix: redisIoAdapter
hoeppner-dataport Aug 8, 2024
eb6df25
fix redis adapter
hoeppner-dataport Aug 8, 2024
1c06bbe
finetune redisIoAdapter
hoeppner-dataport Aug 8, 2024
4ead105
disable fetch board
hoeppner-dataport Aug 8, 2024
c68c04c
chore: remove unneeded async keyword
hoeppner-dataport Aug 8, 2024
b00f2b9
chore: remove loging
hoeppner-dataport Aug 8, 2024
9006d50
redis again
hoeppner-dataport Aug 8, 2024
ae5c318
skip: redis connect
hoeppner-dataport Aug 9, 2024
489fd5d
return promise from connectToRedis
hoeppner-dataport Aug 9, 2024
b8a0ffe
remove unneeded connect-calls
hoeppner-dataport Aug 9, 2024
32d57f8
improve stats output during testrun
hoeppner-dataport Aug 9, 2024
2baa9ca
switch back to other waitSuccess function
hoeppner-dataport Aug 9, 2024
1df32a3
configurable class counts
hoeppner-dataport Aug 9, 2024
bbc91cc
minor change to stats
hoeppner-dataport Aug 9, 2024
2d25cf4
remove fetchCard from tests
hoeppner-dataport Aug 9, 2024
c8f4d3d
switch to log based success
hoeppner-dataport Aug 9, 2024
7d6e2b7
optimization aproach create-column
hoeppner-dataport Aug 9, 2024
d533dc2
disable checkPermission for createColumn
hoeppner-dataport Aug 9, 2024
ae71245
fix: socket connect before first action
hoeppner-dataport Aug 9, 2024
3208162
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Aug 9, 2024
8ea6475
chore: use enums
hoeppner-dataport Aug 12, 2024
eb1f643
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Aug 12, 2024
f67cd49
make ioAdapter configurable through env variable
hoeppner-dataport Aug 12, 2024
1a3b68c
chore: fix boardUc test
hoeppner-dataport Aug 12, 2024
ba62e11
add logging for server-side emitted actions
hoeppner-dataport Aug 12, 2024
1915123
improved connection handling
hoeppner-dataport Aug 12, 2024
7413eb5
chore: finetuning timeouts
hoeppner-dataport Aug 12, 2024
f16cb7d
chore: refactorings
hoeppner-dataport Aug 14, 2024
9bdbb20
make connection amount configurable
hoeppner-dataport Aug 14, 2024
2879403
chore: further refactorings
hoeppner-dataport Aug 14, 2024
0bbadaa
chore: small changes
hoeppner-dataport Aug 14, 2024
c18d04a
refactoring
hoeppner-dataport Aug 15, 2024
5c59a32
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Aug 15, 2024
9cb9546
chore: fix some tests
hoeppner-dataport Aug 16, 2024
1276c4d
chore: test fixes
hoeppner-dataport Aug 16, 2024
b9f2c69
chore: fix naming
hoeppner-dataport Aug 16, 2024
952e0a5
chore: added testfile
hoeppner-dataport Aug 16, 2024
b49bfb0
chore: added tests
hoeppner-dataport Aug 16, 2024
addec1b
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Aug 16, 2024
abbab59
chore: tests
hoeppner-dataport Aug 16, 2024
e0a6d4f
chore: additional tests
hoeppner-dataport Aug 19, 2024
48da98f
chore: increase test coverage
hoeppner-dataport Aug 19, 2024
1f63809
chore: fix tests
hoeppner-dataport Aug 19, 2024
b2ba65d
chore: fix tests
hoeppner-dataport Aug 19, 2024
3fbed62
chore: add tests
hoeppner-dataport Aug 19, 2024
f6eb1de
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Aug 19, 2024
6c8dc13
chore: remove unfinished test
hoeppner-dataport Aug 19, 2024
747b3d7
chore: add test
hoeppner-dataport Aug 20, 2024
857fa04
chore: add tests
hoeppner-dataport Aug 20, 2024
9129ac2
chore: fix loadtest-runner coverage
hoeppner-dataport Aug 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions apps/server/src/apps/board-collaboration.app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { install as sourceMapInstall } from 'source-map-support';

// application imports
import { SwaggerDocumentOptions } from '@nestjs/swagger';
import { DB_URL } from '@src/config';
// import { DB_URL } from '@src/config';
import { LegacyLogger, Logger } from '@src/core/logger';
import { MongoIoAdapter } from '@src/infra/socketio';
import { MongoIoAdapter, 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 @@ -18,19 +18,29 @@ import {
createAndStartPrometheusMetricsAppIfEnabled,
} from '@src/apps/helpers/prometheus-metrics';
import { ExpressAdapter } from '@nestjs/platform-express';
import { DB_URL } from '@src/config';
import { IoAdapter } from '@nestjs/platform-socket.io';

async function bootstrap() {
sourceMapInstall();

const nestExpress = express();
const nestExpressAdapter = new ExpressAdapter(nestExpress);
const nestApp = await NestFactory.create(BoardCollaborationModule, nestExpressAdapter);
nestApp.useLogger(await nestApp.resolve(LegacyLogger));
const legacyLogger = await nestApp.resolve(LegacyLogger);
nestApp.useLogger(legacyLogger);
nestApp.enableCors({ exposedHeaders: ['Content-Disposition'] });

const mongoIoAdapter = new MongoIoAdapter(nestApp);
await mongoIoAdapter.connectToMongoDb(DB_URL);
nestApp.useWebSocketAdapter(mongoIoAdapter);
let ioAdapter: IoAdapter;
// eslint-disable-next-line no-process-env
if (process.env.COLLABORATION_WEBSOCKET_ADAPTER === 'redis') {
ioAdapter = new RedisIoAdapter(nestApp);
} else {
const mongoAdapter = new MongoIoAdapter(nestApp);
await mongoAdapter.connectToMongoDb(DB_URL);
ioAdapter = mongoAdapter;
}
nestApp.useWebSocketAdapter(ioAdapter);

const options: SwaggerDocumentOptions = {
operationIdFactory: (_controllerKey: string, methodKey: string) => methodKey,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/infra/socketio/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { Socket } from './types';
export { WsValidationPipe } from './ws-validation.pipe';
export { MongoIoAdapter } from './mongodb-ioadapter';
export { RedisIoAdapter } from './redis-ioadapter';
42 changes: 42 additions & 0 deletions apps/server/src/infra/socketio/redis-ioadapter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { LegacyLogger } from '@src/core/logger';
import { RedisIoAdapter } from './redis-ioadapter';

jest.mock('@src/core/logger', () => {
return {
LegacyLogger: jest.fn().mockImplementation(() => {
return {
error: jest.fn(),
};
}),
};
});

const redisMock = {
on: jest.fn(),
psubscribe: jest.fn(),
subscribe: jest.fn(),
};

jest.mock('ioredis', () => {
return {
Redis: jest.fn().mockImplementation(() => redisMock),
};
});

describe('RedisIoAdapter', () => {
const setup = () => {
const legacyLogger = { error: jest.fn() } as unknown as LegacyLogger;
const redisIoAdapter = new RedisIoAdapter(legacyLogger);
return { redisIoAdapter, legacyLogger };
};

describe('createIOServer', () => {
it('should send several actions', () => {
const { redisIoAdapter } = setup();

redisIoAdapter.createIOServer(1234);

expect(redisMock.on).toHaveBeenCalledTimes(6);
});
});
});
49 changes: 49 additions & 0 deletions apps/server/src/infra/socketio/redis-ioadapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { IoAdapter } from '@nestjs/platform-socket.io';
import { ServerOptions, Server } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { Configuration } from '@hpi-schul-cloud/commons';
import { Redis } from 'ioredis';
import { LegacyLogger } from '@src/core/logger';

export class RedisIoAdapter extends IoAdapter {
private adapterConstructor: ReturnType<typeof createAdapter> | undefined = undefined;

constructor(private readonly legacyLogger: LegacyLogger) {
super();
}

connectToRedis(): void {
const redisUri = Configuration.has('REDIS_URI') ? (Configuration.get('REDIS_URI') as string) : 'localhost:6379';
const pubClient = new Redis(redisUri);
const subClient = new Redis(redisUri);

pubClient.on('error', (err) => {
// istanbul ignore next
this.legacyLogger.error('pubClient error', err);
});

subClient.on('error', (err) => {
// istanbul ignore next
this.legacyLogger.error('subClient error', err);
});

this.adapterConstructor = createAdapter(pubClient, subClient);
}

createIOServer(port: number, options?: ServerOptions): Server {
this.connectToRedis();
// istanbul ignore next
if (!this.adapterConstructor) {
throw new Error('Redis adapter is not connected to Redis yet.');
}
const server = super.createIOServer(port, options) as Server;
// istanbul ignore next
if (server === undefined) {
throw new Error('Unable to create RedisServer');
}
server.adapter(this.adapterConstructor);
return server;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect {
...data,
newColumn,
};
await emitter.joinRoom(column);
emitter.emitToClientAndRoom(responsePayload, column);

// payload needs to be returned to allow the client to do sequential operation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* eslint-disable no-process-env */
import { CreateBoardLoadTest, SocketConfiguration } from './types';
import { viewersClass, collaborativeClass } from './helper/class-definitions';
import { SocketConnectionManager } from './socket-connection-manager';
import { LoadtestRunner } from './loadtest-runner';
import { BoardLoadTest } from './board-load-test';

describe('Board Collaboration Load Test', () => {
it('should run a basic load test', async () => {
const { COURSE_ID, TOKEN, TARGET_URL } = process.env;
const viewerClassesAmount = process.env.viewerClasses ? parseInt(process.env.viewerClasses, 10) : 20;
const collabClassesAmount = process.env.collabClasses ? parseInt(process.env.collabClasses, 10) : 0;
if (COURSE_ID && TOKEN && TARGET_URL) {
const socketConfiguration: SocketConfiguration = {
baseUrl: TARGET_URL,
path: '/board-collaboration',
token: TOKEN,
connectTimeout: 5000,
};

const socketConnectionManager = new SocketConnectionManager(socketConfiguration);
const createBoardLoadTest: CreateBoardLoadTest = (...args) => new BoardLoadTest(...args);
const runner = new LoadtestRunner(socketConnectionManager, createBoardLoadTest);
await runner.runLoadtest({
socketConfiguration,
courseId: COURSE_ID,
configurations: [
{ classDefinition: viewersClass, amount: viewerClassesAmount },
{ classDefinition: collaborativeClass, amount: collabClassesAmount },
],
});
} else {
expect('this should only be ran manually').toBeTruthy();
}
}, 600000);
});
127 changes: 127 additions & 0 deletions apps/server/src/modules/board/loadtest/board-load-test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { BoardLoadTest } from './board-load-test';
import { fastEditor } from './helper/class-definitions';
import { SocketConnectionManager } from './socket-connection-manager';
import { ClassDefinition } from './types';
import { SocketConnection } from './socket-connection';
import { LoadtestClient } from './loadtest-client';

jest.mock('./helper/sleep', () => {
return { sleep: () => Promise.resolve(true) };
});

jest.mock('./loadtest-client', () => {
return {
createColumn: jest.fn().mockResolvedValue({ id: 'some-id' }),
createCard: jest.fn().mockResolvedValue({ id: 'some-id' }),
createElement: jest.fn().mockResolvedValue({ id: 'some-id' }),
createAndUpdateLinkElement: jest.fn().mockResolvedValue({ id: 'some-id' }),
createAndUpdateTextElement: jest.fn().mockResolvedValue({ id: 'some-id' }),
updateCardTitle: jest.fn().mockResolvedValue({ id: 'some-id' }),
updateColumnTitle: jest.fn().mockResolvedValue({ id: 'some-id' }),
};
});

jest.mock('./socket-connection-manager');

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

beforeEach(() => {
jest.resetAllMocks();
jest.useFakeTimers();
});

afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
});

describe('BoardLoadTest', () => {
const setup = () => {
const socketConfiguration = { baseUrl: '', path: '', token: '' };
const socketConnectionManager = new SocketConnectionManager(socketConfiguration);
const socketConnection = new SocketConnection(socketConfiguration, console.log);

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

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

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

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

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

await boarLoadTest.runBoardTest(boardId, testClass);

expect(socketConnectionManager.createConnection).toHaveBeenCalledTimes(5);
});
});
});

describe('simulateUserActions', () => {
it('should create columns and cards', async () => {
const { boarLoadTest } = setup();
const loadtestClient = {
createColumn: jest.fn().mockResolvedValue({ id: 'some-id' }),
createCard: jest.fn().mockResolvedValue({ id: 'some-id' }),
createElement: jest.fn().mockResolvedValue({ id: 'some-id' }),
createAndUpdateLinkElement: jest.fn().mockResolvedValue({ id: 'some-id' }),
createAndUpdateTextElement: jest.fn().mockResolvedValue({ id: 'some-id' }),
updateCardTitle: jest.fn().mockResolvedValue({ id: 'some-id' }),
updateColumnTitle: jest.fn().mockResolvedValue({ id: 'some-id' }),
} as unknown as LoadtestClient;
const userProfile = fastEditor;

await boarLoadTest.simulateUserActions(loadtestClient, userProfile, 10);

expect(loadtestClient.createColumn).toHaveBeenCalled();
expect(loadtestClient.createCard).toHaveBeenCalled();
}, 10000);
});

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

const loadtestClient = {
createColumn: jest.fn().mockResolvedValue({ id: 'some-id' }),
updateColumnTitle: jest.fn().mockResolvedValue({ id: 'some-id' }),
} as unknown as LoadtestClient;
await boarLoadTest.createColumn(loadtestClient);

expect(loadtestClient.createColumn).toHaveBeenCalled();
});
});

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

const loadtestClient = {
createCard: jest.fn().mockResolvedValue({ id: 'some-id' }),
updateCardTitle: jest.fn().mockResolvedValue({ id: 'some-id' }),
createAndUpdateLinkElement: jest.fn().mockResolvedValue({ id: 'some-id' }),
createAndUpdateTextElement: jest.fn().mockResolvedValue({ id: 'some-id' }),
} as unknown as LoadtestClient;
await boarLoadTest.createRandomCard(loadtestClient);

expect(loadtestClient.createCard).toHaveBeenCalled();
}, 100000);
});
});
Loading
Loading