From 41214b03dffdcb4c6ae53f40f3cc47df4f5afc59 Mon Sep 17 00:00:00 2001 From: hided62 Date: Tue, 4 Aug 2026 16:18:45 +0000 Subject: [PATCH] fix(runtime): bound build memory and parallelism --- .env.example | 3 +++ README.md | 6 ++++++ compose.yaml | 2 ++ runtime/validate-compose-model.mjs | 11 +++++++++++ test/validate-compose-model.test.mjs | 27 +++++++++++++++++++++++++-- 5 files changed, 47 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index f85a7c2..1f1e6c0 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,9 @@ RUNTIME_MEMORY_LIMIT=4g RUNTIME_MEMORY_SWAP_LIMIT=4g RUNTIME_CPU_LIMIT=4 RUNTIME_PIDS_LIMIT=256 +# Bound Node/Rolldown build parallelism inside the runtime container. +RUNTIME_NODE_OPTIONS=--max-old-space-size=1536 +RUNTIME_RAYON_NUM_THREADS=2 # Optional smoke-only limits used with compose.smoke.yaml (maximum supported defaults shown). # SMOKE_RUNTIME_MEMORY_LIMIT=4g # SMOKE_RUNTIME_MEMORY_SWAP_LIMIT=4g diff --git a/README.md b/README.md index 966f439..fb63185 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,12 @@ runtime은 기본적으로 memory/swap 각각 4 GiB, CPU 4개, PID 256개 상한 무제한으로 두지 않습니다. PM2 process/restart 수가 예상보다 증가하면 로그 수집보다 runtime 중지가 우선입니다. +초기 빌드와 profile worktree 빌드는 기본 Node heap 1536 MiB, +`RAYON_NUM_THREADS=2` 안에서 실행됩니다. `RUNTIME_NODE_OPTIONS`의 heap 상한은 +512~2048 MiB, `RUNTIME_RAYON_NUM_THREADS`는 1~4만 허용됩니다. 이는 빌드 중 +container hard limit에 먼저 닿는 것을 막고 실행 process에도 같은 상한을 +적용합니다. + `scripts/check.sh`는 example placeholder, 짧은 비밀값, 잘못된 domain/email, repository credential 조합을 실제 값 출력 없이 거부합니다. 최소 길이는 DB·Redis password 24자, game/bootstrap token 32자, 최초 관리자 password 16자입니다. diff --git a/compose.yaml b/compose.yaml index 0fedc53..ad71d2d 100644 --- a/compose.yaml +++ b/compose.yaml @@ -49,6 +49,8 @@ services: CORE_REPOSITORY_TOKEN: ${CORE_REPOSITORY_TOKEN:-} CORE_SSH_PRIVATE_KEY_BASE64: ${CORE_SSH_PRIVATE_KEY_BASE64:-} CORE_SSH_KNOWN_HOSTS_BASE64: ${CORE_SSH_KNOWN_HOSTS_BASE64:-} + NODE_OPTIONS: ${RUNTIME_NODE_OPTIONS:---max-old-space-size=1536} + RAYON_NUM_THREADS: ${RUNTIME_RAYON_NUM_THREADS:-2} POSTGRES_HOST: postgres POSTGRES_PORT: '5432' POSTGRES_DB: ${POSTGRES_DB:-sammo} diff --git a/runtime/validate-compose-model.mjs b/runtime/validate-compose-model.mjs index e1826bb..3fd57ca 100644 --- a/runtime/validate-compose-model.mjs +++ b/runtime/validate-compose-model.mjs @@ -19,6 +19,17 @@ export const validateComposeModel = (model, mode) => { if (!positive(runtime.cpus)) errors.push('runtime CPU limit must be positive'); if (!positive(runtime.pids_limit)) errors.push('runtime PID limit must be positive'); + const nodeOptions = runtime.environment?.NODE_OPTIONS ?? ''; + const heapMatch = /(?:^|\s)--max-old-space-size=(\d+)(?:\s|$)/.exec(nodeOptions); + const heapMiB = Number(heapMatch?.[1] ?? 0); + if (!heapMatch || heapMiB < 512 || heapMiB > 2048) { + errors.push('runtime Node heap limit must be between 512 and 2048 MiB'); + } + const rayonThreads = Number(runtime.environment?.RAYON_NUM_THREADS ?? 0); + if (!Number.isInteger(rayonThreads) || rayonThreads < 1 || rayonThreads > 4) { + errors.push('runtime Rayon thread count must be between 1 and 4'); + } + if (mode === 'development') { if (runtime.environment?.RUNTIME_MODE !== 'development') { errors.push('development runtime mode must be literal development'); diff --git a/test/validate-compose-model.test.mjs b/test/validate-compose-model.test.mjs index fa55b38..dbc7b75 100644 --- a/test/validate-compose-model.test.mjs +++ b/test/validate-compose-model.test.mjs @@ -4,7 +4,11 @@ import assert from 'node:assert/strict'; import { validateComposeModel } from '../runtime/validate-compose-model.mjs'; const safeRuntime = { - environment: { CORE_SOURCE_MODE: 'clone' }, + environment: { + CORE_SOURCE_MODE: 'clone', + NODE_OPTIONS: '--max-old-space-size=1536', + RAYON_NUM_THREADS: '2', + }, restart: 'unless-stopped', mem_limit: String(4 * 1024 * 1024 * 1024), memswap_limit: String(4 * 1024 * 1024 * 1024), @@ -21,7 +25,12 @@ test('accepts a literal, non-restarting development model', () => { services.runtime = { ...safeRuntime, restart: 'no', - environment: { CORE_SOURCE_MODE: 'bind', RUNTIME_MODE: 'development' }, + environment: { + CORE_SOURCE_MODE: 'bind', + RUNTIME_MODE: 'development', + NODE_OPTIONS: '--max-old-space-size=1536', + RAYON_NUM_THREADS: '2', + }, }; assert.deepEqual(validateComposeModel({ services }, 'development'), []); }); @@ -47,3 +56,17 @@ test('rejects an unbounded or production-mode development runtime', () => { assert.ok(errors.some((error) => error.includes('literal development'))); assert.ok(errors.some((error) => error.includes('restart policy must be no'))); }); + +test('rejects missing or excessive build memory and parallelism limits', () => { + const runtime = { + ...safeRuntime, + environment: { + CORE_SOURCE_MODE: 'clone', + NODE_OPTIONS: '--max-old-space-size=4096', + RAYON_NUM_THREADS: '8', + }, + }; + const errors = validateComposeModel({ services: { runtime } }, 'production'); + assert.ok(errors.some((error) => error.includes('Node heap limit'))); + assert.ok(errors.some((error) => error.includes('Rayon thread count'))); +});