Skip to content
Open
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
14 changes: 14 additions & 0 deletions tests/load/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
BASE_URL=https://codedang.com/api
USERNAME=
PASSWORD=
PROBLEM_ID=395

# Reduced default load. Override as needed.
NORMAL_VUS=20
VILLAIN_VUS=2
RAMP_UP_DURATION=30s
STEADY_DURATION=2m
RAMP_DOWN_DURATION=30s

# Enable with: --out experimental-prometheus-rw
# K6_PROMETHEUS_RW_SERVER_URL=https://prometheus.codedang.com/api/v1/write
61 changes: 53 additions & 8 deletions tests/load/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ k6 (외부)

| 시나리오 | VU | 설명 |
|----------|---:|------|
| `normal_users` | 140 | 언어별 정답 코드 제출 (A+B) |
| `villain_users` | 10 | 자원 소모 악성 코드 제출 (무한루프, 메모리폭탄 등) |
| `normal_users` | 20 | 언어별 정답 코드 제출 (A+B) |
| `villain_users` | 2 | 자원 소모 악성 코드 제출 (무한루프, 메모리폭탄 등) |

**단계**: 2분 ramp-up → 10분 steady → 2분 ramp-down (총 14분)
**기본 단계**: 30초 ramp-up → 2분 steady → 30초 ramp-down (총 3분)

VU 수와 시간은 `.env` 또는 `k6 run -e`로 조정할 수 있습니다.

## 사전 준비

Expand All @@ -29,16 +31,54 @@ k6 (외부)

## 실행

로컬 설정 파일을 만듭니다. `.env`는 git에 커밋되지 않습니다.

```bash
cd tests/load
cp .env.example .env
```

`.env` 예시:

```dotenv
BASE_URL=https://codedang.com/api
USERNAME=tasoo1118
PASSWORD=your-password
PROBLEM_ID=395

NORMAL_VUS=20
VILLAIN_VUS=2
RAMP_UP_DURATION=30s
STEADY_DURATION=2m
RAMP_DOWN_DURATION=30s
```

실행:

```bash
cd tests/load
k6 run \
--out "json=results/$(date +%Y%m%d-%H%M%S).json" \
submission-load.js
```

`-e`로 넘긴 값은 `.env`보다 우선합니다.

```bash
k6 run \
-e NORMAL_VUS=5 \
-e VILLAIN_VUS=0 \
-e STEADY_DURATION=30s \
--out "json=results/$(date +%Y%m%d-%H%M%S).json" \
submission-load.js
```

Prometheus remote write를 함께 사용할 때:

```bash
k6 run \
-e BASE_URL=https://codedang.com/api \
-e USERNAME=loadtest-user \
-e PASSWORD=your-password \
-e PROBLEM_ID=123 \
--out experimental-prometheus-rw \
--out "json=results/$(date +%Y%m%d-%H%M%S).json" \
-e K6_PROMETHEUS_RW_SERVER_URL=https://prometheus.codedang.com/api/v1/write \
submission-load.js
```

Expand All @@ -60,6 +100,11 @@ k6 run \
| `USERNAME` | O | 로그인 사용자명 |
| `PASSWORD` | O | 로그인 비밀번호 |
| `PROBLEM_ID` | O | 테스트 대상 문제 ID |
| `NORMAL_VUS` | | 일반 사용자 VU 수 (기본: `20`) |
| `VILLAIN_VUS` | | 악성 사용자 VU 수 (기본: `2`, `0`이면 비활성화) |
| `RAMP_UP_DURATION` | | ramp-up 시간 (기본: `30s`) |
| `STEADY_DURATION` | | steady 시간 (기본: `2m`) |
| `RAMP_DOWN_DURATION` | | ramp-down 시간 (기본: `30s`) |
| `K6_PROMETHEUS_RW_SERVER_URL` | | Prometheus remote write URL (`--out` 사용 시) |

## Grafana 대시보드 가이드
Expand Down
116 changes: 87 additions & 29 deletions tests/load/submission-load.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,99 @@ const submissionErrors = new Counter('submission_errors')
const submissionDuration = new Trend('submission_duration', true)

