perf: 프로필 DB 풀과 advisory lock 격리

역할별 PostgreSQL pool 상한과 동일 process 공유 pool을 적용하고 health 지표를 노출한다.\n\n게임 기능 advisory lock을 schema namespace로 통일하고 실제 PostgreSQL 통합 하네스를 보강한다.
This commit is contained in:
2026-08-17 17:30:06 +00:00
parent 0d535a8221
commit 6f2d6f0379
30 changed files with 423 additions and 70 deletions
+4
View File
@@ -29,6 +29,10 @@ Gateway process 환경에 전달하지 않습니다. 이 값이 frontend 정의
frontend build 계약입니다.
- `RELEASE_CONTROLLER_POLL_MS`, `RELEASE_CONTROLLER_READINESS_TIMEOUT_MS`: queue
poll과 준비 제한 시간입니다.
- `RELEASE_CONTROLLER_POSTGRES_POOL_MAX`: controller 자체 Gateway DB pool 상한이며
기본값은 2입니다. Gateway API/orchestrator는 각각
`GATEWAY_API_POSTGRES_POOL_MAX`(기본 4),
`GATEWAY_ORCHESTRATOR_POSTGRES_POOL_MAX`(기본 2)를 사용합니다.
- `TURBO_CACHE_DIR`: 선택 사항인 공유 local cache 경로입니다. 없으면 원래
`RELEASE_CONTROLLER_WORKSPACE_ROOT/.turbo/release-cache`를 사용합니다. 상대 경로는
원래 workspace 기준으로 해석합니다.
+3
View File
@@ -1,6 +1,7 @@
import path from 'node:path';
import { sanitizeManagedProcessEnv } from '@sammo-ts/gateway-api';
import { resolvePostgresPoolMax } from '@sammo-ts/infra';
const parsePositiveInt = (value: string | undefined, fallback: number, name: string): number => {
if (!value) return fallback;
@@ -25,6 +26,7 @@ export interface ReleaseControllerConfig {
gatewayBasePath: string;
pollIntervalMs: number;
readinessTimeoutMs: number;
postgresPoolMax: number;
baseEnv: Record<string, string>;
}
@@ -54,6 +56,7 @@ export const resolveReleaseControllerConfig = (env: NodeJS.ProcessEnv = process.
60000,
'RELEASE_CONTROLLER_READINESS_TIMEOUT_MS'
),
postgresPoolMax: resolvePostgresPoolMax(env.RELEASE_CONTROLLER_POSTGRES_POOL_MAX ?? env.POSTGRES_POOL_MAX, 2),
baseEnv: {
...sanitizeManagedProcessEnv(env),
REDIS_URL: env.REDIS_URL.trim(),
+4 -1
View File
@@ -16,7 +16,10 @@ export * from './selfUpgrade.js';
const main = async (): Promise<void> => {
const config = resolveReleaseControllerConfig();
const postgres = createGatewayPostgresConnector({ url: config.gatewayDatabaseUrl });
const postgres = createGatewayPostgresConnector({
url: config.gatewayDatabaseUrl,
maxConnections: config.postgresPoolMax,
});
await postgres.connect();
const repository = createGatewayReleaseRepository(postgres.prisma as GatewayPrismaClient);
const workspaceManager = new GitWorkspaceManager({
@@ -17,6 +17,7 @@ import {
readReleaseManifest,
sanitizeManagedProcessEnv,
} from '@sammo-ts/gateway-api';
import { resolvePostgresPoolMax } from '@sammo-ts/infra';
import type { ReleaseControllerConfig } from './config.js';
@@ -25,6 +26,9 @@ const HEARTBEAT_INTERVAL_MS = 60_000;
const PROCESS_NAMES = ['sammo:gateway-api', 'sammo:gateway-frontend', 'sammo:gateway-orchestrator'] as const;
const SENSITIVE_ENV_NAME = /(SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE_KEY|CLIENT_SECRET|DATABASE_URL|REDIS_URL)/iu;
const managedPostgresPoolMax = (env: Record<string, string>, roleVariable: string, fallback: number): string =>
String(resolvePostgresPoolMax(env[roleVariable] ?? env.POSTGRES_POOL_MAX, fallback));
const buildGatewayReleaseCommands = (
workspaceRoot: string,
needsInstall: boolean,
@@ -78,7 +82,16 @@ export const buildGatewayProcessDefinitions = (
GATEWAY_DATABASE_URL: config.gatewayDatabaseUrl,
};
return [
{ name: 'sammo:gateway-api', script: apiScript, cwd: apiCwd, env: { ...env, GATEWAY_ROLE: 'api' } },
{
name: 'sammo:gateway-api',
script: apiScript,
cwd: apiCwd,
env: {
...env,
POSTGRES_POOL_MAX: managedPostgresPoolMax(config.baseEnv, 'GATEWAY_API_POSTGRES_POOL_MAX', 4),
GATEWAY_ROLE: 'api',
},
},
{
name: 'sammo:gateway-frontend',
script: frontendScript,
@@ -90,7 +103,11 @@ export const buildGatewayProcessDefinitions = (
name: 'sammo:gateway-orchestrator',
script: apiScript,
cwd: apiCwd,
env: { ...env, GATEWAY_ROLE: 'orchestrator' },
env: {
...env,
POSTGRES_POOL_MAX: managedPostgresPoolMax(config.baseEnv, 'GATEWAY_ORCHESTRATOR_POSTGRES_POOL_MAX', 2),
GATEWAY_ROLE: 'orchestrator',
},
},
];
};
@@ -41,6 +41,7 @@ export const buildReleaseControllerDefinition = (
...sanitizeManagedProcessEnv(config.baseEnv),
GATEWAY_DATABASE_URL: config.gatewayDatabaseUrl,
GATEWAY_DB_SCHEMA: config.gatewayDbSchema,
POSTGRES_POOL_MAX: String(config.postgresPoolMax),
RELEASE_CONTROLLER_WORKSPACE_ROOT: config.workspaceRoot,
RELEASE_CONTROLLER_WORKTREE_ROOT: config.worktreeRoot,
},
@@ -73,6 +73,7 @@ const config: ReleaseControllerConfig = {
gatewayBasePath: '/gateway',
pollIntervalMs: 5,
readinessTimeoutMs: 10,
postgresPoolMax: 2,
baseEnv: {
REDIS_URL: 'redis://integration.invalid:6379/0',
GATEWAY_BOOTSTRAP_TOKEN: 'bootstrap-secret-value',
@@ -134,6 +135,14 @@ it('runs Gateway preview from the frontend workspace dependency', () => {
const definitions = buildGatewayProcessDefinitions('/srv/sammo/release', config);
const frontend = definitions.find((definition) => definition.name === 'sammo:gateway-frontend');
expect(frontend?.script).toBe('/srv/sammo/release/app/gateway-frontend/node_modules/vite/bin/vite.js');
expect(definitions.find((definition) => definition.name === 'sammo:gateway-api')?.env).toHaveProperty(
'POSTGRES_POOL_MAX',
'4'
);
expect(definitions.find((definition) => definition.name === 'sammo:gateway-orchestrator')?.env).toHaveProperty(
'POSTGRES_POOL_MAX',
'2'
);
});
it('does not forward release-controller PM2 identity to Gateway processes', () => {
@@ -323,6 +332,17 @@ describe('resolveReleaseControllerConfig', () => {
});
expect(new URL(resolved.gatewayDatabaseUrl).searchParams.get('schema')).toBe('gateway_release');
expect(resolved.postgresPoolMax).toBe(2);
});
it('applies a dedicated release-controller pool budget', () => {
const resolved = resolveReleaseControllerConfig({
GATEWAY_DATABASE_URL: 'postgresql://user:pass@127.0.0.1:5432/sammo',
REDIS_URL: 'redis://127.0.0.1:6379/0',
RELEASE_CONTROLLER_POSTGRES_POOL_MAX: '3',
});
expect(resolved.postgresPoolMax).toBe(3);
});
it('removes PM2 metadata from the inherited controller environment', () => {
@@ -371,6 +391,7 @@ describe('upgradeReleaseController', () => {
expect(definition.env).toMatchObject({ DATABASE_URL: 'postgresql://integration.invalid/sammo' });
expect(definition.env).toHaveProperty('RELEASE_CONTROLLER_WORKSPACE_ROOT', config.workspaceRoot);
expect(definition.env).toHaveProperty('POSTGRES_POOL_MAX', '2');
expect(definition.env).not.toHaveProperty('pm_id');
expect(definition.env).not.toHaveProperty('pm_exec_path');
expect(definition.env).not.toHaveProperty('name');