Skip to content

feat(load-test): Add load test for classroom scenario #3203

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

Merged
merged 6 commits into from
Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
11 changes: 6 additions & 5 deletions load-tests/README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
# Load testing with k6

## To run

1. Download k6: https://k6.io/docs/get-started/installation/ - you can also get a binary from the github page
2. Enter the credentials and deployment you wish to test in a file named `config.js`, you can
use the `example.config.js` to start - just make a copy and rename it.
use the `example.config.js` to start - just make a copy and rename it.
3. Run the tests with `k6 run testFileName.js`

## Limitations

- This can log into Renku only and specifically in the cases where Renku has its own built-in gitlab that does
not require a separate log in OR when the gitlab deployment is part of another renku deployment
not require a separate log in OR when the gitlab deployment is part of another renku deployment
- The login flow cannot handle giving authorization when prompted in the oauth flow - do this
for the first time manually then run the tests with the same account
for the first time manually then run the tests with the same account
- The project used to test migrations has to be in a namespace that you control and can create
other projects in. When forks are created they are always created in the same namespace as the
original project with the name being unique.
other projects in. When forks are created they are always created in the same namespace as the
original project with the name being unique.
244 changes: 244 additions & 0 deletions load-tests/class-scenario.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import http from "k6/http";
import exec from "k6/execution";
import { Trend } from "k6/metrics";

import { renkuLogin } from "./oauth.js";
import { check, fail, sleep } from "k6";

import {
baseUrl,
credentials,
sampleGitProjectUrl,
serverOptions,
} from "./config.js";

export const options = {
scenarios: {
lecture: {
executor: "per-vu-iterations",
vus: 10, // tested up to 30
iterations: 1,
},
},
};

// k6 custom metrics
const sessionStartupTrend = new Trend("session_startup");

function httpRetry(httpRequest, n, logMessage) {
let res,
i = 0;
do {
sleep(i);
res = httpRequest;
console.log(
`${exec.vu.idInInstance}-vu: ${logMessage}, status: ${res.status}, retries: ${i}`
);
i++;
} while (!(res.status >= 200 && res.status < 300) && i < n);

if (res.status >= 400) {
throw new Error(
`${exec.vu.idInInstance}-vu: FAILED ${logMessage}, status: ${res.status}, retry: ${i}`
);
}

return res;
}

function showProjectInfo(baseUrl, gitUrl) {
const payload = {
git_url: gitUrl,
is_delayed: false,
migrate_project: false,
};
const res = http.post(
`${baseUrl}/ui-server/api/renku/project.show`,
JSON.stringify(payload),
{ headers: { "Content-Type": "application/json" } }
);
console.log(res.status);
if (
!check(res, {
"getting project info succeeded with 2XX": (res) =>
res.status >= 200 && res.status < 300,
"getting project info response has no error": (res) =>
res.json().error === undefined,
})
) {
fail(
`getting project info failed with status ${res.status} and body ${res.body}`
);
}

return JSON.parse(res.body);
}

function forkProject(baseUrl, projectInfo) {
const name = projectInfo.result.name;
const projectPathComponents = projectInfo.result.id.split("/");
const path = projectPathComponents.pop();
const namespace_path = projectPathComponents.pop();
const id = namespace_path + "%2F" + path;

const vuIdPostfix = "-" + String(exec.vu.idInInstance);

console.log(`${exec.vu.idInInstance}-vu: project id: ${id}`);

const payload = {
id: id,
name: name + vuIdPostfix,
namespace_path: namespace_path,
path: path + vuIdPostfix,
};

const res = httpRetry(
http.post(
`${baseUrl}/ui-server/api/projects/${id}/fork`,
JSON.stringify(payload),
{ headers: { "Content-Type": "application/json" } }
),
10,
"fork project"
);

return JSON.parse(res.body);
}

function getCommitShas(baseUrl, projectInfo) {
const id = projectInfo.id;
console.log(`${exec.vu.idInInstance}-vu: project id to fork ${id}`);

const res = httpRetry(
http.get(
`${baseUrl}/ui-server/api/projects/${id}/repository/commits?ref_name=master&per_page=100&page=1`
),
10,
"get commit sha"
);

//console.log(`${exec.vu.idInInstance}-vu: commit sha request status: ${res.status}`)

return JSON.parse(res.body);
}

