Add k6 tests

This commit is contained in:
Florian THIERRY
2025-09-19 17:41:46 +02:00
parent d6a06ba6cd
commit 193fcc1596
28 changed files with 4293 additions and 2 deletions

3
.gitignore vendored
View File

@@ -41,3 +41,6 @@ out/
### Local postgresql database data ###
/local/postgresql/pgdata
**/node_modules
**/dist

21
k6-example/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Sebastian Southern
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

40
k6-example/README.md Normal file
View File

@@ -0,0 +1,40 @@
# K6 Example
This repository contains a starting point for running k6 tests in typescript. It uses a publicly hosted endpoint so that anyone with internet access can test out the API themselves and then use this repo as a starting point to continue building their own tests.
## Pre-Reqs
- Download `k6` from here https://k6.io/docs/get-started/installation/
- Install dependencies `yarn install`
- Docker
### Installing xk6-dashboard with Docker
[xk6-dashboard](https://github.com/grafana/xk6-dashboard) is a k6 extension that can be used to visualise your performance test in real time.
To run the tests with monitoring with xk6-dashboard extension, we need to install it. The simplest way to install is via docker and can be done via
`docker pull ghcr.io/grafana/xk6-dashboard:0.6.1`
## Tests
### reqres
We use the [reqres](https://reqres.in/) publicly hosted REST API to showcase the testing with k6
To execute the first sample test that showcases how `per-vu-iterations` works, you can run:
`yarn test:demo`
To test with monitoring in place, run:
`yarn test-with-monitoring:demo`
To execute the second sample test that showcases how to use `stages`, you can run:
`yarn test:demo-stages`
To test with monitoring in place, run:
`yarn test-with-monitoring:demo-stages`

View File

@@ -0,0 +1,3 @@
target:
url: http://localhost
port: 51001

2208
k6-example/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
k6-example/package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "k6-example",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"type": "module",
"scripts": {
"build": "vite build",
"test:demo": "yarn build && k6 run dist/tests/load-tests.cjs",
"test:demo-stages": "yarn build && k6 run dist/tests/reqres-stages.cjs",
"test-with-monitoring:demo": "yarn build && docker run --platform linux/amd64 -it -p 5665:5665 -v $(pwd)/dist/:/src ghcr.io/grafana/xk6-dashboard:0.6.1 run --out 'dashboard=period=2s' /src/tests/reqres.cjs",
"test-with-monitoring:demo-stages": "yarn build && docker run --platform linux/amd64 -it -p 5665:5665 -v $(pwd)/dist/:/src ghcr.io/grafana/xk6-dashboard:0.6.1 run --out 'dashboard=period=2s' /src/tests/reqres-stages.cjs"
},
"devDependencies": {
"@babel/core": "7.23.3",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/js-yaml": "^4.0.9",
"@types/k6": "~0.47.3",
"@types/node": "^24.5.2",
"rollup-plugin-copy": "^3.5.0",
"typescript": "5.3.2",
"vite": "^5.0.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"dependencies": {
"js-yaml": "^4.1.0"
}
}

View File

@@ -0,0 +1,36 @@
import {Marketplace} from "../data/marketplace";
import http from "k6/http";
import {logWaitingTime} from "../utils/logger";
import {Trend} from "k6/metrics";
import {check} from "k6";
import {Response} from "../data/common";
const marketplaceUrl = 'http://localhost:51001'
// Metrics that we want to track
const metrics = {
getMarketplace: new Trend("get_marketplace_time", true),
};
export const getMarketplace = (): Response<Marketplace> => {
const url = `${marketplaceUrl}/api/marketplace`;
const response = http.get(url);
// console.log('response:', response);
logWaitingTime({
metric: metrics.getMarketplace,
response: response,
messageType: 'Get Marketplace',
});
check(response, {
'Get Marketplace: is 200': r => r.status === 200
});
return {
data: {} as Marketplace,
statusCode: response.status
}
}

View File

@@ -0,0 +1,113 @@
import { check } from "k6";
import http, { StructuredRequestBody } from "k6/http";
import { Trend } from "k6/metrics";
import { logWaitingTime } from "../utils/logger";
import { Response } from '../data/common';
export type User = {
id: number;
email: string;
first_name: string;
last_name: string;
avatar: string;
};
// Metrics that we want to track
const metrics = {
getUserResponseTime: new Trend("get_user_response_time", true),
updateUserResponseTime: new Trend("update_user_response_time", true),
deleteUserResponseTime: new Trend("delete_user_response_time", true),
};
// Endpoint that we test against
const reqResUrl = "https://reqres.in/api";
/**
* getUser makes a GET request to the /users/:id endpoint and asserts
* that the response is 200 and that the user id and email match the
* user object that was passed in.
* @param user
* @returns Response<User>
*/
export const getUser = (user: User): Response<User> => {
const url = reqResUrl;
const params = {
headers: {},
};
const res = http.get(`${url}/users/${user.id}`, params);
const jsonRes = res.json() as { data: User };
logWaitingTime({
metric: metrics.getUserResponseTime,
response: res,
messageType: `Get User`,
});
check(res, {
"Get User By Id: is 200": (r) => r.status === 200,
"Get User By Id: has correct id": (_) => jsonRes.data.id === user.id,
"Get User By Id: has correct email": (_) =>
jsonRes.data.email === user.email,
});
return {
data: jsonRes.data,
statusCode: res.status,
};
};
type UpdateUserResponse = {
updatedAt: string;
job: string;
}
export const updateUser = (user: User): Response<UpdateUserResponse> => {
const url = reqResUrl;
const params = {
headers: {},
};
const jobTitle = "QA Engineer";
const payload: StructuredRequestBody = {
job: jobTitle,
};
const res = http.patch(`${url}/users/${user.id}`, payload, params);
const jsonRes = res.json() as UpdateUserResponse
logWaitingTime({
metric: metrics.updateUserResponseTime,
response: res,
messageType: `Update User`,
});
check(res, {
"Update User By Id: is 200": (r) => r.status === 200,
"Update User By Id: has correct updatedAt": (_) => jsonRes.updatedAt !== "",
"Update User By Id: has correct job title": (_) => jsonRes.job === jobTitle,
});
return {
data: jsonRes,
statusCode: res.status,
};
};
export const deleteUser = (userId: number): Response<{}> => {
const url = reqResUrl;
const res = http.del(`${url}/users/${userId}`);
logWaitingTime({
metric: metrics.deleteUserResponseTime,
response: res,
messageType: `Delete User`,
});
check(res, {
"Delete User By Id: is 204": (r) => r.status === 204,
});
return {
data: {},
statusCode: res.status,
};
};

View File

@@ -0,0 +1,4 @@
export type Response<T> = {
data: T;
statusCode: number;
};

View File

@@ -0,0 +1,15 @@
export interface Catalog {
id: string;
name: string;
}
export interface Item {
id: string;
name: string;
isShared: boolean;
}
export interface Marketplace {
catalogs: Catalog[];
sharedItems: Item[]
}

View File

@@ -0,0 +1,86 @@
export const users = [
{
id: 1,
email: "george.bluth@reqres.in",
first_name: "George",
last_name: "Bluth",
avatar: "https://reqres.in/img/faces/1-image.jpg",
},
{
id: 2,
email: "janet.weaver@reqres.in",
first_name: "Janet",
last_name: "Weaver",
avatar: "https://reqres.in/img/faces/2-image.jpg",
},
{
id: 3,
email: "emma.wong@reqres.in",
first_name: "Emma",
last_name: "Wong",
avatar: "https://reqres.in/img/faces/3-image.jpg",
},
{
id: 4,
email: "eve.holt@reqres.in",
first_name: "Eve",
last_name: "Holt",
avatar: "https://reqres.in/img/faces/4-image.jpg",
},
{
id: 5,
email: "charles.morris@reqres.in",
first_name: "Charles",
last_name: "Morris",
avatar: "https://reqres.in/img/faces/5-image.jpg",
},
{
id: 6,
email: "tracey.ramos@reqres.in",
first_name: "Tracey",
last_name: "Ramos",
avatar: "https://reqres.in/img/faces/6-image.jpg",
},
{
id: 7,
email: "michael.lawson@reqres.in",
first_name: "Michael",
last_name: "Lawson",
avatar: "https://reqres.in/img/faces/7-image.jpg",
},
{
id: 8,
email: "lindsay.ferguson@reqres.in",
first_name: "Lindsay",
last_name: "Ferguson",
avatar: "https://reqres.in/img/faces/8-image.jpg",
},
{
id: 9,
email: "tobias.funke@reqres.in",
first_name: "Tobias",
last_name: "Funke",
avatar: "https://reqres.in/img/faces/9-image.jpg",
},
{
id: 10,
email: "byron.fields@reqres.in",
first_name: "Byron",
last_name: "Fields",
avatar: "https://reqres.in/img/faces/10-image.jpg",
},
{
id: 11,
email: "george.edwards@reqres.in",
first_name: "George",
last_name: "Edwards",
avatar: "https://reqres.in/img/faces/11-image.jpg",
},
{
id: 12,
email: "rachel.howell@reqres.in",
first_name: "Rachel",
last_name: "Howell",
avatar: "https://reqres.in/img/faces/12-image.jpg",
},
];

View File

@@ -0,0 +1,3 @@
export const VUs = 1

View File

@@ -0,0 +1,22 @@
// @ts-ignore
import { vu } from "k6/execution";
import { Options } from "k6/options";
import { logger } from "../utils/logger";
import {getMarketplace} from "../apis/marketplace-apis";
export const options: Options = {
scenarios: {
login: {
executor: 'per-vu-iterations',
vus: 50,
iterations: 10,
maxDuration: '1h30m',
},
},
};
export default function test () {
logger.info(`Running iteration ${vu.iterationInInstance}`);
getMarketplace();
}

View File

@@ -0,0 +1,34 @@
import { SharedArray } from "k6/data";
import { vu } from "k6/execution";
import { Options } from "k6/options";
import { deleteUser, getUser, updateUser } from "../apis/reqres";
import { users } from "../data/users";
import { logger } from "../utils/logger";
const data = new SharedArray("users", function () {
return users;
});
export const options: Options = {
stages: [
// Ramp up to 12 users over 30 seconds
{ duration: "1m", target: 12 },
// Maintain steady state of 12 users over the next two minutes
{ duration: "2m", target: 12 },
// Ramp down to 0 users over the next 30 seconds
{ duration: "30s", target: 0 },
],
};
export default function test() {
// Get a random user from data that isn't currently being tested
const user = data[vu.idInTest - 1];
logger.info(
`Running iteration ${vu.iterationInInstance} for user id ${user.id} with name ${user.first_name} ${user.last_name}`
);
getUser(user);
updateUser(user);
deleteUser(user.id);
}

View File

@@ -0,0 +1,35 @@
import { SharedArray } from "k6/data";
// @ts-ignore
import { vu } from "k6/execution";
import { Options } from "k6/options";
import { deleteUser, getUser, updateUser } from "../apis/reqres";
import { users } from "../data/users";
import { logger } from "../utils/logger";
const data = new SharedArray("users", function () {
return users;
});
export const options: Options = {
scenarios: {
login: {
executor: 'per-vu-iterations',
vus: 5,
iterations: 10,
maxDuration: '1h30m',
},
},
};
export default function test () {
// Get a random user from data that isn't currently being tested
const user = data[vu.idInTest - 1];
logger.info(
`Running iteration ${vu.iterationInInstance} for user id ${user.id} with name ${user.first_name} ${user.last_name}`
);
getUser(user);
updateUser(user);
deleteUser(user.id);
}

View File

@@ -0,0 +1,39 @@
import * as fs from 'fs';
import * as yaml from 'js-yaml';
import * as path from 'path';
const FILE_ENCODING = 'utf-8';
const YAML_FILE_PATH = '../configuration.yml';
export interface Configuration {
target: TargetConfiguration;
}
export interface TargetConfiguration {
url: string;
port: string;
}
export function loadConfiguration(): Configuration {
try {
const fileContent = fs.readFileSync(path.resolve(__dirname, YAML_FILE_PATH), FILE_ENCODING);
const data = yaml.load(fileContent);
return {
target: {
// @ts-ignore
url: data['target']['url'],
// @ts-ignore
port: data['target']['port']
}
};
} catch (e) {
let error = {
message: 'Error while loading the configuration.',
cause: e
};
console.error(error);
throw error;
}
}

View File

@@ -0,0 +1,49 @@
export const getTimestamp = (): string => {
const date = new Date();
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
const seconds = date.getSeconds().toString().padStart(2, "0");
const milliseconds = date.getMilliseconds().toString().padStart(3, "0");
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
};
export const logger = {
info(...val: any): void {
console.log(getTimestamp(), ...val);
},
warn(...val: any): void {
console.warn(getTimestamp(), ...val);
},
error(...val: any): void {
console.error(getTimestamp(), ...val);
},
};
export const logWaitingTime = ({
metric,
response,
messageType,
}: {
metric: any;
response: any;
messageType: string;
}): void => {
const responseTimeThreshold = 5000;
let correlationId = "";
let responseTime = response.timings.waiting;
try {
let json = response.json();
correlationId = json.correlationId;
} catch (err) {
// noop
}
// Log any responses that far longer than expected so we can troubleshoot those particular queries
if (responseTime > responseTimeThreshold) {
logger.warn(
`${messageType} with correlationId '${correlationId}' took longer than ${responseTimeThreshold}`
);
}
metric.add(responseTime);
};

26
k6-example/tsconfig.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"moduleResolution": "node",
"module": "commonjs",
"noEmit": true,
"allowJs": true,
"removeComments": false,
"strict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"skipLibCheck": true,
}
}

