Skip to content

Commit a814af0

Browse files
li-il-liolevskiDario Wirtz
authored
chore: Add load test for classroom scenario (#3203)
Co-authored-by: Tasko Olevski <[email protected]> Co-authored-by: Dario Wirtz <[email protected]> Co-authored-by: Tasko Olevski <[email protected]>
1 parent 74b0b8c commit a814af0

File tree

6 files changed

+473
-145
lines changed

6 files changed

+473
-145
lines changed

load-tests/README.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
# Load testing with k6
22

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

910
## Limitations
1011

1112
- This can log into Renku only and specifically in the cases where Renku has its own built-in gitlab that does
12-
not require a separate log in OR when the gitlab deployment is part of another renku deployment
13+
not require a separate log in OR when the gitlab deployment is part of another renku deployment
1314
- The login flow cannot handle giving authorization when prompted in the oauth flow - do this
14-
for the first time manually then run the tests with the same account
15+
for the first time manually then run the tests with the same account
1516
- The project used to test migrations has to be in a namespace that you control and can create
16-
other projects in. When forks are created they are always created in the same namespace as the
17-
original project with the name being unique.
17+
other projects in. When forks are created they are always created in the same namespace as the
18+
original project with the name being unique.

load-tests/class-scenario.js

+244
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
import http from "k6/http";
2+
import exec from "k6/execution";
3+
import { Trend } from "k6/metrics";
4+
5+
import { renkuLogin } from "./oauth.js";
6+
import { check, fail, sleep } from "k6";
7+
8+
import {
9+
baseUrl,
10+
credentials,
11+
sampleGitProjectUrl,
12+
serverOptions,
13+
} from "./config.js";
14+
15+
export const options = {
16+
scenarios: {
17+
lecture: {
18+
executor: "per-vu-iterations",
19+
vus: 10, // tested up to 30
20+
iterations: 1,
21+
},
22+
},
23+
};
24+
25+
// k6 custom metrics
26+
const sessionStartupTrend = new Trend("session_startup");
27+
28+
function httpRetry(httpRequest, n, logMessage) {
29+
let res,
30+
i = 0;
31+
do {
32+
sleep(i);
33+
res = httpRequest;
34+
console.log(
35+
`${exec.vu.idInInstance}-vu: ${logMessage}, status: ${res.status}, retries: ${i}`
36+
);
37+
i++;
38+
} while (!(res.status >= 200 && res.status < 300) && i < n);
39+
40+
if (res.status >= 400) {
41+
throw new Error(
42+
`${exec.vu.idInInstance}-vu: FAILED ${logMessage}, status: ${res.status}, retry: ${i}`
43+
);
44+
}
45+
46+
return res;
47+
}
48+
49+
function showProjectInfo(baseUrl, gitUrl) {
50+
const payload = {
51+
git_url: gitUrl,
52+
is_delayed: false,
53+
migrate_project: false,
54+
};
55+
const res = http.post(
56+
`${baseUrl}/ui-server/api/renku/project.show`,
57+
JSON.stringify(payload),
58+
{ headers: { "Content-Type": "application/json" } }
59+
);
60+
console.log(res.status);
61+
if (
62+
!check(res, {
63+
"getting project info succeeded with 2XX": (res) =>
64+
res.status >= 200 && res.status < 300,
65+
"getting project info response has no error": (res) =>
66+
res.json().error === undefined,
67+
})
68+
) {
69+
fail(
70+
`getting project info failed with status ${res.status} and body ${res.body}`
71+
);
72+
}
73+
74+
return JSON.parse(res.body);
75+
}
76+
77+
function forkProject(baseUrl, projectInfo) {
78+
const name = projectInfo.result.name;
79+
const projectPathComponents = projectInfo.result.id.split("/");
80+
const path = projectPathComponents.pop();
81+
const namespace_path = projectPathComponents.pop();
82+
const id = namespace_path + "%2F" + path;
83+
84+
const vuIdPostfix = "-" + String(exec.vu.idInInstance);
85+
86+
console.log(`${exec.vu.idInInstance}-vu: project id: ${id}`);
87+
88+
const payload = {
89+
id: id,
90+
name: name + vuIdPostfix,
91+
namespace_path: namespace_path,
92+
path: path + vuIdPostfix,
93+
};
94+
95+
const res = httpRetry(
96+
http.post(
97+
`${baseUrl}/ui-server/api/projects/${id}/fork`,
98+
JSON.stringify(payload),
99+
{ headers: { "Content-Type": "application/json" } }
100+
),
101+
10,
102+
"fork project"
103+
);
104+
105+
return JSON.parse(res.body);
106+
}
107+
108+
function getCommitShas(baseUrl, projectInfo) {
109+
const id = projectInfo.id;
110+
console.log(`${exec.vu.idInInstance}-vu: project id to fork ${id}`);
111+
112+
const res = httpRetry(
113+
http.get(
114+
`${baseUrl}/ui-server/api/projects/${id}/repository/commits?ref_name=master&per_page=100&page=1`
115+
),
116+
10,
117+
"get commit sha"
118+
);
119+
120+
//console.log(`${exec.vu.idInInstance}-vu: commit sha request status: ${res.status}`)
121+
122+
return JSON.parse(res.body);
123+
}
124+
125+
function startServer(baseUrl, forkedProject, commitShas) {
126+
const payload = {
127+
branch: "master",
128+
commit_sha: commitShas[0].id,
129+
namespace: forkedProject.namespace.path,
130+
project: forkedProject.name,
131+
serverOptions: serverOptions,
132+
};
133+
134+
const res = httpRetry(
135+
http.post(
136+
`${baseUrl}/ui-server/api/notebooks/servers`,
137+
JSON.stringify(payload),
138+
{ headers: { "Content-Type": "application/json" } }
139+
),
140+
10,
141+
"start server/session"
142+
);
143+
144+
console.log(
145+
`${exec.vu.idInInstance}-vu: start server, status: ${res.status}`
146+
);
147+
148+
return JSON.parse(res.body);
149+
}
150+
151+
function pollServerStatus(baseUrl, server) {
152+
const serverName = server.name;
153+
console.log(`${exec.vu.idInInstance}-vu: server name: ${serverName}`);
154+
155+
const ServerStates = {
156+
Starting: "starting",
157+
Running: "running",
158+
};
159+
160+
let resBody,
161+
counter = 0;
162+
do {
163+
sleep(1);
164+
resBody = JSON.parse(
165+
http.get(`${baseUrl}/ui-server/api/notebooks/servers/${serverName}`).body
166+
);
167+
counter++;
168+
} while (
169+
resBody.status === undefined ||
170+
resBody.status.state == ServerStates.Starting
171+
);
172+
173+
sessionStartupTrend.add(counter);
174+
175+
return resBody;
176+
}
177+
178+
function stopServer(baseUrl, server) {
179+
const serverName = server.name;
180+
const res = http.del(
181+
`${baseUrl}/ui-server/api/notebooks/servers/${serverName}`
182+
);
183+
184+
return res.status;
185+
}
186+
187+
function deleteProject(baseUrl, projectInfo) {
188+
const id = projectInfo.id;
189+
190+
const res = httpRetry(
191+
http.del(`${baseUrl}/ui-server/api/projects/${id}`),
192+
10,
193+
"delete project"
194+
);
195+
196+
console.log("shuttdown");
197+
198+
return res.status;
199+
}
200+
201+
// Test
202+
203+
export function setup() {
204+
renkuLogin(baseUrl, credentials);
205+
206+
const projectInfo = showProjectInfo(baseUrl, sampleGitProjectUrl);
207+
208+
return projectInfo;
209+
}
210+
211+
export default function test(projectInfo) {
212+
const vu = exec.vu.idInInstance;
213+
214+
sleep(vu); // lets VUs start in sequence
215+
216+
console.log(`${vu}-vu: login to renku`);
217+
renkuLogin(baseUrl, credentials);
218+
219+
console.log(`${vu}-vu: fork 'test' project -> 'test-${vu}'`);
220+
const forkedProject = forkProject(baseUrl, projectInfo);
221+
222+
sleep(90); // workaround
223+
224+
console.log(`${vu}-vu: get latest commit hash from forked project`);
225+
const commitShas = getCommitShas(baseUrl, forkedProject);
226+
227+
console.log(`${vu}-vu: start server/session with latest commit`);
228+
const server = startServer(baseUrl, forkedProject, commitShas);
229+
230+
console.log(`${vu}-vu: wait for server to enter state 'running'`);
231+
pollServerStatus(baseUrl, server);
232+
console.log(`${vu}-vu: server 'running'`);
233+
234+
console.log(`${vu}-vu: let server run for 200 seconds`);
235+
sleep(200);
236+
237+
console.log(`${vu}-vu: shutdown server`);
238+
stopServer(baseUrl, server);
239+
240+
console.log(`${vu}-vu: delete 'project-${vu}'`);
241+
deleteProject(baseUrl, forkedProject);
242+
243+
console.log(`${vu}-vu: test finished`);
244+
}

load-tests/example.config.js

+17-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
export const baseUrl = "https://dev.renku.ch"
1+
export const baseUrl = "https://dev.renku.ch";
22
// oldGitlabProjectId has to point to a project that resides in a namespace that the user
33
// has at least maintainer access to. This is because the load tests will fork this project
44
// into the same namespace as where the original project resides and only generate a uuid-like
55
// name for the project. So if you point to a project that resides in a namespace to which
66
// the test runner has no permissions, the forking part of the tests will fail.
7-
export const oldGitlabProjectId = 5011
7+
export const oldGitlabProjectId = 5011;
88
// This project is used to test calling api/renku/project.show, the project is not forked
99
// and it does not have the same strict requirements as the project mentioned above. Any
1010
// public project should work here (whether the user has write access to it or not).
11-
export const sampleGitProjectUrl = "https://dev.renku.ch/gitlab/tasko.olevski/test-project-2.git"
11+
export const sampleGitProjectUrl =
12+
"https://dev.renku.ch/gitlab/tasko.olevski/test-project-2.git";
1213

1314
// Two sets of credentials are needed only if the Renku deployment
1415
// has a separate Gitlab that requires logging into another Renku
@@ -18,10 +19,20 @@ export const sampleGitProjectUrl = "https://dev.renku.ch/gitlab/tasko.olevski/te
1819
export const credentials = [
1920
{
2021
username: "[email protected]",
21-
password: "secret-password1"
22+
password: "secret-password1",
2223
},
2324
{
2425
username: "[email protected]",
25-
password: "secret-password1"
26+
password: "secret-password1",
2627
},
27-
]
28+
];
29+
30+
// Describes the configuration of a Jupyter Server.
31+
export const serverOptions = {
32+
cpu_request: 0.5,
33+
defaultUrl: "/lab",
34+
disk_request: "1G",
35+
gpu_request: 0,
36+
lfs_auto_fetch: false,
37+
mem_request: "1G",
38+
};

0 commit comments

Comments
 (0)