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-6854 - basic load tests #5099

Merged
merged 24 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
71530e0
improve performance of findCards
hoeppner-dataport Jul 3, 2024
26c5b8e
track number action calls in separate metrics
hoeppner-dataport Jul 4, 2024
0307e61
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Jul 4, 2024
4019bad
chore: switch from counter to gauge
hoeppner-dataport Jul 4, 2024
f2efa95
chore: log inc of counter metrics
hoeppner-dataport Jul 5, 2024
08f3410
update to newer version of prom-client
hoeppner-dataport Jul 5, 2024
a6bf2eb
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Jul 5, 2024
df0cc61
chore: log executionTimes
hoeppner-dataport Jul 5, 2024
5609e4a
chore: add 0.5 percentile0
hoeppner-dataport Jul 5, 2024
bb7d68f
chore: change maxAge
hoeppner-dataport Jul 5, 2024
513b361
chore: reset to 600s maxAge
hoeppner-dataport Jul 5, 2024
eb7b13d
use gauge and counter
hoeppner-dataport Jul 5, 2024
02f5c47
implementation of load testing
hoeppner-dataport Jul 9, 2024
ffe7077
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Jul 9, 2024
d9cb422
added option to pass target and scenerio as parameters
hoeppner-dataport Jul 9, 2024
76b14bc
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Jul 11, 2024
a28cc32
chore: take credentials from environment variables or user input
hoeppner-dataport Jul 11, 2024
416689a
chore: extend gitignore
hoeppner-dataport Jul 11, 2024
ea1c78f
add steps for windows
Metauriel Jul 11, 2024
9f6e0fa
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Jul 15, 2024
98fe3b8
Merge branch 'BC-6854-basic-load-tests' of github.com:hpi-schul-cloud…
hoeppner-dataport Jul 15, 2024
a2e6eb1
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Jul 15, 2024
325d9dc
chore: fix tests
hoeppner-dataport Jul 15, 2024
5ebe901
chore: remove unneeded import
hoeppner-dataport Jul 15, 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,5 @@ build
/coverage
/.nyc_output
/.idea/
/apps/server/src/modules/board/loadtest/**/*.html
/apps/server/src/modules/board/loadtest/artilleryreport.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect {
trackExecutionTime(methodName: string, executionTimeMs: number) {
if (this.metricsService) {
this.metricsService.setExecutionTime(methodName, executionTimeMs);
this.metricsService.incrementActionCount(methodName);
this.metricsService.incrementActionGauge(methodName);
this.metricsService.incrementActionCount('all');
this.metricsService.incrementActionGauge('all');
}
}

Expand Down Expand Up @@ -128,6 +132,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect {
}

@SubscribeMessage('update-card-height-request')
@TrackExecutionTime()
@UseRequestContext()
async updateCardHeight(socket: Socket, data: UpdateCardHeightMessageParams) {
const emitter = this.buildBoardSocketEmitter({ socket, action: 'update-card-height' });
Expand All @@ -142,6 +147,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect {
}

@SubscribeMessage('delete-card-request')
@TrackExecutionTime()
@UseRequestContext()
async deleteCard(socket: Socket, data: DeleteCardMessageParams) {
const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-card' });
Expand Down Expand Up @@ -178,6 +184,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect {
}

@SubscribeMessage('create-column-request')
@TrackExecutionTime()
@UseRequestContext()
async createColumn(socket: Socket, data: CreateColumnMessageParams) {
const emitter = this.buildBoardSocketEmitter({ socket, action: 'create-column' });
Expand Down Expand Up @@ -219,6 +226,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect {
}

@SubscribeMessage('move-card-request')
@TrackExecutionTime()
@UseRequestContext()
async moveCard(socket: Socket, data: MoveCardMessageParams) {
const emitter = this.buildBoardSocketEmitter({ socket, action: 'move-card' });
Expand All @@ -233,6 +241,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect {
}

@SubscribeMessage('move-column-request')
@TrackExecutionTime()
@UseRequestContext()
async moveColumn(socket: Socket, data: MoveColumnMessageParams) {
const emitter = this.buildBoardSocketEmitter({ socket, action: 'move-column' });
Expand Down Expand Up @@ -267,6 +276,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect {
}

@SubscribeMessage('update-board-visibility-request')
@TrackExecutionTime()
@UseRequestContext()
async updateBoardVisibility(socket: Socket, data: UpdateBoardVisibilityMessageParams) {
const emitter = this.buildBoardSocketEmitter({ socket, action: 'update-board-visibility' });
Expand All @@ -281,6 +291,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect {
}

@SubscribeMessage('delete-column-request')
@TrackExecutionTime()
@UseRequestContext()
async deleteColumn(socket: Socket, data: DeleteColumnMessageParams) {
const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-column' });
Expand Down Expand Up @@ -312,6 +323,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect {
}

@SubscribeMessage('create-element-request')
@TrackExecutionTime()
@UseRequestContext()
async createElement(socket: Socket, data: CreateContentElementMessageParams) {
const emitter = this.buildBoardSocketEmitter({ socket, action: 'create-element' });
Expand Down Expand Up @@ -346,6 +358,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect {
}

@SubscribeMessage('delete-element-request')
@TrackExecutionTime()
@UseRequestContext()
async deleteElement(socket: Socket, data: DeleteContentElementMessageParams) {
const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-element' });
Expand All @@ -361,6 +374,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect {
}

@SubscribeMessage('move-element-request')
@TrackExecutionTime()
@UseRequestContext()
async moveElement(socket: Socket, data: MoveContentElementMessageParams) {
const emitter = this.buildBoardSocketEmitter({ socket, action: 'move-element' });
Expand Down
76 changes: 76 additions & 0 deletions apps/server/src/modules/board/loadtest/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Loadtesting the boards

The socket.io documentation suggests to use the tool artillery in order to load test a socket-io tool like our board-collaboration service.

For defining scenarios you need to use/create Yaml-files that define which operations with which parameters need to be executed in which order.

Some sceneraios were already prepared and are stored in the subfolder scenarios.

## install artillery

To run artillery from your local environment you need to install it first including an adapter that supports socketio-v3-websocket communication:

```sh
npm install -g artillery artillery-engine-socketio-v3
```

## manual execution

To execute a scenario you can run artillery from the shell / commandline...:

Using the `--variables` parameter it is possible to define several variables and there values that can be used in the scenerio-yaml-file:

- **target**: defines the base url for all requests (REST and WebSocket)
e.g. `https://main.dbc.dbildungscloud.dev`
- **token**: a valid JWT for the targeted system
- **board_id**: id of an existing board the tests should be executed on

```bash
npx artillery run --variables "{'target': 'https://main.dbc.dbildungscloud.dev', 'token': 'eJ....', 'board_id': '668d0e03bf3689d12e1e86fb' }" './scenarios/3users.yml' --output artilleryreport.json
```

On Windows Powershell, the variables value needs to be wrapped in singlequotes, and inside the json you need to use backslash-escaped doublequotes:

```powershell
npx artillery run --variables '{\"target\": \"https://main.dbc.dbildungscloud.dev\", \"token\": \"eJ....\", \"board_id\": \"668d0e03bf3689d12e1e86fb\" }' './scenarios/3users.yml' --output artilleryreport.json
```

## visualizing the recorded results

It is possible to generate a HTML-report based on the recorded data.

```powershell
npx artillery report --output=$board_title.html artilleryreport.json
```

## automatic execution

You can run one of the existing scenarios by executing:

```bash
bash runScenario.sh
```

This will:

1. let you choose from scenario-files
2. create a fresh JWT-webtoken
3. create a fresh board (in one of the courses) the user has access to
4. name the board by a combination of datetime and the scenario name.
5. output a link to the generated board (in order open and see the test live)
6. start the execution of the scenario against this newly created board
7. generate a html report in the end

You can also provide the target as the first and the name of the scenario as the second parameter - to avoid the need to select those. Here is an example:

```bash
bash runScenario.sh https://bc-6854-basic-load-tests.nbc.dbildungscloud.dev 3users
```

## password

By typeing `export CARL_CORD_PASSWORD=realpassword` the script will not ask you anymore for the password to create a token.

## Todos

- [ ] enable optional parameter course_id
139 changes: 139 additions & 0 deletions apps/server/src/modules/board/loadtest/runScenario.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/bin/bash

function select_target() {
declare -a targets=("https://main.nbc.dbildungscloud.dev" "https://bc-6854-basic-load-tests.nbc.dbildungscloud.dev")
echo "Please select the target for the test:" >&2
select target in "${targets[@]}"; do
if [[ -n $target ]]; then
break
else
echo "Invalid selection. Please try again." >&2
fi
done
}

function select_scenario() {
# list files in the scenarios directory
scenarios_dir="./scenarios"
declare -a scenario_files=($(ls $scenarios_dir))

echo "Please select a scenario file for the test:" >&2
select scenario_file in "${scenario_files[@]}"; do
if [[ -n $scenario_file ]]; then
echo "You have selected: $scenario_file" >&2
break
else
echo "Invalid selection. Please try again." >&2
fi
done

scenario_name="${scenario_file%.*}"
}

function get_credentials() {
if [ -z "$CARL_CORD_PASSWORD" ]; then
echo "Password for Carl Cord is unknown. Provide it as an enviroment variable (CARL_CORD_PASSWORD) or enter it:"
read CARL_CORD_PASSWORD
export CARL_CORD_PASSWORD
fi
}

function get_token() {
response=$(curl -s -f -X 'POST' \
"$target/api/v3/authentication/local" \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d "{
\"username\": \"[email protected]\",
\"password\": \"$CARL_CORD_PASSWORD\"
}")

if [ $? -ne 0 ]; then
echo "ERROR: Failed to get token. Please check your credentials and target URL." >&2
exit 1
fi

token=$(echo $response | sed -n 's/.*"accessToken":"\([^"]*\)".*/\1/p')
}

function get_course_id() {
response=$(curl -s -f -X 'GET' \
"$target/api/v3/courses" \
-H "Accept: application/json" \
-H "Authorization: Bearer $token")

if [ $? -ne 0 ]; then
echo "ERROR: Failed to get course list. Please check your credentials and target URL." >&2
exit 1
fi

course_id=$(echo $response | sed -n 's/.*"id":"\([^"]*\)".*/\1/p')
}

function create_board_title() {
current_date=$(date +%Y-%m-%d_%H:%M)
board_title="${current_date}_$1"
}

function create_board() {
response=$(curl -s -f -X 'POST' \
"$target/api/v3/boards" \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $token" \
-d "{
\"title\": \"$board_title\",
\"parentId\": \"$course_id\",
\"parentType\": \"course\",
\"layout\": \"columns\"
}")

if [ $? -ne 0 ]; then
echo "ERROR: Failed to create a board." >&2
exit 1
fi

board_id=$(echo $response | sed -n 's/.*"id":"\([^"]*\)".*/\1/p' )
}

if [ -z "$1" ]; then
select_target
else
target=$1
fi
echo " "
echo "target: $target"


if [ -z "$2" ]; then
select_scenario
echo "scenario_name: $scenario_name"
else
scenario_name="$2"
scenario_name=${scenario_name//.yml/}
fi
echo "scenario_name: $scenario_name"

get_credentials

get_token
echo "token: ${token:0:50}..."
echo " "

get_course_id
echo "course_id: $course_id"
echo " "

create_board_title $scenario_name
echo "board_title: $board_title"

create_board
echo "board_id $board_id"

echo "board: $target/rooms/$board_id/board"
echo " "
echo "Running artillery test..."

npx artillery run --variables "{\"target\": \"$target\", \"token\": \"$token\", \"board_id\": \"$board_id\" }" "./scenarios/$scenario_name.yml" --output artilleryreport.json

npx artillery report --output=$board_title.html artilleryreport.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
config:
target: '{{ target }}'
engines:
socketio:
transport: ['websocket', 'polling']
path: '/board-collaboration'
socketio-v3:
path: '/board-collaboration'
timeout: 1000000
extraHeaders:
Cookie: 'jwt={{ token }}'

phases:
- duration: 300
arrivalRate: 10
maxVusers: 30

scenarios:
- name: create card
engine: socketio-v3
socketio-v3:
extraHeaders:
Cookie: 'jwt={{ token }}'
flow:
- think: 1

- emit:
channel: 'fetch-board-request'
data:
boardId: '{{ board_id }}'

- think: 1

- emit:
channel: 'create-column-request'
data:
boardId: '{{ board_id }}'
response:
on: 'create-column-success'
capture:
- json: $.newColumn.id
as: columnId

- think: 1

- loop:
- emit:
channel: 'create-card-request'
data:
columnId: '{{ columnId}}'
response:
on: 'create-card-success'
capture:
- json: $.newCard.id
as: cardId

- think: 1

- emit:
channel: 'fetch-card-request'
data:
cardIds:
- '{{ cardId }}'

- think: 2

- emit:
channel: 'update-card-title-request'
data:
cardId: '{{ cardId }}'
newTitle: 'Card {{ cardId}}'

- think: 1

count: 20
Loading
Loading