diff --git a/src/main/java/com/tiketeer/Tiketeer/domain/stresstest/usecase/CleanUpUseCase.java b/src/main/java/com/tiketeer/Tiketeer/domain/stresstest/usecase/CleanUpUseCase.java index 639efdf..ed81cd5 100644 --- a/src/main/java/com/tiketeer/Tiketeer/domain/stresstest/usecase/CleanUpUseCase.java +++ b/src/main/java/com/tiketeer/Tiketeer/domain/stresstest/usecase/CleanUpUseCase.java @@ -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, diff --git a/stress-test/cleanup.sh b/stress-test/cleanup.sh new file mode 100644 index 0000000..f914309 --- /dev/null +++ b/stress-test/cleanup.sh @@ -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 \ No newline at end of file diff --git a/stress-test/docker-compose.stress-k6-only.yml b/stress-test/docker-compose.stress-k6-only.yml new file mode 100644 index 0000000..7b3fba1 --- /dev/null +++ b/stress-test/docker-compose.stress-k6-only.yml @@ -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 서비스 함께 올리기 \ No newline at end of file diff --git a/stress-test/k6-scripts/stress.js b/stress-test/k6-scripts/stress.js index 5db95a6..96995a7 100644 --- a/stress-test/k6-scripts/stress.js +++ b/stress-test/k6-scripts/stress.js @@ -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) + }; +} \ No newline at end of file diff --git a/stress-test/output/.gitkeep b/stress-test/output/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/stress-test/run.sh b/stress-test/run.sh new file mode 100644 index 0000000..c2f4c28 --- /dev/null +++ b/stress-test/run.sh @@ -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 " + 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 \ No newline at end of file diff --git a/stress-test/run_dlock.sh b/stress-test/run_dlock.sh new file mode 100644 index 0000000..e69de29 diff --git a/stress-test/run_olock.sh b/stress-test/run_olock.sh new file mode 100644 index 0000000..e69de29 diff --git a/stress-test/run_plock.sh b/stress-test/run_plock.sh new file mode 100644 index 0000000..63ac6a6 --- /dev/null +++ b/stress-test/run_plock.sh @@ -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 +