// ── Environment variables ───────────────────────────────────────
const BASE_URL = __ENV.BASE_URL || 'https://codedang.com/api'
const USERNAME = __ENV.USERNAME
const PASSWORD = __ENV.PASSWORD
const PROBLEM_ID = __ENV.PROBLEM_ID
function loadDotEnv() {
try {
return parseDotEnv(open('./.env'))
} catch (_) {
return {}
}
}
Comment thread
tasoo-oos marked this conversation as resolved.

function parseDotEnv(content) {
const env = {}
for (const line of content.split(/\r?\n/)) {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) continue

const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/)
if (!match) continue

let value = match[2].trim()
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1)
}

env[match[1]] = value
}
return env
}

const dotenv = loadDotEnv()

function env(name, fallback) {
const value = __ENV[name] !== undefined ? __ENV[name] : dotenv[name]
return value !== undefined && value !== '' ? value : fallback
}

function numberEnv(name, fallback) {
const value = Number(env(name, fallback))
if (!Number.isFinite(value) || value < 0) {
throw new Error(`${name} must be a non-negative number`)
}
return value
}

const BASE_URL = env('BASE_URL', 'https://codedang.com/api')
const USERNAME = env('USERNAME')
const PASSWORD = env('PASSWORD')
const PROBLEM_ID = env('PROBLEM_ID')

const NORMAL_VUS = numberEnv('NORMAL_VUS', 20)
const VILLAIN_VUS = numberEnv('VILLAIN_VUS', 2)
const RAMP_UP_DURATION = env('RAMP_UP_DURATION', '30s')
const STEADY_DURATION = env('STEADY_DURATION', '2m')
const RAMP_DOWN_DURATION = env('RAMP_DOWN_DURATION', '30s')

if (NORMAL_VUS === 0 && VILLAIN_VUS === 0) {
throw new Error('At least one of NORMAL_VUS or VILLAIN_VUS must be greater than 0')
}

if (!USERNAME || !PASSWORD || !PROBLEM_ID) {
throw new Error(
'Required env vars: USERNAME, PASSWORD, PROBLEM_ID\n' +
'Usage: k6 run -e USERNAME=x -e PASSWORD=x -e PROBLEM_ID=123 submission-load.js'
'Usage: create .env or run k6 with -e USERNAME=x -e PASSWORD=x -e PROBLEM_ID=123 submission-load.js'
)
}

function rampingScenario(exec, vus, tags) {
return {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: RAMP_UP_DURATION, target: vus },
{ duration: STEADY_DURATION, target: vus },
{ duration: RAMP_DOWN_DURATION, target: 0 }
],
exec,
tags
}
}

const scenarios = {}
if (NORMAL_VUS > 0) {
scenarios.normal_users = rampingScenario('normalSubmission', NORMAL_VUS, {
user_type: 'normal'
})
}
if (VILLAIN_VUS > 0) {
scenarios.villain_users = rampingScenario('villainSubmission', VILLAIN_VUS, {
user_type: 'villain'
})
}

// ── Payloads ────────────────────────────────────────────────────
const normalPayloads = new SharedArray('normal', () => [
{ lang: 'C', code: open('./payloads/normal/solution.c') },
Expand All @@ -39,30 +120,7 @@ const villainPayloads = new SharedArray('villain', () => [

// ── Scenarios ───────────────────────────────────────────────────
export const options = {
scenarios: {
normal_users: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 140 },
{ duration: '10m', target: 140 },
{ duration: '2m', target: 0 }
],
exec: 'normalSubmission',
tags: { user_type: 'normal' }
},
villain_users: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 10 },
{ duration: '10m', target: 10 },
{ duration: '2m', target: 0 }
],
exec: 'villainSubmission',
tags: { user_type: 'villain' }
}
},
scenarios,
thresholds: {
http_req_duration: ['p(95)<5000'],
submission_errors: ['count<50']
Expand Down
Loading