function startServer(baseUrl, forkedProject, commitShas) {
const payload = {
branch: "master",
commit_sha: commitShas[0].id,
namespace: forkedProject.namespace.path,
project: forkedProject.name,
serverOptions: serverOptions,
};

const res = httpRetry(
http.post(
`${baseUrl}/ui-server/api/notebooks/servers`,
JSON.stringify(payload),
{ headers: { "Content-Type": "application/json" } }
),
10,
"start server/session"
);

console.log(
`${exec.vu.idInInstance}-vu: start server, status: ${res.status}`
);

return JSON.parse(res.body);
}

function pollServerStatus(baseUrl, server) {
const serverName = server.name;
console.log(`${exec.vu.idInInstance}-vu: server name: ${serverName}`);

const ServerStates = {
Starting: "starting",
Running: "running",
};

let resBody,
counter = 0;
do {
sleep(1);
resBody = JSON.parse(
http.get(`${baseUrl}/ui-server/api/notebooks/servers/${serverName}`).body
);
counter++;
} while (
resBody.status === undefined ||
resBody.status.state == ServerStates.Starting
);

sessionStartupTrend.add(counter);

return resBody;
}

function stopServer(baseUrl, server) {
const serverName = server.name;
const res = http.del(
`${baseUrl}/ui-server/api/notebooks/servers/${serverName}`
);

return res.status;
}

function deleteProject(baseUrl, projectInfo) {
const id = projectInfo.id;

const res = httpRetry(
http.del(`${baseUrl}/ui-server/api/projects/${id}`),
10,
"delete project"
);

console.log("shuttdown");

return res.status;
}

// Test

export function setup() {
renkuLogin(baseUrl, credentials);

const projectInfo = showProjectInfo(baseUrl, sampleGitProjectUrl);

return projectInfo;
}

export default function test(projectInfo) {
const vu = exec.vu.idInInstance;

sleep(vu); // lets VUs start in sequence

console.log(`${vu}-vu: login to renku`);
renkuLogin(baseUrl, credentials);

console.log(`${vu}-vu: fork 'test' project -> 'test-${vu}'`);
const forkedProject = forkProject(baseUrl, projectInfo);

sleep(90); // workaround

console.log(`${vu}-vu: get latest commit hash from forked project`);
const commitShas = getCommitShas(baseUrl, forkedProject);

console.log(`${vu}-vu: start server/session with latest commit`);
const server = startServer(baseUrl, forkedProject, commitShas);

console.log(`${vu}-vu: wait for server to enter state 'running'`);
pollServerStatus(baseUrl, server);
console.log(`${vu}-vu: server 'running'`);

console.log(`${vu}-vu: let server run for 200 seconds`);
sleep(200);

console.log(`${vu}-vu: shutdown server`);
stopServer(baseUrl, server);

console.log(`${vu}-vu: delete 'project-${vu}'`);
deleteProject(baseUrl, forkedProject);

console.log(`${vu}-vu: test finished`);
}
23 changes: 17 additions & 6 deletions load-tests/example.config.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
export const baseUrl = "https://dev.renku.ch"
export const baseUrl = "https://dev.renku.ch";
// oldGitlabProjectId has to point to a project that resides in a namespace that the user
// has at least maintainer access to. This is because the load tests will fork this project
// into the same namespace as where the original project resides and only generate a uuid-like
// name for the project. So if you point to a project that resides in a namespace to which
// the test runner has no permissions, the forking part of the tests will fail.
export const oldGitlabProjectId = 5011
export const oldGitlabProjectId = 5011;
// This project is used to test calling api/renku/project.show, the project is not forked
// and it does not have the same strict requirements as the project mentioned above. Any
// public project should work here (whether the user has write access to it or not).
export const sampleGitProjectUrl = "https://dev.renku.ch/gitlab/tasko.olevski/test-project-2.git"
export const sampleGitProjectUrl =
"https://dev.renku.ch/gitlab/tasko.olevski/test-project-2.git";

// Two sets of credentials are needed only if the Renku deployment
// has a separate Gitlab that requires logging into another Renku
Expand All @@ -18,10 +19,20 @@ export const sampleGitProjectUrl = "https://dev.renku.ch/gitlab/tasko.olevski/te
export const credentials = [
{
username: "[email protected]",
password: "secret-password1"
password: "secret-password1",
},
{
username: "[email protected]",
password: "secret-password1"
password: "secret-password1",
},
]
];

// Describes the configuration of a Jupyter Server.
export const serverOptions = {
cpu_request: 0.5,
defaultUrl: "/lab",
disk_request: "1G",
gpu_request: 0,
lfs_auto_fetch: false,
mem_request: "1G",
};
Loading