diff --git a/auction/src/main/java/com/smore/auction/infrastructure/websocket/interceptor/AuctionStompInterceptor.java b/auction/src/main/java/com/smore/auction/infrastructure/websocket/interceptor/AuctionStompInterceptor.java index fd747c49..f2b1d885 100644 --- a/auction/src/main/java/com/smore/auction/infrastructure/websocket/interceptor/AuctionStompInterceptor.java +++ b/auction/src/main/java/com/smore/auction/infrastructure/websocket/interceptor/AuctionStompInterceptor.java @@ -2,6 +2,7 @@ import com.smore.auction.infrastructure.websocket.manager.AuctionPubManager; import com.smore.auction.infrastructure.websocket.manager.AuctionSessionManager; +import java.nio.file.AccessDeniedException; import java.security.Principal; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -73,11 +74,15 @@ public class AuctionStompInterceptor implements ChannelInterceptor { return null; // 이상한 토픽 구독 차단 } String auctionId = extractAuctionId(destination, true); - sessionManager.handleSubscribe( + boolean isExist = sessionManager.handleSubscribe( accessor.getSessionId(), Long.valueOf(principal.getName()), auctionId ); + if (!isExist) { + log.info("경매방이 열려있지 않아 SUBSCRIBE 차단"); + return null; + } } // 3. SEND 검증 (/pub/auction/** 만 처리) @@ -95,7 +100,8 @@ public class AuctionStompInterceptor implements ChannelInterceptor { pubManager.validateSend(accessor.getSessionId(), auctionId); } catch (Exception e) { log.info(String.valueOf(e)); - log.warn("Unauthorized SEND: session={}, auction={}", accessor.getSessionId(), auctionId); + log.warn("Unauthorized SEND: session={}, auction={}", accessor.getSessionId(), + auctionId); return null; // 메시지 브로커로 안 보냄 } } diff --git a/auction/src/main/java/com/smore/auction/infrastructure/websocket/manager/AuctionSessionManager.java b/auction/src/main/java/com/smore/auction/infrastructure/websocket/manager/AuctionSessionManager.java index 30f56f49..2107b851 100644 --- a/auction/src/main/java/com/smore/auction/infrastructure/websocket/manager/AuctionSessionManager.java +++ b/auction/src/main/java/com/smore/auction/infrastructure/websocket/manager/AuctionSessionManager.java @@ -1,6 +1,6 @@ package com.smore.auction.infrastructure.websocket.manager; public interface AuctionSessionManager { - void handleSubscribe(String sessionId, Long userId, String auctionId); + boolean handleSubscribe(String sessionId, Long userId, String auctionId); void handleDisconnect(String sessionId); } \ No newline at end of file diff --git a/auction/src/main/java/com/smore/auction/infrastructure/websocket/manager/impl/AuctionSessionManagerImpl.java b/auction/src/main/java/com/smore/auction/infrastructure/websocket/manager/impl/AuctionSessionManagerImpl.java index 9c0a8fe7..607894d6 100644 --- a/auction/src/main/java/com/smore/auction/infrastructure/websocket/manager/impl/AuctionSessionManagerImpl.java +++ b/auction/src/main/java/com/smore/auction/infrastructure/websocket/manager/impl/AuctionSessionManagerImpl.java @@ -17,13 +17,13 @@ public class AuctionSessionManagerImpl implements AuctionSessionManager { private final RedisKeyFactory key; @Override - public void handleSubscribe(String sessionId, Long userId, String auctionId) { + public boolean handleSubscribe(String sessionId, Long userId, String auctionId) { log.info("구독매니저 진입 경매진행 중인지 확인 후 sub"); Boolean auctionExist = redis.hasKey(key.auctionOpen(auctionId)); log.info("경매방 검증: {}", redis.hasKey(key.auctionOpen(auctionId))); if (!auctionExist) { - return; + return false; } // 해당 경매에 참여중인 세션으로 기록 (메시지 발송용) redis.opsForSet() @@ -34,6 +34,8 @@ public void handleSubscribe(String sessionId, Long userId, String auctionId) { // 세션이 어느 유저Id 로 들어왔는지 기록 redis.opsForValue() .set(key.sessionUser(sessionId), userId.toString()); + + return true; } @Override diff --git a/k6-test/README.md b/k6-test/README.md index e9990c24..879ba053 100644 --- a/k6-test/README.md +++ b/k6-test/README.md @@ -1,35 +1,84 @@ -# k6 WebSocket STOMP load test (SockJS) - -This folder contains: -- `ws_test.js`: SockJS + STOMP load test for auction bidding -- `tokens.txt`: one JWT per line for gateway tests -- `test_command.txt`: copy/paste commands - -## How the script works -- Calls SockJS info endpoint: `GET /ws-auction/info` -- Opens WebSocket: `/ws-auction/{server}/{session}/websocket` -- Sends STOMP `CONNECT`, then `SUBSCRIBE /topic/auction/{auctionId}` -- Sends repeated `SEND /pub/auction/{auctionId}/bid` while the session is open -- Adjusts bid price upward based on the server "min bid" message (see `updateMinBidFromFrame` in `ws_test.js`) - -## Environment variables -- `WS_BASE`: WebSocket base URL (default: `ws://host.docker.internal:6600`) -- `HTTP_BASE`: HTTP base URL (default: `WS_BASE` with `ws` -> `http`) +# k6 WebSocket STOMP 부하 테스트 (SockJS) + +이 폴더에는 다음 파일이 있습니다: +- `ws_test.js`: SockJS + STOMP 입찰 부하 테스트 스크립트 +- `tokens.txt`: 게이트웨이 테스트용 JWT (1줄 1개) +- `test_command.txt`: PowerShell 실행 커맨드 모음 +- `outbox_load.sql`: outbox 더미 데이터 부하용 SQL + +## 동작 방식 +- SockJS info 엔드포인트 호출: `GET /ws-auction/info` +- WebSocket 연결: `/ws-auction/{server}/{session}/websocket` +- STOMP `CONNECT` 후 `SUBSCRIBE /topic/auction/{auctionId}` +- 세션 동안 `SEND /pub/auction/{auctionId}/bid` 반복 전송 +- 서버의 "최소 입찰가" 메시지에 맞춰 입찰가 자동 증가 (`updateMinBidFromFrame` 참고) + +## 실행 전 확인 +- Docker 또는 k6 실행 환경이 필요합니다 (예시는 Docker 사용) +- 외부 WS를 사용할 경우 `WS_BASE`, `HTTP_BASE`를 외부 주소로 지정하세요 +- 토큰 사용 시 `TOKENS_FILE` 또는 `TOKEN`을 지정하세요 + +## 환경 변수 +- `WS_BASE`: WebSocket base URL (기본값: `ws://host.docker.internal:6600`) +- `HTTP_BASE`: HTTP base URL (기본값: `WS_BASE`에서 `ws` -> `http` 변환) - `AUCTION_ID`: auction UUID -- `TOKENS_FILE`: path to tokens file in container (one JWT per line) -- `TOKEN`: single JWT (used if `TOKENS_FILE` is not set) -- `SEND_DELAY_MS`: delay before first bid after connect (default: `0`) -- `SEND_EVERY_MS`: interval between bids per session (default: `1000`) -- `SESSION_MS`: session lifetime before closing (default: `10000`) -- `BID_PRICE_BASE`: base bid price (default: `1000`) -- `BID_PRICE_STEP`: bid price increment (default: `0.01`) -- `VUS`, `DURATION`: k6 options (also set via `--vus`, `--duration`) -- `USER_ID_BASE`: starting user id for direct mode (no token) -- `DEBUG=1`: prints SockJS info/handshake status +- `TOKENS_FILE`: 컨테이너 내 토큰 파일 경로 (1줄 1 JWT) +- `TOKEN`: 단일 JWT (`TOKENS_FILE` 미지정 시 사용) +- `SEND_DELAY_MS`: 연결 후 첫 입찰 딜레이 (기본값: `0`) +- `SEND_EVERY_MS`: 입찰 전송 간격 (기본값: `1000`) +- `SESSION_MS`: 세션 유지 시간 (기본값: `10000`) +- `BID_PRICE_BASE`: 기본 입찰가 (기본값: `1000`) +- `BID_PRICE_STEP`: 입찰가 증가 폭 (기본값: `0.01`) +- `BID_PRICE_START`: 시작 입찰가 (설정 시 VU별 메시지마다 `BID_PRICE_STEP`씩 증가) +- `BID_PRICE_MODE`: `global-seq` 사용 시 VU 간 중복 없이 증가 (정확한 전역 순서 보장은 아님) +- `VUS`, `DURATION`: 일반 시나리오 옵션 (`--vus`, `--duration`로도 지정 가능) +- `USER_ID_BASE`: 직접 모드(토큰 없음)에서 사용할 시작 user id +- `DEBUG=1`: SockJS info/handshake 로그 출력 +- `LOG_WS_ERRORS=1`: WebSocket 핸드셰이크 실패 로그 출력 +- `LOG_INFO_SLOW_MS`: `/ws-auction/info` 응답이 지정 ms 이상이면 로그 출력 +- `SCENARIO=step-bid`: 단계 시나리오 사용 (아래 참고) +- `STEP_FIRST_RPS`: 단계 시나리오 1차 전송률 (VU당 초당 메시지 수) +- `STEP_SECOND_RPS`: 단계 시나리오 2차 전송률 (VU당 초당 메시지 수) +- `STEP_RATE_SWITCH_MS`: 1차 -> 2차 전송률 전환 시점 (밀리초) + +## 단계 시나리오 (step-bid) +- VU 단계: 50 → 100 → 150 → 200 → 250 → 300 → 350 → 400 (각 단계 2분) +- 각 단계는 1분 동안 `STEP_FIRST_RPS`, 이후 1분 동안 `STEP_SECOND_RPS`로 전송 +- `SCENARIO=step-bid` 사용 시 `VUS`, `DURATION`은 무시됩니다 + +요청한 패턴 예시: +- 50 VU 2분 (1분: VU당 5 msg/s, 다음 1분: VU당 10 msg/s) +- 100 VU 2분 동일 패턴 +- 150 VU 2분 동일 패턴 +- 200 VU 2분 동일 패턴 +- 250 VU 2분 동일 패턴 +- 300 VU 2분 동일 패턴 +- 350 VU 2분 동일 패턴 +- 400 VU 2분 동일 패턴 + +## 최근 사용 시나리오 요약 +- GW 경유 (`host.docker.internal:17700`) +- 토큰 파일 사용 (`TOKENS_FILE=/scripts/tokens.txt`) +- 입찰가: `BID_PRICE_START=1001`, `BID_PRICE_STEP=1` +- 단계 시나리오: 50→400 VU, 각 2분 (1분 5 msg/s → 1분 10 msg/s) ## Commands (PowerShell) -Gateway, multi-token, continuous bidding (500 VU, 60s, dashboard): +GW 경유 + 단계 시나리오 (50→400 VU, 각 2분): +``` +docker run --rm -i -p 5665:5665 -v "${PWD}\k6-test:/scripts" ` + -e K6_WEB_DASHBOARD=true -e K6_WEB_DASHBOARD_HOST=0.0.0.0 ` + -e SCENARIO=step-bid ` + -e STEP_FIRST_RPS=5 -e STEP_SECOND_RPS=10 -e STEP_RATE_SWITCH_MS=60000 ` + -e WS_BASE=ws://host.docker.internal:17700 ` + -e HTTP_BASE=http://host.docker.internal:17700 ` + -e TOKENS_FILE=/scripts/tokens.txt ` + -e AUCTION_ID=11111111-1111-1111-1111-111111111111 ` + -e BID_PRICE_START=1001 -e BID_PRICE_STEP=1 ` + grafana/k6 run --quiet /scripts/ws_test.js +``` + +Gateway, 다중 토큰, 지속 입찰 (500 VU, 60s, 대시보드): ``` docker run --rm -i -p 5665:5665 -v "${PWD}\k6-test:/scripts" ` -e K6_WEB_DASHBOARD=true -e K6_WEB_DASHBOARD_HOST=0.0.0.0 ` @@ -43,7 +92,7 @@ docker run --rm -i -p 5665:5665 -v "${PWD}\k6-test:/scripts" ` /scripts/ws_test.js ``` -Gateway, multi-token, continuous bidding (1000 VU, 5m, dashboard): +Gateway, 다중 토큰, 지속 입찰 (1000 VU, 5m, 대시보드): ``` docker run --rm -i -p 5665:5665 -v "${PWD}\k6-test:/scripts" ` -e K6_WEB_DASHBOARD=true -e K6_WEB_DASHBOARD_HOST=0.0.0.0 ` @@ -57,7 +106,7 @@ docker run --rm -i -p 5665:5665 -v "${PWD}\k6-test:/scripts" ` /scripts/ws_test.js ``` -Direct auction service (no token, header-based): +Direct auction service (토큰 없음, 헤더 기반): ``` docker run --rm -i -v "${PWD}\k6-test:/scripts" grafana/k6 run ` --vus 500 --duration 60s ` @@ -74,17 +123,17 @@ Dashboard URL: `http://localhost:5665` ``` docker run --rm -i -p 5665:5665 -v "${PWD}\k6-test:/scripts" ` -e K6_WEB_DASHBOARD=true -e K6_WEB_DASHBOARD_HOST=0.0.0.0 ` - grafana/k6 run --vus 500 --duration 60s ` + -e SCENARIO=step-bid ` + -e STEP_FIRST_RPS=5 -e STEP_SECOND_RPS=10 -e STEP_RATE_SWITCH_MS=60000 ` -e WS_BASE=ws://host.docker.internal:17700 ` -e HTTP_BASE=http://host.docker.internal:17700 ` -e TOKENS_FILE=/scripts/tokens.txt ` -e AUCTION_ID=11111111-1111-1111-1111-111111111111 ` - -e SEND_DELAY_MS=100 -e SEND_EVERY_MS=1000 -e SESSION_MS=60000 ` - -e BID_PRICE_BASE=1000 -e BID_PRICE_STEP=0.01 ` - /scripts/ws_test.js 2>&1 | Tee-Object -FilePath k6-test\k6_run.log + -e BID_PRICE_START=1001 -e BID_PRICE_STEP=1 ` + grafana/k6 run --quiet /scripts/ws_test.js 2>&1 | Tee-Object -FilePath k6-test\k6_run.log ``` -## Redis verification +## Redis 검증 ``` docker exec redis-stack redis-cli HGET auction:11111111-1111-1111-1111-111111111111:open minPrice docker exec redis-stack redis-cli HGET auction:11111111-1111-1111-1111-111111111111:open stock @@ -92,7 +141,11 @@ docker exec redis-stack redis-cli ZCARD auction:11111111-1111-1111-1111-11111111 docker exec redis-stack redis-cli ZREVRANGE auction:11111111-1111-1111-1111-111111111111:bids 0 9 WITHSCORES ``` +## Codex로 다시 실행하기 +다음에 같은 테스트를 Codex에 요청할 때는 이런 식으로 말하면 됩니다: +- "k6-test step-bid 시나리오로 50→400 VU, 각 2분, VU당 1초에 5회 → 10회로 전송. GW는 host.docker.internal:17700, 토큰 파일 사용, BID_PRICE_START=1001, BID_PRICE_STEP=1로 PowerShell 커맨드 만들어줘" + ## Notes -- After bid count reaches `stock`, bids below the cutoff are rejected. The script now increases bid price automatically. -- High VU counts can produce `connection refused` on `/ws-auction/info` or WebSocket protocol errors. Consider ramping VUs or increasing gateway capacity. -- SockJS URLs include random session IDs, so k6 may warn about high metric cardinality. +- 입찰 수가 `stock`에 도달하면 컷오프 이하 입찰이 거절됩니다. 스크립트는 최소 입찰가에 맞춰 자동으로 올립니다. +- 높은 VU에서 `/ws-auction/info`의 `connection refused`나 WebSocket 프로토콜 오류가 날 수 있습니다. VU 램프업 또는 게이트웨이 용량 조정이 필요할 수 있습니다. +- SockJS URL에 랜덤 세션 ID가 포함되어 k6 메트릭 카디널리티가 높아질 수 있습니다. diff --git a/k6-test/test_command.txt b/k6-test/test_command.txt index 5312b5a6..7d84c636 100644 --- a/k6-test/test_command.txt +++ b/k6-test/test_command.txt @@ -1,4 +1,33 @@ -GW 경유, 다중 토큰, 지속 입찰(500 VU, 1분, 대시보드) +GW 경유 + 단계 시나리오 (50→400 VU, 각 2분) + +docker run --rm -i -p 5665:5665 -v "${PWD}\k6-test:/scripts" ` + -e K6_WEB_DASHBOARD=true -e K6_WEB_DASHBOARD_HOST=0.0.0.0 ` + -e SCENARIO=step-bid ` + -e STEP_FIRST_RPS=5 -e STEP_SECOND_RPS=10 -e STEP_RATE_SWITCH_MS=60000 ` + -e WS_BASE=ws://host.docker.internal:17700 ` + -e HTTP_BASE=http://host.docker.internal:17700 ` + -e TOKENS_FILE=/scripts/tokens.txt ` + -e AUCTION_ID=11111111-1111-1111-1111-111111111111 ` + -e BID_PRICE_START=1001 -e BID_PRICE_STEP=1 ` + grafana/k6 run --quiet /scripts/ws_test.js + +실패/느린 응답 로그 추가 (진단용) + +docker run --rm -i -p 5665:5665 -v "${PWD}\k6-test:/scripts" ` + -e K6_WEB_DASHBOARD=true -e K6_WEB_DASHBOARD_HOST=0.0.0.0 ` + -e SCENARIO=step-bid ` + -e STEP_FIRST_RPS=5 -e STEP_SECOND_RPS=10 -e STEP_RATE_SWITCH_MS=60000 ` + -e WS_BASE=ws://host.docker.internal:17700 ` + -e HTTP_BASE=http://host.docker.internal:17700 ` + -e TOKENS_FILE=/scripts/tokens.txt ` + -e AUCTION_ID=11111111-1111-1111-1111-111111111111 ` + -e BID_PRICE_START=1001 -e BID_PRICE_STEP=1 ` + -e LOG_WS_ERRORS=1 -e LOG_INFO_SLOW_MS=3000 ` + grafana/k6 run --quiet /scripts/ws_test.js + +대시보드 http://localhost:5665 + +GW 경유, 다중 토큰, 지속 입찰 (500 VU, 1분, 대시보드) docker run --rm -i -p 5665:5665 -v "${PWD}\k6-test:/scripts" ` -e K6_WEB_DASHBOARD=true -e K6_WEB_DASHBOARD_HOST=0.0.0.0 ` @@ -14,9 +43,7 @@ docker run --rm -i -p 5665:5665 -v "${PWD}\k6-test:/scripts" ` -e BID_PRICE_STEP=0.01 ` /scripts/ws_test.js -대시보드: http://localhost:5665 - -워밍업(100 VU, 10초, 지속 입찰) +GW 경유, 다중 토큰, 지속 입찰 (100 VU, 10초) docker run --rm -i -p 5665:5665 -v "${PWD}\k6-test:/scripts" ` -e K6_WEB_DASHBOARD=true -e K6_WEB_DASHBOARD_HOST=0.0.0.0 ` @@ -32,13 +59,13 @@ docker run --rm -i -p 5665:5665 -v "${PWD}\k6-test:/scripts" ` -e BID_PRICE_STEP=0.01 ` /scripts/ws_test.js -단일 토큰 스모크(1 VU, 10초, 지속 입찰) +단일 토큰 (1 VU, 10초) docker run --rm -i -v "${PWD}\k6-test:/scripts" grafana/k6 run ` --vus 1 --duration 10s ` -e WS_BASE=ws://host.docker.internal:17700 ` -e HTTP_BASE=http://host.docker.internal:17700 ` - -e TOKEN= ` + -e TOKEN= ` -e AUCTION_ID=11111111-1111-1111-1111-111111111111 ` -e SEND_DELAY_MS=100 ` -e SEND_EVERY_MS=1000 ` @@ -47,7 +74,7 @@ docker run --rm -i -v "${PWD}\k6-test:/scripts" grafana/k6 run ` -e BID_PRICE_STEP=0.01 ` /scripts/ws_test.js -Auction 직통(토큰 없이 헤더로, 지속 입찰) +Auction 직접 연결 (토큰 없음, 헤더 기반) docker run --rm -i -v "${PWD}\k6-test:/scripts" grafana/k6 run ` --vus 500 --duration 60s ` @@ -60,7 +87,7 @@ docker run --rm -i -v "${PWD}\k6-test:/scripts" grafana/k6 run ` -e BID_PRICE_STEP=0.01 ` /scripts/ws_test.js -GW 경유, 다중 토큰, 지속 입찰(1000 VU, 5분, 대시보드) +GW 경유, 다중 토큰, 지속 입찰 (1000 VU, 5분, 대시보드) docker run --rm -i -p 5665:5665 -v "${PWD}\k6-test:/scripts" ` -e K6_WEB_DASHBOARD=true -e K6_WEB_DASHBOARD_HOST=0.0.0.0 ` diff --git a/k6-test/ws_test.js b/k6-test/ws_test.js index a77556d1..5693821a 100644 --- a/k6-test/ws_test.js +++ b/k6-test/ws_test.js @@ -4,21 +4,68 @@ import { check } from 'k6'; const VUS = __ENV.VUS ? parseInt(__ENV.VUS, 10) : 500; const DURATION = __ENV.DURATION || '30s'; +const USE_STEP_SCENARIO = __ENV.SCENARIO === 'step-bid'; + +const STEP_STAGE_MINUTES = 2; +const STEP_RATE_SWITCH_MS = __ENV.STEP_RATE_SWITCH_MS + ? parseInt(__ENV.STEP_RATE_SWITCH_MS, 10) + : 60000; +const STEP_FIRST_RPS = __ENV.STEP_FIRST_RPS ? parseFloat(__ENV.STEP_FIRST_RPS) : 2; +const STEP_SECOND_RPS = __ENV.STEP_SECOND_RPS ? parseFloat(__ENV.STEP_SECOND_RPS) : 4; +const STEP_FIRST_SEND_EVERY_MS = Math.round(1000 / STEP_FIRST_RPS); +const STEP_SECOND_SEND_EVERY_MS = Math.round(1000 / STEP_SECOND_RPS); + +const STEP_SCENARIO_STAGES = [ + { vus: 50 }, + { vus: 100 }, + { vus: 150 }, + { vus: 200 }, + { vus: 250 }, + { vus: 300 }, + { vus: 350 }, + { vus: 400 }, +]; + +function buildStepScenarios() { + const scenarios = {}; + const stageSessionMs = STEP_STAGE_MINUTES * 60 * 1000; + STEP_SCENARIO_STAGES.forEach((stage, index) => { + scenarios[`step_${stage.vus}vus`] = { + executor: 'constant-vus', + vus: stage.vus, + duration: `${STEP_STAGE_MINUTES}m`, + startTime: `${index * STEP_STAGE_MINUTES}m`, + env: { + SESSION_MS: String(stageSessionMs), + SCENARIO_VUS: String(stage.vus), + }, + }; + }); + return scenarios; +} export const options = { - vus: VUS, - duration: DURATION, + ...(USE_STEP_SCENARIO + ? { scenarios: buildStepScenarios() } + : { vus: VUS, duration: DURATION }), }; const WS_BASE = __ENV.WS_BASE || 'ws://host.docker.internal:6600'; const HTTP_BASE = __ENV.HTTP_BASE || WS_BASE.replace(/^ws/i, 'http'); const AUCTION_ID = __ENV.AUCTION_ID || '11111111-1111-1111-1111-111111111111'; const DEBUG = __ENV.DEBUG === '1'; +const LOG_WS_ERRORS = __ENV.LOG_WS_ERRORS === '1'; +const LOG_INFO_SLOW_MS = __ENV.LOG_INFO_SLOW_MS + ? parseInt(__ENV.LOG_INFO_SLOW_MS, 10) + : 0; const SEND_DELAY_MS = __ENV.SEND_DELAY_MS ? parseInt(__ENV.SEND_DELAY_MS, 10) : 0; const SEND_EVERY_MS = __ENV.SEND_EVERY_MS ? parseInt(__ENV.SEND_EVERY_MS, 10) : 1000; const SESSION_MS = __ENV.SESSION_MS ? parseInt(__ENV.SESSION_MS, 10) : 10000; const BID_PRICE_BASE = __ENV.BID_PRICE_BASE ? parseFloat(__ENV.BID_PRICE_BASE) : 1000; const BID_PRICE_STEP = __ENV.BID_PRICE_STEP ? parseFloat(__ENV.BID_PRICE_STEP) : 0.01; +const BID_PRICE_START = __ENV.BID_PRICE_START ? parseFloat(__ENV.BID_PRICE_START) : null; +const BID_PRICE_MODE = (__ENV.BID_PRICE_MODE || '').toLowerCase(); +const VU_COUNT = __ENV.SCENARIO_VUS ? parseInt(__ENV.SCENARIO_VUS, 10) : VUS; function buildQuery(params) { const parts = []; @@ -75,9 +122,18 @@ function pseudoUuid() { } function nextBidPrice(bidState) { - const bidIndex = bidState.baseIndex + bidState.seq; + const seq = bidState.seq; bidState.seq += 1; - const candidate = BID_PRICE_BASE + (bidIndex * BID_PRICE_STEP); + const base = BID_PRICE_START !== null ? BID_PRICE_START : BID_PRICE_BASE; + let candidate; + if (BID_PRICE_MODE === 'global-seq') { + const globalIndex = (seq * VU_COUNT) + (__VU - 1); + candidate = base + (globalIndex * BID_PRICE_STEP); + } else if (BID_PRICE_START !== null) { + candidate = base + (seq * BID_PRICE_STEP); + } else { + candidate = BID_PRICE_BASE + ((bidState.baseIndex + seq) * BID_PRICE_STEP); + } const minBased = bidState.minBid !== null ? bidState.minBid + BID_PRICE_STEP : null; @@ -107,6 +163,23 @@ function sendBid(socket, bidState) { ); } +function stepSendEveryMs(startedAt) { + const elapsed = Date.now() - startedAt; + return elapsed < STEP_RATE_SWITCH_MS + ? STEP_FIRST_SEND_EVERY_MS + : STEP_SECOND_SEND_EVERY_MS; +} + +function scheduleStepBids(socket, bidState, startedAt) { + const intervalMs = stepSendEveryMs(startedAt); + sendBid(socket, bidState); + if (intervalMs > 0) { + socket.setTimeout(() => { + scheduleStepBids(socket, bidState, startedAt); + }, intervalMs); + } +} + function stompFrame(command, headers, body) { let frame = `${command}\n`; Object.keys(headers || {}).forEach((key) => { @@ -189,6 +262,16 @@ export default function () { if (DEBUG && __ITER === 0) { console.log(`sockjs info status=${infoRes.status}`); } + if ( + LOG_INFO_SLOW_MS > 0 + && infoRes + && infoRes.timings + && infoRes.timings.duration > LOG_INFO_SLOW_MS + ) { + console.warn( + `slow info: status=${infoRes.status} duration_ms=${infoRes.timings.duration}` + ); + } const res = ws.connect( wsUrl, @@ -203,7 +286,7 @@ export default function () { (socket) => { let biddingStarted = false; const bidState = { - baseIndex: (__ITER * VUS) + (__VU - 1) + 1, + baseIndex: (__ITER * VU_COUNT) + (__VU - 1) + 1, seq: 0, minBid: null, }; @@ -250,6 +333,11 @@ export default function () { ); const startBidding = () => { + if (USE_STEP_SCENARIO) { + scheduleStepBids(socket, bidState, Date.now()); + return; + } + sendBid(socket, bidState); if (SEND_EVERY_MS > 0) { socket.setInterval(() => { @@ -258,8 +346,11 @@ export default function () { } }; - if (SEND_DELAY_MS > 0) { - socket.setTimeout(startBidding, SEND_DELAY_MS); + const startDelayMs = USE_STEP_SCENARIO + ? STEP_FIRST_SEND_EVERY_MS + : SEND_DELAY_MS; + if (startDelayMs > 0) { + socket.setTimeout(startBidding, startDelayMs); } else { startBidding(); } @@ -284,6 +375,11 @@ export default function () { `ws.connect status=${res && res.status} error=${res && res.error} code=${res && res.error_code}` ); } + if (LOG_WS_ERRORS && res && res.status !== 101) { + console.warn( + `ws.connect failed status=${res.status} error=${res.error} code=${res.error_code}` + ); + } check(res, { 'status is 101': (r) => r && r.status === 101,