67
k6-example/vite.config.js Normal file
View File

@@ -0,0 +1,67 @@
import { defineConfig } from 'vite';
import { babel } from '@rollup/plugin-babel';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import copy from 'rollup-plugin-copy';
import fg from 'fast-glob';
/**
* Gets the entrypoints of files within a set of given paths (i.e src/tests).
* This is useful as when we execute k6, we run it against individual test files
* @returns an object of [fileName]: absolute file path
*/
const getEntryPoints = (entryPoints) => {
// Searches for files that match the patterns defined in the array of input points.
// Returns an array of absolute file paths.
const files = fg.sync(entryPoints, { absolute: true });
// Maps the file paths in the "files" array to an array of key-value pair.
const entities = files.map((file) => {
// Extract the part of the file path after the "src" folder and before the file extension.
const [key] = file.match(/(?<=src\/).*$/) || [];
// Remove the file extension from the key.
const keyWithoutExt = key.replace(/\.[^.]*$/, '');
return [keyWithoutExt, file];
});
// Convert the array of key-value pairs to an object using the Object.fromEntries() method.
// Returns an object where each key is the file name without the extension and the value is the absolute file path.
return Object.fromEntries(entities);
};
export default defineConfig({
mode: 'production',
build: {
lib: {
entry: getEntryPoints(['./src/tests/*.ts']),
fileName: '[name]',
formats: ['cjs'],
sourcemap: true,
},
outDir: 'dist',
minify: false, // Don't minimize, as it's not used in the browser
rollupOptions: {
external: [new RegExp(/^(k6|https?\:\/\/)(\/.*)?/)],
},
},
resolve: {
extensions: ['.ts', '.js'],
},
plugins: [
copy({
targets: [
{
src: 'assets/**/*',
dest: 'dist',
noErrorOnMissing: true,
},
],
}),
babel({
babelHelpers: 'bundled',
exclude: /node_modules/,
}),
nodeResolve(),
],
});

