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

[DEV-190] 로드테스트 반복 스크립트 작성 (PLOCK) #9

Merged
merged 9 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@

@Service
public class CleanUpUseCase {
private MemberRepository memberRepository;
private PurchaseRepository purchaseRepository;
private TicketRepository ticketRepository;
private TicketingRepository ticketingRepository;
private final MemberRepository memberRepository;
private final PurchaseRepository purchaseRepository;
private final TicketRepository ticketRepository;
private final TicketingRepository ticketingRepository;

@Autowired
public CleanUpUseCase(MemberRepository memberRepository, PurchaseRepository purchaseRepository,
Expand Down
16 changes: 16 additions & 0 deletions stress-test/cleanup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash

# Endpoint and resource
url="localhost:4000/api/stress-test"

# Send DELETE request
response=$(curl -s -X DELETE "$url" -w "%{http_code}")

http_status=$(echo "$response" | tail -n1)

# Check if the DELETE was successful
if [ "$http_status" -eq 200 ]; then
echo "Delete successful."
else
echo "Failed to delete resource. HTTP Status: $http_status"
fi
19 changes: 19 additions & 0 deletions stress-test/docker-compose.stress-k6-only.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: "3.8"

services:
k6:
image: grafana/k6
volumes:
- ./output:/output
- ./k6-scripts:/scripts
command: >
run
-e VSR=${VSR}
-e TICKETS=${TICKETS}
-e ITERATION=${ITERATION}
-e LOCKTYPE=${LOCKTYPE}
/scripts/stress.js
extra_hosts:
- "host.docker.internal:host-gateway"

# TODO: 서버, DB 서비스 함께 올리기
235 changes: 134 additions & 101 deletions stress-test/k6-scripts/stress.js
Original file line number Diff line number Diff line change
@@ -1,140 +1,173 @@
import { uuidv4 } from "https://jslib.k6.io/k6-utils/1.4.0/index.js";
import {uuidv4} from "https://jslib.k6.io/k6-utils/1.4.0/index.js";
import http from "k6/http";

import { check } from "k6";
import {check} from "k6";

const setupVar = {
userNum: 300, // 구매자 수 (vUser),
ticketStock: 50, // 티켓 재고
ticketPrice: 1000,
setupTimeout: "120s",
userNum: __ENV.VSR || 10, // 구매자 수 (vUser),
ticketStock: __ENV.TICKETS || 50, // 티켓 재고
backoff: __ENV.BACKOFF || 0,
retry: __ENV.RETRY || 0,
waitTime: __ENV.WAITTIME || 0,
leaseTime: __ENV.LEASETIME || 0,
iteration: __ENV.ITERATION || 1,
lockType: __ENV.LOCKTYPE || 'plock',
ticketPrice: 1000,
setupTimeout: "120s",
};

export const options = {
setupTimeout: setupVar.setupTimeout,
scenarios: {
contacts: {
executor: "per-vu-iterations",
vus: setupVar.userNum,
iterations: 1,

setupTimeout: setupVar.setupTimeout,
scenarios: {
contacts: {
executor: "per-vu-iterations",
vus: setupVar.userNum,
iterations: 1,
},
},
},
};

const commonHeader = { "Content-Type": "application/json" };
const commonHeader = {"Content-Type": "application/json"};

const domain = "http://host.docker.internal:4000";

const commonPwd = "1q2w3e4r@@Q";

export function setup() {
// 멤버 생성 (1: seller, n: buyer)
const buyerEmailList = Array(setupVar.userNum)
.fill()
.map(() => `test-buyer-${uuidv4()}@test.com`);
// 멤버 생성 (1: seller, n: buyer)

const buyerEmailList = Array.from({length: setupVar.userNum}, () =>
`test-buyer-${uuidv4()}@test.com`
);

const sellerEmail = `test-seller-${uuidv4()}@test.com`;

wrapWithTimeLogging("유저 생성", () => {
createUsers({
emailList: [...buyerEmailList, sellerEmail],
const sellerEmail = `test-seller-${uuidv4()}@test.com`;

wrapWithTimeLogging("유저 생성", () => {
createUsers({
emailList: [...buyerEmailList, sellerEmail],
});
});
});

// 티케팅 생성
const now = new Date();
const saleStart = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours() + 9,
now.getMinutes(),
now.getSeconds() + 5
);

const ticketingId = wrapWithTimeLogging("티케팅 생성", () => {
return createTicketing({
title: "Stress Test",
location: "서울",
category: "IT",
runningMinutes: 100,
price: setupVar.ticketPrice,
saleStart,
saleEnd: new Date(
new Date().setFullYear(now.getFullYear() + 1)
).toISOString(),
eventTime: new Date(
new Date().setFullYear(now.getFullYear() + 2)
).toISOString(),
stock: setupVar.ticketStock,
email: sellerEmail,

// 티케팅 생성
const now = new Date();
const saleStart = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours() + 9,
now.getMinutes(),
now.getSeconds() + 5
);

const ticketingId = wrapWithTimeLogging("티케팅 생성", () => {
return createTicketing({
title: "Stress Test",
location: "서울",
category: "IT",
runningMinutes: 100,
price: setupVar.ticketPrice,
saleStart,
saleEnd: new Date(
new Date().setFullYear(now.getFullYear() + 1)
).toISOString(),
eventTime: new Date(
new Date().setFullYear(now.getFullYear() + 2)
).toISOString(),
stock: setupVar.ticketStock,
email: sellerEmail,
});
});
});

return { buyerEmailList, ticketingId };
return {buyerEmailList, ticketingId};
}

export default function ({ buyerEmailList, ticketingId }) {
const userCounter = __VU;
export default function ({buyerEmailList, ticketingId}) {
const userCounter = __VU;

const email = buyerEmailList[userCounter];
const email = buyerEmailList[userCounter - 1];

// TODO: 락 방법론 마다 분리된 EP 잘 찔러보기
puchase(ticketingId, 1, email);

// TODO: 락 방법론 마다 분리된 EP 잘 찔러보기
purchase(ticketingId, 1, email);
}

function createUsers(users) {
const response = http.post(domain + "/api/members", JSON.stringify(users), {
headers: commonHeader,
});
const response = http.post(domain + "/api/members", JSON.stringify(users), {
headers: commonHeader,
});

check(response, {
"status check after create user": (r) => r.status === 200,
});
check(response, {
"status check after create user": (r) => r.status === 200,
});
}

function createTicketing(ticketingMetadata) {
const response = http.post(
domain + "/api/ticketings",
JSON.stringify(ticketingMetadata),
{
headers: commonHeader,
cookies: { accessToken },
}
);

check(response, {
"status check after create ticketing": (r) => r.status === 201,
"ticketing id check after create ticketing": (r) => {
const ticketingId = r.json().data["ticketingId"];
return typeof ticketingId === "string" && ticketingId !== "";
},
});
const response = http.post(
domain + "/api/ticketings",
JSON.stringify(ticketingMetadata),
{
headers: commonHeader,
}
);


check(response, {
"status check after create ticketing": (r) => r.status === 201,
"ticketing id check after create ticketing": (r) => {
const ticketingId = r.json().data["ticketingId"];
return typeof ticketingId === "string" && ticketingId !== "";
},
});

return response.json("data")["ticketingId"];
return response.json("data")["ticketingId"];
}

function puchase(ticketingId, count, buyerEmail) {
const response = http.post(
domain + "/api/purchases",
JSON.stringify({
ticketingId,
count,
}),
{
headers: commonHeader,
}
);

check(response, {
"status check after create purchase": (r) => r.status === 201,
});
function purchase(ticketingId, count, buyerEmail) {

const response = http.post(
domain + "/api/purchases/p-lock",
JSON.stringify({
ticketingId,
count,
email: buyerEmail
}),
{
headers: commonHeader,
}
);


check(response, {
"status check after create purchase": (r) => r.status === 201,
});
}

function wrapWithTimeLogging(tag, callback) {
const start = new Date();
const result = callback();
const end = new Date();
console.log(`${tag}: 소요시간 - ${end.getTime() - start.getTime()}ms`);
return result;
const start = new Date();
const result = callback();
const end = new Date();
console.log(`${tag}: 소요시간 - ${end.getTime() - start.getTime()}ms`);
return result;
}

export function handleSummary(data) {

const filenameParts = [
setupVar.lockType,
`vus_${setupVar.userNum}`,
`tickets_${setupVar.ticketStock}`,
`backoff_${setupVar.backoff}`,
`retry_${setupVar.retry}`,
`waitTime_${setupVar.waitTime}`,
`leaseTime_${setupVar.leaseTime}`,
`${setupVar.iteration}`
];

const filename = `/output/${filenameParts.join('_')}.json`;

return {
[filename]: JSON.stringify(data)
};
}
Empty file added stress-test/output/.gitkeep
Empty file.
19 changes: 19 additions & 0 deletions stress-test/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash

declare -a vsrs=(30 60)
declare -a tickets=(30 60)

if [[ -z "$1" ]] || ! [[ "$1" =~ ^[0-9]+$ ]]; then
echo "Usage: $0 <number of iterations>"
exit 1
fi

for vsr in "${vsrs[@]}"; do
for ticket in "${tickets[@]}"; do
for ((i = 1; i <= $1; i++)); do
sh ./run_plock.sh ${vsr} ${ticket} ${i}
sleep 1
sh ./cleanup.sh
done
done
done
Empty file added stress-test/run_dlock.sh
Empty file.
Empty file added stress-test/run_olock.sh
Empty file.
20 changes: 20 additions & 0 deletions stress-test/run_plock.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash
# ${1} : VSR
# ${2} : TICKETS
# ${3} : ITERATION
echo ${1} ${2} ${3}

echo "Run with VSR:${1} TICKETS:${2} ITERATION:${3}"

VSR=${1}
TICKETS=${2}
ITERATION=${3}
LOCKTYPE="dlock"

export VSR
export TICKETS
export ITERATION
export LOCKTYPE

docker compose -f docker-compose.stress-k6-only.yml up

Loading