feat: Enhance Gateway Orchestrator with preopen and open scheduling, add error handling for paused states, and implement new profile management features
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
"scripts": {
|
||||
"build": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/gateway-api",
|
||||
"dev": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/gateway-api --watch",
|
||||
"orchestrator": "GATEWAY_ROLE=orchestrator node dist/index.js",
|
||||
"lint": "node -e \"console.log('lint not configured')\"",
|
||||
"test": "vitest run --config vitest.config.ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
|
||||
@@ -58,7 +58,10 @@ export const adminRouter = router({
|
||||
scenario: z.string().min(1).max(64),
|
||||
apiPort: z.number().int().min(1).max(65535),
|
||||
status: zProfileStatus.optional(),
|
||||
preopenAt: z.string().datetime().optional(),
|
||||
openAt: z.string().datetime().optional(),
|
||||
scheduledStartAt: z.string().datetime().optional(),
|
||||
buildCommitSha: z.string().min(7).max(64).optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
@@ -68,7 +71,10 @@ export const adminRouter = router({
|
||||
scenario: input.scenario,
|
||||
apiPort: input.apiPort,
|
||||
status,
|
||||
preopenAt: input.preopenAt,
|
||||
openAt: input.openAt,
|
||||
scheduledStartAt: input.scheduledStartAt,
|
||||
buildCommitSha: input.buildCommitSha,
|
||||
});
|
||||
}),
|
||||
setStatus: adminProcedure
|
||||
@@ -76,21 +82,34 @@ export const adminRouter = router({
|
||||
z.object({
|
||||
profileName: z.string().min(1),
|
||||
status: zProfileStatus,
|
||||
preopenAt: z.string().datetime().optional(),
|
||||
openAt: z.string().datetime().optional(),
|
||||
scheduledStartAt: z.string().datetime().optional(),
|
||||
buildCommitSha: z.string().min(7).max(64).optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (input.status === 'RESERVED' && !input.scheduledStartAt) {
|
||||
if (input.status === 'RESERVED' && (!input.preopenAt || !input.openAt)) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'scheduledStartAt is required for RESERVED status.',
|
||||
message: 'preopenAt and openAt are required for RESERVED status.',
|
||||
});
|
||||
}
|
||||
const result = await ctx.profiles.updateStatus(
|
||||
input.profileName,
|
||||
input.status,
|
||||
input.status === 'RESERVED' ? input.scheduledStartAt : null
|
||||
{
|
||||
preopenAt: input.preopenAt,
|
||||
openAt: input.openAt,
|
||||
scheduledStartAt:
|
||||
input.status === 'RESERVED' ? input.scheduledStartAt : null,
|
||||
}
|
||||
);
|
||||
if (input.buildCommitSha) {
|
||||
await ctx.profiles.updateBuildStatus(input.profileName, 'IDLE', {
|
||||
commitSha: input.buildCommitSha,
|
||||
});
|
||||
}
|
||||
await ctx.orchestrator.reconcileNow();
|
||||
return result;
|
||||
}),
|
||||
|
||||
@@ -23,6 +23,16 @@ export interface GatewayApiConfig {
|
||||
worktreeRoot: string;
|
||||
}
|
||||
|
||||
export interface GatewayOrchestratorConfig {
|
||||
redisKeyPrefix: string;
|
||||
gameTokenSecret: string;
|
||||
orchestratorReconcileIntervalMs: number;
|
||||
orchestratorScheduleIntervalMs: number;
|
||||
orchestratorBuildIntervalMs: number;
|
||||
workspaceRootHint: string;
|
||||
worktreeRoot: string;
|
||||
}
|
||||
|
||||
const parseNumber = (value: string | undefined, fallback: number, label: string): number => {
|
||||
if (!value) {
|
||||
return fallback;
|
||||
@@ -85,7 +95,40 @@ export const resolveGatewayApiConfigFromEnv = (
|
||||
kakaoRedirectUri,
|
||||
publicBaseUrl,
|
||||
adminToken: env.GATEWAY_ADMIN_TOKEN,
|
||||
orchestratorEnabled: parseBoolean(env.GATEWAY_ORCHESTRATOR_ENABLED, true),
|
||||
orchestratorEnabled: parseBoolean(env.GATEWAY_ORCHESTRATOR_ENABLED, false),
|
||||
orchestratorReconcileIntervalMs: parseNumber(
|
||||
env.GATEWAY_ORCHESTRATOR_RECONCILE_MS,
|
||||
15000,
|
||||
'GATEWAY_ORCHESTRATOR_RECONCILE_MS'
|
||||
),
|
||||
orchestratorScheduleIntervalMs: parseNumber(
|
||||
env.GATEWAY_ORCHESTRATOR_SCHEDULE_MS,
|
||||
5000,
|
||||
'GATEWAY_ORCHESTRATOR_SCHEDULE_MS'
|
||||
),
|
||||
orchestratorBuildIntervalMs: parseNumber(
|
||||
env.GATEWAY_ORCHESTRATOR_BUILD_MS,
|
||||
10000,
|
||||
'GATEWAY_ORCHESTRATOR_BUILD_MS'
|
||||
),
|
||||
workspaceRootHint: env.GATEWAY_WORKSPACE_ROOT ?? process.cwd(),
|
||||
worktreeRoot:
|
||||
env.GATEWAY_WORKTREE_ROOT ??
|
||||
path.resolve(env.GATEWAY_WORKSPACE_ROOT ?? process.cwd(), '.worktrees'),
|
||||
};
|
||||
};
|
||||
|
||||
export const resolveGatewayOrchestratorConfigFromEnv = (
|
||||
env: NodeJS.ProcessEnv = process.env
|
||||
): GatewayOrchestratorConfig => {
|
||||
const secret = env.GAME_TOKEN_SECRET ?? env.GATEWAY_TOKEN_SECRET ?? '';
|
||||
if (!secret) {
|
||||
throw new Error('GAME_TOKEN_SECRET is required for game server processes.');
|
||||
}
|
||||
const redisKeyPrefix = env.GATEWAY_REDIS_PREFIX ?? 'sammo:gateway';
|
||||
return {
|
||||
redisKeyPrefix,
|
||||
gameTokenSecret: secret,
|
||||
orchestratorReconcileIntervalMs: parseNumber(
|
||||
env.GATEWAY_ORCHESTRATOR_RECONCILE_MS,
|
||||
15000,
|
||||
|
||||
@@ -2,6 +2,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { runGatewayApiServer } from './server.js';
|
||||
import { runGatewayOrchestrator } from './orchestrator/orchestratorServer.js';
|
||||
|
||||
export * from './config.js';
|
||||
export * from './context.js';
|
||||
@@ -27,8 +28,11 @@ const isMain = (): boolean => {
|
||||
};
|
||||
|
||||
if (isMain()) {
|
||||
runGatewayApiServer().catch((error) => {
|
||||
console.error('[gateway-api] failed to start', error);
|
||||
const role = process.env.GATEWAY_ROLE ?? 'api';
|
||||
const run = role === 'orchestrator' ? runGatewayOrchestrator : runGatewayApiServer;
|
||||
run().catch((error) => {
|
||||
const prefix = role === 'orchestrator' ? 'gateway-orchestrator' : 'gateway-api';
|
||||
console.error(`[${prefix}] failed to start`, error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,7 +54,12 @@ export const planProfileReconcile = (
|
||||
status: GatewayProfileStatus,
|
||||
runtime: ProfileRuntimeState
|
||||
): { shouldStart: boolean; shouldStop: boolean } => {
|
||||
if (status === 'RUNNING') {
|
||||
if (
|
||||
status === 'RUNNING' ||
|
||||
status === 'PREOPEN' ||
|
||||
status === 'PAUSED' ||
|
||||
status === 'COMPLETED'
|
||||
) {
|
||||
return {
|
||||
shouldStart: !(runtime.apiRunning && runtime.daemonRunning),
|
||||
shouldStop: false,
|
||||
@@ -94,6 +99,7 @@ const buildProcessDefinitions = (
|
||||
TURN_PROFILE: profile.profile,
|
||||
PROFILE: profile.profile,
|
||||
SCENARIO: profile.scenario,
|
||||
TURN_PROFILE_NAME: profile.profileName,
|
||||
};
|
||||
return {
|
||||
api: {
|
||||
@@ -221,18 +227,41 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
const now = this.now();
|
||||
const due = await this.repository.listReservedToStart(now);
|
||||
for (const profile of due) {
|
||||
try {
|
||||
await this.repository.updateStatus(
|
||||
profile.profileName,
|
||||
'RUNNING',
|
||||
null
|
||||
);
|
||||
await this.startProfile(profile);
|
||||
} catch (error) {
|
||||
if (!profile.preopenAt || !profile.openAt) {
|
||||
await this.repository.updateLastError(
|
||||
profile.profileName,
|
||||
error instanceof Error ? error.message : 'Failed to start scheduled profile.'
|
||||
'Reserved profile is missing preopen/open schedule.'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!profile.buildCommitSha) {
|
||||
await this.repository.updateLastError(
|
||||
profile.profileName,
|
||||
'Reserved profile is missing build commit SHA.'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const queued =
|
||||
profile.buildStatus === 'QUEUED' || profile.buildStatus === 'RUNNING';
|
||||
if (!queued) {
|
||||
await this.repository.updateBuildStatus(profile.profileName, 'QUEUED', {
|
||||
requestedAt: now.toISOString(),
|
||||
error: null,
|
||||
commitSha: profile.buildCommitSha,
|
||||
});
|
||||
}
|
||||
}
|
||||
const profiles = await this.repository.listProfiles();
|
||||
for (const profile of profiles) {
|
||||
if (
|
||||
profile.status === 'PREOPEN' &&
|
||||
profile.openAt &&
|
||||
new Date(profile.openAt) <= now
|
||||
) {
|
||||
await this.repository.updateStatus(profile.profileName, 'RUNNING', {
|
||||
preopenAt: profile.preopenAt ?? null,
|
||||
openAt: profile.openAt ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -304,12 +333,24 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
completedAt,
|
||||
error: null,
|
||||
});
|
||||
if (queued.status !== 'RUNNING' && queued.status !== 'DISABLED') {
|
||||
if (queued.status === 'RESERVED') {
|
||||
await this.repository.updateStatus(
|
||||
queued.profileName,
|
||||
'COMPLETED',
|
||||
queued.scheduledStartAt ? queued.scheduledStartAt : null
|
||||
queued.openAt && new Date(queued.openAt) <= this.now()
|
||||
? 'RUNNING'
|
||||
: 'PREOPEN',
|
||||
{
|
||||
preopenAt: queued.preopenAt ?? null,
|
||||
openAt: queued.openAt ?? null,
|
||||
}
|
||||
);
|
||||
} else if (queued.status === 'PREOPEN' && queued.openAt) {
|
||||
if (new Date(queued.openAt) <= this.now()) {
|
||||
await this.repository.updateStatus(queued.profileName, 'RUNNING', {
|
||||
preopenAt: queued.preopenAt ?? null,
|
||||
openAt: queued.openAt ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await this.repository.updateBuildStatus(queued.profileName, 'FAILED', {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { PrismaClient } from '@prisma/client';
|
||||
|
||||
import type { GatewayOrchestratorConfig } from '../config.js';
|
||||
import { createGatewayProfileRepository } from './profileRepository.js';
|
||||
import { GatewayOrchestrator } from './gatewayOrchestrator.js';
|
||||
import { Pm2ProcessManager } from './pm2ProcessManager.js';
|
||||
import { PnpmBuildRunner } from './buildRunner.js';
|
||||
import { resolveWorkspaceRoot } from './workspaceRoot.js';
|
||||
import { GitWorkspaceManager } from './workspaceManager.js';
|
||||
|
||||
export const buildEnvMap = (env: NodeJS.ProcessEnv): Record<string, string> => {
|
||||
const entries = Object.entries(env).filter(
|
||||
(entry): entry is [string, string] => typeof entry[1] === 'string'
|
||||
);
|
||||
return Object.fromEntries(entries);
|
||||
};
|
||||
|
||||
export const createGatewayOrchestrator = (
|
||||
prisma: PrismaClient,
|
||||
config: GatewayOrchestratorConfig,
|
||||
env: NodeJS.ProcessEnv = process.env
|
||||
): {
|
||||
orchestrator: GatewayOrchestrator;
|
||||
profiles: ReturnType<typeof createGatewayProfileRepository>;
|
||||
} => {
|
||||
const profiles = createGatewayProfileRepository(prisma);
|
||||
const workspaceRoot = resolveWorkspaceRoot(config.workspaceRootHint);
|
||||
const processManager = new Pm2ProcessManager();
|
||||
const buildRunner = new PnpmBuildRunner();
|
||||
const baseEnv = buildEnvMap(env);
|
||||
const workspaceManager = new GitWorkspaceManager({
|
||||
repoRoot: workspaceRoot,
|
||||
worktreeRoot: config.worktreeRoot,
|
||||
baseEnv,
|
||||
});
|
||||
const orchestrator = new GatewayOrchestrator({
|
||||
repository: profiles,
|
||||
processManager,
|
||||
buildRunner,
|
||||
workspaceManager,
|
||||
processConfig: {
|
||||
workspaceRoot,
|
||||
redisKeyPrefix: config.redisKeyPrefix,
|
||||
gameTokenSecret: config.gameTokenSecret,
|
||||
baseEnv,
|
||||
},
|
||||
reconcileIntervalMs: config.orchestratorReconcileIntervalMs,
|
||||
scheduleIntervalMs: config.orchestratorScheduleIntervalMs,
|
||||
buildIntervalMs: config.orchestratorBuildIntervalMs,
|
||||
});
|
||||
return { orchestrator, profiles };
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { PrismaClient } from '@prisma/client';
|
||||
import {
|
||||
createPostgresConnector,
|
||||
resolvePostgresConfigFromEnv,
|
||||
} from '@sammo-ts/infra';
|
||||
|
||||
import { resolveGatewayOrchestratorConfigFromEnv } from '../config.js';
|
||||
import { createGatewayOrchestrator } from './orchestratorFactory.js';
|
||||
|
||||
export const runGatewayOrchestrator = async (): Promise<void> => {
|
||||
const config = resolveGatewayOrchestratorConfigFromEnv();
|
||||
const postgres = createPostgresConnector(resolvePostgresConfigFromEnv());
|
||||
await postgres.connect();
|
||||
|
||||
const { orchestrator } = createGatewayOrchestrator(
|
||||
postgres.prisma as PrismaClient,
|
||||
config,
|
||||
process.env
|
||||
);
|
||||
|
||||
const stop = async (reason: string): Promise<void> => {
|
||||
console.info(`[gateway-orchestrator] stopping: ${reason}`);
|
||||
await orchestrator.stop();
|
||||
await postgres.disconnect();
|
||||
};
|
||||
|
||||
process.on('SIGINT', () => void stop('SIGINT'));
|
||||
process.on('SIGTERM', () => void stop('SIGTERM'));
|
||||
|
||||
orchestrator.start();
|
||||
console.info('[gateway-orchestrator] started');
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Prisma, type PrismaClient } from '@prisma/client';
|
||||
|
||||
export const GATEWAY_PROFILE_STATUSES = [
|
||||
'COMPLETED',
|
||||
'RESERVED',
|
||||
'PREOPEN',
|
||||
'RUNNING',
|
||||
'PAUSED',
|
||||
'COMPLETED',
|
||||
'STOPPED',
|
||||
'DISABLED',
|
||||
] as const;
|
||||
@@ -28,6 +30,8 @@ export interface GatewayProfileRecord {
|
||||
buildCommitSha?: string;
|
||||
buildWorkspace?: string;
|
||||
buildLastUsedAt?: string;
|
||||
preopenAt?: string;
|
||||
openAt?: string;
|
||||
scheduledStartAt?: string;
|
||||
buildRequestedAt?: string;
|
||||
buildStartedAt?: string;
|
||||
@@ -44,7 +48,10 @@ export interface GatewayProfileUpsertInput {
|
||||
scenario: string;
|
||||
apiPort: number;
|
||||
status?: GatewayProfileStatus;
|
||||
preopenAt?: string;
|
||||
openAt?: string;
|
||||
scheduledStartAt?: string;
|
||||
buildCommitSha?: string;
|
||||
meta?: Prisma.JsonObject;
|
||||
}
|
||||
|
||||
@@ -55,7 +62,11 @@ export interface GatewayProfileRepository {
|
||||
updateStatus(
|
||||
profileName: string,
|
||||
status: GatewayProfileStatus,
|
||||
scheduledStartAt?: string | null
|
||||
schedule?: {
|
||||
preopenAt?: string | null;
|
||||
openAt?: string | null;
|
||||
scheduledStartAt?: string | null;
|
||||
}
|
||||
): Promise<GatewayProfileRecord | null>;
|
||||
updateBuildStatus(
|
||||
profileName: string,
|
||||
@@ -94,6 +105,8 @@ const mapProfile = (row: {
|
||||
buildCommitSha: string | null;
|
||||
buildWorkspace: string | null;
|
||||
buildLastUsedAt: Date | null;
|
||||
preopenAt: Date | null;
|
||||
openAt: Date | null;
|
||||
scheduledStartAt: Date | null;
|
||||
buildRequestedAt: Date | null;
|
||||
buildStartedAt: Date | null;
|
||||
@@ -113,6 +126,8 @@ const mapProfile = (row: {
|
||||
buildCommitSha: row.buildCommitSha ?? undefined,
|
||||
buildWorkspace: row.buildWorkspace ?? undefined,
|
||||
buildLastUsedAt: toIso(row.buildLastUsedAt),
|
||||
preopenAt: toIso(row.preopenAt),
|
||||
openAt: toIso(row.openAt),
|
||||
scheduledStartAt: toIso(row.scheduledStartAt),
|
||||
buildRequestedAt: toIso(row.buildRequestedAt),
|
||||
buildStartedAt: toIso(row.buildStartedAt),
|
||||
@@ -152,19 +167,36 @@ export const createGatewayProfileRepository = (
|
||||
scenario: input.scenario,
|
||||
apiPort: input.apiPort,
|
||||
status: input.status ?? 'STOPPED',
|
||||
preopenAt: input.preopenAt ? new Date(input.preopenAt) : null,
|
||||
openAt: input.openAt ? new Date(input.openAt) : null,
|
||||
scheduledStartAt: input.scheduledStartAt
|
||||
? new Date(input.scheduledStartAt)
|
||||
: null,
|
||||
buildCommitSha: input.buildCommitSha ?? null,
|
||||
meta: (input.meta ?? {}) as Prisma.JsonObject,
|
||||
},
|
||||
update: {
|
||||
apiPort: input.apiPort,
|
||||
status: input.status,
|
||||
preopenAt: input.preopenAt
|
||||
? new Date(input.preopenAt)
|
||||
: input.preopenAt === null
|
||||
? null
|
||||
: undefined,
|
||||
openAt: input.openAt
|
||||
? new Date(input.openAt)
|
||||
: input.openAt === null
|
||||
? null
|
||||
: undefined,
|
||||
scheduledStartAt: input.scheduledStartAt
|
||||
? new Date(input.scheduledStartAt)
|
||||
: input.scheduledStartAt === null
|
||||
? null
|
||||
: undefined,
|
||||
buildCommitSha:
|
||||
input.buildCommitSha === undefined
|
||||
? undefined
|
||||
: input.buildCommitSha,
|
||||
meta: input.meta ? (input.meta as Prisma.JsonObject) : undefined,
|
||||
},
|
||||
});
|
||||
@@ -173,17 +205,33 @@ export const createGatewayProfileRepository = (
|
||||
async updateStatus(
|
||||
profileName: string,
|
||||
status: GatewayProfileStatus,
|
||||
scheduledStartAt?: string | null
|
||||
schedule?: {
|
||||
preopenAt?: string | null;
|
||||
openAt?: string | null;
|
||||
scheduledStartAt?: string | null;
|
||||
}
|
||||
): Promise<GatewayProfileRecord | null> {
|
||||
const row = await prisma.gatewayProfile.update({
|
||||
where: { profileName },
|
||||
data: {
|
||||
status,
|
||||
scheduledStartAt:
|
||||
scheduledStartAt === undefined
|
||||
preopenAt:
|
||||
schedule?.preopenAt === undefined
|
||||
? undefined
|
||||
: scheduledStartAt
|
||||
? new Date(scheduledStartAt)
|
||||
: schedule?.preopenAt
|
||||
? new Date(schedule.preopenAt)
|
||||
: null,
|
||||
openAt:
|
||||
schedule?.openAt === undefined
|
||||
? undefined
|
||||
: schedule?.openAt
|
||||
? new Date(schedule.openAt)
|
||||
: null,
|
||||
scheduledStartAt:
|
||||
schedule?.scheduledStartAt === undefined
|
||||
? undefined
|
||||
: schedule?.scheduledStartAt
|
||||
? new Date(schedule.scheduledStartAt)
|
||||
: null,
|
||||
},
|
||||
});
|
||||
@@ -240,7 +288,7 @@ export const createGatewayProfileRepository = (
|
||||
const rows = await prisma.gatewayProfile.findMany({
|
||||
where: {
|
||||
status: 'RESERVED',
|
||||
scheduledStartAt: {
|
||||
preopenAt: {
|
||||
lte: now,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -16,21 +16,9 @@ import { KakaoOAuthClient } from './auth/kakaoClient.js';
|
||||
import { RedisOAuthSessionStore } from './auth/oauthSessionStore.js';
|
||||
import { createPostgresUserRepository } from './auth/postgresUserRepository.js';
|
||||
import { RedisGatewaySessionService } from './auth/redisSessionService.js';
|
||||
import { createGatewayProfileRepository } from './orchestrator/profileRepository.js';
|
||||
import { GatewayOrchestrator } from './orchestrator/gatewayOrchestrator.js';
|
||||
import { Pm2ProcessManager } from './orchestrator/pm2ProcessManager.js';
|
||||
import { PnpmBuildRunner } from './orchestrator/buildRunner.js';
|
||||
import { resolveWorkspaceRoot } from './orchestrator/workspaceRoot.js';
|
||||
import { GitWorkspaceManager } from './orchestrator/workspaceManager.js';
|
||||
import { createGatewayOrchestrator } from './orchestrator/orchestratorFactory.js';
|
||||
import { appRouter } from './router.js';
|
||||
|
||||
const buildEnvMap = (env: NodeJS.ProcessEnv): Record<string, string> => {
|
||||
const entries = Object.entries(env).filter(
|
||||
(entry): entry is [string, string] => typeof entry[1] === 'string'
|
||||
);
|
||||
return Object.fromEntries(entries);
|
||||
};
|
||||
|
||||
export const createGatewayApiServer = async () => {
|
||||
const config = resolveGatewayApiConfigFromEnv();
|
||||
const postgres = createPostgresConnector(resolvePostgresConfigFromEnv());
|
||||
@@ -58,33 +46,11 @@ export const createGatewayApiServer = async () => {
|
||||
config.oauthSessionTtlSeconds
|
||||
);
|
||||
|
||||
const profiles = createGatewayProfileRepository(
|
||||
postgres.prisma as PrismaClient
|
||||
const { orchestrator, profiles } = createGatewayOrchestrator(
|
||||
postgres.prisma as PrismaClient,
|
||||
config,
|
||||
process.env
|
||||
);
|
||||
const workspaceRoot = resolveWorkspaceRoot(config.workspaceRootHint);
|
||||
const processManager = new Pm2ProcessManager();
|
||||
const buildRunner = new PnpmBuildRunner();
|
||||
const baseEnv = buildEnvMap(process.env);
|
||||
const workspaceManager = new GitWorkspaceManager({
|
||||
repoRoot: workspaceRoot,
|
||||
worktreeRoot: config.worktreeRoot,
|
||||
baseEnv,
|
||||
});
|
||||
const orchestrator = new GatewayOrchestrator({
|
||||
repository: profiles,
|
||||
processManager,
|
||||
buildRunner,
|
||||
workspaceManager,
|
||||
processConfig: {
|
||||
workspaceRoot,
|
||||
redisKeyPrefix: config.redisKeyPrefix,
|
||||
gameTokenSecret: config.gameTokenSecret,
|
||||
baseEnv,
|
||||
},
|
||||
reconcileIntervalMs: config.orchestratorReconcileIntervalMs,
|
||||
scheduleIntervalMs: config.orchestratorScheduleIntervalMs,
|
||||
buildIntervalMs: config.orchestratorBuildIntervalMs,
|
||||
});
|
||||
|
||||
const app = fastify({
|
||||
logger: true,
|
||||
@@ -121,10 +87,6 @@ export const createGatewayApiServer = async () => {
|
||||
ok: true,
|
||||
}));
|
||||
|
||||
if (config.orchestratorEnabled) {
|
||||
orchestrator.start();
|
||||
}
|
||||
|
||||
app.addHook('onClose', async () => {
|
||||
await orchestrator.stop();
|
||||
await redis.disconnect();
|
||||
|
||||
@@ -12,6 +12,15 @@ describe('planProfileReconcile', () => {
|
||||
).toEqual({ shouldStart: true, shouldStop: false });
|
||||
});
|
||||
|
||||
it('starts processes for preopen profiles', () => {
|
||||
expect(
|
||||
planProfileReconcile('PREOPEN', {
|
||||
apiRunning: false,
|
||||
daemonRunning: false,
|
||||
})
|
||||
).toEqual({ shouldStart: true, shouldStop: false });
|
||||
});
|
||||
|
||||
it('does nothing when running profile is healthy', () => {
|
||||
expect(
|
||||
planProfileReconcile('RUNNING', {
|
||||
|
||||
Reference in New Issue
Block a user