1160
k6-example/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

18
load-tests/package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"scripts": {
"build": "tsc",
"start": "yarn build && node dist/api-test.js"
},
"dependencies": {
"js-yaml": "^4.1.0",
"k6": "^0.0.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"devDependencies": {
"@types/js-yaml": "^4.0.9",
"@types/k6": "^1.2.0",
"@types/node": "^24.5.2",
"tsx": "^4.20.5",
"typescript": "^5.9.2"
}
}

View File

@@ -0,0 +1,9 @@
import http from 'k6/http';
import * as fs from 'fs';
import * as yaml from 'js-yaml';
const
export default function () {
}

View File

0
load-tests/tsconfig.json Normal file
View File

236
load-tests/yarn.lock Normal file
View File

@@ -0,0 +1,236 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@esbuild/aix-ppc64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz#ee6b7163a13528e099ecf562b972f2bcebe0aa97"
integrity sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==
"@esbuild/android-arm64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz#115fc76631e82dd06811bfaf2db0d4979c16e2cb"
integrity sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==
"@esbuild/android-arm@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.10.tgz#8d5811912da77f615398611e5bbc1333fe321aa9"
integrity sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==
"@esbuild/android-x64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.10.tgz#e3e96516b2d50d74105bb92594c473e30ddc16b1"
integrity sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==
"@esbuild/darwin-arm64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz#6af6bb1d05887dac515de1b162b59dc71212ed76"
integrity sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==
"@esbuild/darwin-x64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz#99ae82347fbd336fc2d28ffd4f05694e6e5b723d"
integrity sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==
"@esbuild/freebsd-arm64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz#0c6d5558a6322b0bdb17f7025c19bd7d2359437d"
integrity sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==
"@esbuild/freebsd-x64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz#8c35873fab8c0857a75300a3dcce4324ca0b9844"
integrity sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==
"@esbuild/linux-arm64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz#3edc2f87b889a15b4cedaf65f498c2bed7b16b90"
integrity sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==
"@esbuild/linux-arm@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz#86501cfdfb3d110176d80c41b27ed4611471cde7"
integrity sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==
"@esbuild/linux-ia32@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz#e6589877876142537c6864680cd5d26a622b9d97"
integrity sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==
"@esbuild/linux-loong64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz#11119e18781f136d8083ea10eb6be73db7532de8"
integrity sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==
"@esbuild/linux-mips64el@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz#3052f5436b0c0c67a25658d5fc87f045e7def9e6"
integrity sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==
"@esbuild/linux-ppc64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz#2f098920ee5be2ce799f35e367b28709925a8744"
integrity sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==
"@esbuild/linux-riscv64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz#fa51d7fd0a22a62b51b4b94b405a3198cf7405dd"
integrity sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==
"@esbuild/linux-s390x@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz#a27642e36fc282748fdb38954bd3ef4f85791e8a"
integrity sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==
"@esbuild/linux-x64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz#9d9b09c0033d17529570ced6d813f98315dfe4e9"
integrity sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==
"@esbuild/netbsd-arm64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz#25c09a659c97e8af19e3f2afd1c9190435802151"
integrity sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==
"@esbuild/netbsd-x64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz#7fa5f6ffc19be3a0f6f5fd32c90df3dc2506937a"
integrity sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==
"@esbuild/openbsd-arm64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz#8faa6aa1afca0c6d024398321d6cb1c18e72a1c3"
integrity sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==
"@esbuild/openbsd-x64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz#a42979b016f29559a8453d32440d3c8cd420af5e"
integrity sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==
"@esbuild/openharmony-arm64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz#fd87bfeadd7eeb3aa384bbba907459ffa3197cb1"
integrity sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==
"@esbuild/sunos-x64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz#3a18f590e36cb78ae7397976b760b2b8c74407f4"
integrity sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==
"@esbuild/win32-arm64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz#e71741a251e3fd971408827a529d2325551f530c"
integrity sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==
"@esbuild/win32-ia32@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz#c6f010b5d3b943d8901a0c87ea55f93b8b54bf94"
integrity sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==
"@esbuild/win32-x64@0.25.10":
version "0.25.10"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz#e4b3e255a1b4aea84f6e1d2ae0b73f826c3785bd"
integrity sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==
"@types/js-yaml@^4.0.9":
version "4.0.9"
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2"
integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==
"@types/k6@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@types/k6/-/k6-1.2.0.tgz#c27231f6311b48cb4c553bcf3d8d4bc036d00ab8"
integrity sha512-7F/vjekOUCzFi5AY+yYBOL38iEtN9EXufdKOAMFkJsdLryJ3/bB/W9jppED1FO5o7zeaPf8M46ardYPD8xgy/w==
"@types/node@^24.5.2":
version "24.5.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.5.2.tgz#52ceb83f50fe0fcfdfbd2a9fab6db2e9e7ef6446"
integrity sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==
dependencies:
undici-types "~7.12.0"
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
esbuild@~0.25.0:
version "0.25.10"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.10.tgz#37f5aa5cd14500f141be121c01b096ca83ac34a9"
integrity sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==
optionalDependencies:
"@esbuild/aix-ppc64" "0.25.10"
"@esbuild/android-arm" "0.25.10"
"@esbuild/android-arm64" "0.25.10"
"@esbuild/android-x64" "0.25.10"
"@esbuild/darwin-arm64" "0.25.10"
"@esbuild/darwin-x64" "0.25.10"
"@esbuild/freebsd-arm64" "0.25.10"
"@esbuild/freebsd-x64" "0.25.10"
"@esbuild/linux-arm" "0.25.10"
"@esbuild/linux-arm64" "0.25.10"
"@esbuild/linux-ia32" "0.25.10"
"@esbuild/linux-loong64" "0.25.10"
"@esbuild/linux-mips64el" "0.25.10"
"@esbuild/linux-ppc64" "0.25.10"
"@esbuild/linux-riscv64" "0.25.10"
"@esbuild/linux-s390x" "0.25.10"
"@esbuild/linux-x64" "0.25.10"
"@esbuild/netbsd-arm64" "0.25.10"
"@esbuild/netbsd-x64" "0.25.10"
"@esbuild/openbsd-arm64" "0.25.10"
"@esbuild/openbsd-x64" "0.25.10"
"@esbuild/openharmony-arm64" "0.25.10"
"@esbuild/sunos-x64" "0.25.10"
"@esbuild/win32-arm64" "0.25.10"
"@esbuild/win32-ia32" "0.25.10"
"@esbuild/win32-x64" "0.25.10"
fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
get-tsconfig@^4.7.5:
version "4.10.1"
resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.10.1.tgz#d34c1c01f47d65a606c37aa7a177bc3e56ab4b2e"
integrity sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==
dependencies:
resolve-pkg-maps "^1.0.0"
js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
dependencies:
argparse "^2.0.1"
k6@^0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/k6/-/k6-0.0.0.tgz#8c923200be0a68c578e8f5a32be96b1d8065cc3b"
integrity sha512-GAQSWayS2+LjbH5bkRi+pMPYyP1JSp7o+4j58ANZ762N/RH/SdlAT3CHHztnn8s/xgg8kYNM24Gd2IPo9b5W+g==
resolve-pkg-maps@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f"
integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==
tsx@^4.20.5:
version "4.20.5"
resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.20.5.tgz#856c8b2f114c50a9f4ae108126967a167f240dc7"
integrity sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==
dependencies:
esbuild "~0.25.0"
get-tsconfig "^4.7.5"
optionalDependencies:
fsevents "~2.3.3"
typescript@^5.9.2:
version "5.9.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6"
integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==
undici-types@~7.12.0:
version "7.12.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.12.0.tgz#15c5c7475c2a3ba30659529f5cdb4674b622fafb"
integrity sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==

View File

@@ -0,0 +1,15 @@
meta {
name: Get thread name
type: http
seq: 4
}
get {
url: {{url}}/thread/name
body: none
auth: inherit
}
settings {
encodeUrl: true
}

View File

@@ -8,7 +8,7 @@ import com.zeenea.experiments.virtualthreads.virtualthreadsapp.domain.marketplac
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.*;
@Service
public class MarketplaceService {
@@ -21,9 +21,12 @@ public class MarketplaceService {
}
public Marketplace getMarketplace() {
return getMarketplaceInParallel();
return getMarketplaceInParallel2();
}
/**
* This method uses classic threads while it is using `CompletableFuture`s. This way should be avoided.
*/
private Marketplace getMarketplaceInParallel() {
CompletableFuture<List<Catalog>> getCatalogsFuture = CompletableFuture.supplyAsync(catalogService::getAll);
CompletableFuture<List<Item>> getAllItemsFuture = CompletableFuture.supplyAsync(itemService::getAllItems);
@@ -38,6 +41,23 @@ public class MarketplaceService {
return new Marketplace(catalogs, sharedItems);
}
private Marketplace getMarketplaceInParallel2() {
try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {
Future<List<Catalog>> getCatalogsFuture = executorService.submit(catalogService::getAll);
Future<List<Item>> getItemsFuture = executorService.submit(itemService::getAllItems);
List<Catalog> catalogs = getCatalogsFuture.get();
List<Item> items = getItemsFuture.get();
var sharedItems = items.stream()
.filter(Item::isShared)
.toList();
return new Marketplace(catalogs, sharedItems);
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
}
private Marketplace getMarketplaceSequentially() {
return new Marketplace(
catalogService.getAll(),