fix(gateway): split profile identity from scenario
This commit is contained in:
@@ -1280,7 +1280,10 @@ export const adminRouter = router({
|
||||
sourceRef = resolved;
|
||||
}
|
||||
const scenarios = await listScenarioPreviews({ gitRef: resolved });
|
||||
if (!scenarios.some((scenario) => String(scenario.id) === profile.scenario)) {
|
||||
if (
|
||||
profile.currentScenario === null ||
|
||||
!scenarios.some((scenario) => String(scenario.id) === profile.currentScenario)
|
||||
) {
|
||||
throw new Error('Current scenario is not available at source.');
|
||||
}
|
||||
} catch {
|
||||
@@ -1579,6 +1582,8 @@ export const adminRouter = router({
|
||||
.map((profile) => ({
|
||||
profileName: profile.profileName,
|
||||
profile: profile.profile,
|
||||
instanceKey: profile.instanceKey,
|
||||
currentScenario: profile.currentScenario,
|
||||
meta: {
|
||||
...(typeof profile.meta.korName === 'string' ? { korName: profile.meta.korName } : {}),
|
||||
},
|
||||
@@ -1661,7 +1666,8 @@ export const adminRouter = router({
|
||||
);
|
||||
const profile = await ctx.profiles.getProfile(input.profileName);
|
||||
if (!profile) throw new TRPCError({ code: 'NOT_FOUND', message: 'Profile not found.' });
|
||||
const parsedScenarioId = Number(profile.scenario);
|
||||
const parsedScenarioId =
|
||||
profile.currentScenario === null ? Number.NaN : Number(profile.currentScenario);
|
||||
currentScenarioId = Number.isInteger(parsedScenarioId) ? parsedScenarioId : null;
|
||||
gitRef = profile.buildCommitSha?.trim();
|
||||
if (!gitRef) {
|
||||
@@ -1692,8 +1698,13 @@ export const adminRouter = router({
|
||||
upsert: profileAdminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
profile: z.string().min(1).max(32),
|
||||
scenario: z.string().min(1).max(64),
|
||||
profile: z.string().regex(/^[a-z0-9-]{1,32}$/),
|
||||
instanceKey: z
|
||||
.string()
|
||||
.regex(/^[a-z0-9-]{1,64}$/)
|
||||
.optional(),
|
||||
currentScenario: z.string().min(1).max(64).nullable().optional(),
|
||||
scenario: z.string().min(1).max(64).optional(),
|
||||
apiPort: z.number().int().min(1).max(65535),
|
||||
status: zProfileStatus.optional(),
|
||||
preopenAt: z.string().datetime().optional(),
|
||||
@@ -1706,6 +1717,8 @@ export const adminRouter = router({
|
||||
const status = input.status ?? 'STOPPED';
|
||||
return ctx.profiles.upsertProfile({
|
||||
profile: input.profile,
|
||||
instanceKey: input.instanceKey,
|
||||
currentScenario: input.currentScenario,
|
||||
scenario: input.scenario,
|
||||
apiPort: input.apiPort,
|
||||
status,
|
||||
|
||||
@@ -21,6 +21,9 @@ export type LobbyGeneralStatus = {
|
||||
export type LobbyProfileStatus = {
|
||||
profileName: string;
|
||||
profile: string;
|
||||
instanceKey: string;
|
||||
currentScenario: string | null;
|
||||
/** @deprecated Rollback-compatible mirror of currentScenario. */
|
||||
scenario: string;
|
||||
status: GatewayProfileStatus;
|
||||
apiPort: number;
|
||||
@@ -87,6 +90,8 @@ export class RepositoryProfileStatusService implements GatewayProfileStatusServi
|
||||
return {
|
||||
profileName: row.profileName,
|
||||
profile: row.profile,
|
||||
instanceKey: row.instanceKey,
|
||||
currentScenario: row.currentScenario,
|
||||
scenario: row.scenario,
|
||||
status: row.status,
|
||||
apiPort: row.apiPort,
|
||||
|
||||
@@ -396,7 +396,7 @@ export const buildProcessDefinitions = (
|
||||
...baseEnv,
|
||||
GAME_API_ROLE: 'server',
|
||||
PROFILE: profile.profile,
|
||||
SCENARIO: profile.scenario,
|
||||
SCENARIO: profile.currentScenario ?? 'default',
|
||||
GAME_PROFILE_NAME: profile.profileName,
|
||||
GAME_API_PORT: String(profile.apiPort),
|
||||
GAME_TRPC_PATH: `/${profile.profile}/api/trpc`,
|
||||
@@ -411,7 +411,7 @@ export const buildProcessDefinitions = (
|
||||
GAME_ENGINE_ROLE: 'turn-daemon',
|
||||
TURN_PROFILE: profile.profile,
|
||||
PROFILE: profile.profile,
|
||||
SCENARIO: profile.scenario,
|
||||
SCENARIO: profile.currentScenario ?? 'default',
|
||||
TURN_PROFILE_NAME: profile.profileName,
|
||||
};
|
||||
return {
|
||||
@@ -1422,8 +1422,8 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
} = parseInstallOptions(action);
|
||||
const tickOverride =
|
||||
installOptions?.turnTermMinutes !== undefined ? installOptions.turnTermMinutes * 60 : undefined;
|
||||
const scenarioId = installScenarioId ?? parseScenarioId(profile.scenario);
|
||||
if (!scenarioId) {
|
||||
const scenarioId = installScenarioId ?? parseScenarioId(profile.currentScenario);
|
||||
if (scenarioId === null) {
|
||||
return { status: 'FAILED', detail: 'scenarioId is missing' };
|
||||
}
|
||||
const profileDatabaseUrl = this.resolveProfileDatabaseUrl(profile);
|
||||
@@ -1547,7 +1547,7 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
const desiredStatus = shouldPreopen ? 'PREOPEN' : 'RUNNING';
|
||||
const publishedProfile = await updateClaimedProfile(
|
||||
{
|
||||
scenario: String(scenarioId),
|
||||
currentScenario: String(scenarioId),
|
||||
status: desiredStatus,
|
||||
buildStatus: 'SUCCEEDED',
|
||||
buildWorkspace: workspace.root,
|
||||
@@ -1564,8 +1564,8 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
completedAt,
|
||||
error: null,
|
||||
});
|
||||
if (String(scenarioId) !== profile.scenario) {
|
||||
await this.repository.updateScenario(profile.profileName, String(scenarioId));
|
||||
if (String(scenarioId) !== profile.currentScenario) {
|
||||
await this.repository.updateCurrentScenario(profile.profileName, String(scenarioId));
|
||||
}
|
||||
return this.repository.updateStatus(profile.profileName, desiredStatus, {
|
||||
preopenAt: preopenAt ? preopenAt.toISOString() : openAt ? openAt.toISOString() : null,
|
||||
@@ -1577,6 +1577,7 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
releasePrepared = true;
|
||||
const builtProfile = publishedProfile ?? {
|
||||
...profile,
|
||||
currentScenario: String(scenarioId),
|
||||
scenario: String(scenarioId),
|
||||
status: desiredStatus,
|
||||
buildWorkspace: workspace.root,
|
||||
@@ -1642,7 +1643,7 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
meta: Record<string, unknown>;
|
||||
}> {
|
||||
const databaseUrl = databaseUrlOverride ?? this.resolveProfileDatabaseUrl(profile);
|
||||
let scenarioId = overrides?.scenarioId ?? parseScenarioId(profile.scenario);
|
||||
let scenarioId = overrides?.scenarioId ?? parseScenarioId(profile.currentScenario);
|
||||
let tickSeconds: number | undefined = overrides?.tickSeconds;
|
||||
let meta: Record<string, unknown> = {};
|
||||
const connector = createGamePostgresConnector({ url: databaseUrl });
|
||||
|
||||
@@ -78,6 +78,9 @@ export interface GatewayOperationLogInput {
|
||||
export interface GatewayProfileRecord {
|
||||
profileName: string;
|
||||
profile: string;
|
||||
instanceKey: string;
|
||||
currentScenario: string | null;
|
||||
/** @deprecated Rollback-compatible mirror of currentScenario. */
|
||||
scenario: string;
|
||||
apiPort: number;
|
||||
status: GatewayProfileStatus;
|
||||
@@ -100,7 +103,10 @@ export interface GatewayProfileRecord {
|
||||
|
||||
export interface GatewayProfileUpsertInput {
|
||||
profile: string;
|
||||
scenario: string;
|
||||
instanceKey?: string;
|
||||
currentScenario?: string | null;
|
||||
/** @deprecated Accepted while older bootstrap clients are still supported. */
|
||||
scenario?: string;
|
||||
apiPort: number;
|
||||
status?: GatewayProfileStatus;
|
||||
preopenAt?: string;
|
||||
@@ -111,7 +117,7 @@ export interface GatewayProfileUpsertInput {
|
||||
}
|
||||
|
||||
export interface GatewayClaimedProfileUpdate {
|
||||
scenario?: string;
|
||||
currentScenario?: string | null;
|
||||
status?: GatewayProfileStatus;
|
||||
buildStatus?: GatewayBuildStatus;
|
||||
buildCommitSha?: string | null;
|
||||
@@ -131,7 +137,7 @@ export interface GatewayProfileRepository {
|
||||
listProfiles(): Promise<GatewayProfileRecord[]>;
|
||||
getProfile(profileName: string): Promise<GatewayProfileRecord | null>;
|
||||
upsertProfile(input: GatewayProfileUpsertInput): Promise<GatewayProfileRecord>;
|
||||
updateScenario(profileName: string, scenario: string): Promise<GatewayProfileRecord | null>;
|
||||
updateCurrentScenario(profileName: string, scenario: string | null): Promise<GatewayProfileRecord | null>;
|
||||
updateStatus(
|
||||
profileName: string,
|
||||
status: GatewayProfileStatus,
|
||||
@@ -219,6 +225,8 @@ export const buildRetryOperationSource = (previous: {
|
||||
type GatewayProfileRow = {
|
||||
profileName: string;
|
||||
profile: string;
|
||||
instanceKey: string;
|
||||
currentScenario: string | null;
|
||||
scenario: string;
|
||||
apiPort: number;
|
||||
status: GatewayProfileStatus;
|
||||
@@ -265,6 +273,8 @@ type GatewayOperationRow = {
|
||||
const mapProfile = (row: GatewayProfileRow): GatewayProfileRecord => ({
|
||||
profileName: row.profileName,
|
||||
profile: row.profile,
|
||||
instanceKey: row.instanceKey,
|
||||
currentScenario: row.currentScenario,
|
||||
scenario: row.scenario,
|
||||
apiPort: row.apiPort,
|
||||
status: row.status,
|
||||
@@ -285,7 +295,24 @@ const mapProfile = (row: GatewayProfileRow): GatewayProfileRecord => ({
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
});
|
||||
|
||||
const buildProfileName = (profile: string, scenario: string): string => `${profile}:${scenario}`;
|
||||
export const buildGatewayProfileName = (profile: string, instanceKey: string): string => `${profile}:${instanceKey}`;
|
||||
|
||||
export const resolveGatewayProfileIdentity = (
|
||||
input: GatewayProfileUpsertInput
|
||||
): {
|
||||
instanceKey: string;
|
||||
currentScenario: string | null;
|
||||
shouldUpdateCurrentScenario: boolean;
|
||||
} => {
|
||||
const instanceKey = input.instanceKey ?? input.scenario ?? 'default';
|
||||
if (input.currentScenario !== undefined) {
|
||||
return { instanceKey, currentScenario: input.currentScenario, shouldUpdateCurrentScenario: true };
|
||||
}
|
||||
if (input.instanceKey === undefined && input.scenario !== undefined && input.scenario !== 'default') {
|
||||
return { instanceKey, currentScenario: input.scenario, shouldUpdateCurrentScenario: true };
|
||||
}
|
||||
return { instanceKey, currentScenario: null, shouldUpdateCurrentScenario: false };
|
||||
};
|
||||
|
||||
const mapOperation = (row: GatewayOperationRow): GatewayOperationRecord => ({
|
||||
id: row.id,
|
||||
@@ -331,7 +358,7 @@ const mapOperationLog = (row: {
|
||||
export const createGatewayProfileRepository = (prisma: GatewayPrismaClient): GatewayProfileRepository => ({
|
||||
async listProfiles(): Promise<GatewayProfileRecord[]> {
|
||||
const rows = await prisma.gatewayProfile.findMany({
|
||||
orderBy: [{ profile: 'asc' }, { scenario: 'asc' }],
|
||||
orderBy: [{ profile: 'asc' }, { instanceKey: 'asc' }],
|
||||
});
|
||||
return rows.map(mapProfile);
|
||||
},
|
||||
@@ -342,13 +369,16 @@ export const createGatewayProfileRepository = (prisma: GatewayPrismaClient): Gat
|
||||
return row ? mapProfile(row) : null;
|
||||
},
|
||||
async upsertProfile(input: GatewayProfileUpsertInput): Promise<GatewayProfileRecord> {
|
||||
const profileName = buildProfileName(input.profile, input.scenario);
|
||||
const { instanceKey, currentScenario, shouldUpdateCurrentScenario } = resolveGatewayProfileIdentity(input);
|
||||
const profileName = buildGatewayProfileName(input.profile, instanceKey);
|
||||
const row = await prisma.gatewayProfile.upsert({
|
||||
where: { profileName },
|
||||
create: {
|
||||
profileName,
|
||||
profile: input.profile,
|
||||
scenario: input.scenario,
|
||||
instanceKey,
|
||||
currentScenario,
|
||||
scenario: currentScenario ?? 'default',
|
||||
apiPort: input.apiPort,
|
||||
status: input.status ?? 'STOPPED',
|
||||
preopenAt: input.preopenAt ? new Date(input.preopenAt) : null,
|
||||
@@ -358,6 +388,8 @@ export const createGatewayProfileRepository = (prisma: GatewayPrismaClient): Gat
|
||||
meta: (input.meta ?? {}) as GatewayPrisma.JsonObject,
|
||||
},
|
||||
update: {
|
||||
currentScenario: shouldUpdateCurrentScenario ? currentScenario : undefined,
|
||||
scenario: shouldUpdateCurrentScenario ? (currentScenario ?? 'default') : undefined,
|
||||
apiPort: input.apiPort,
|
||||
status: input.status,
|
||||
preopenAt: input.preopenAt ? new Date(input.preopenAt) : input.preopenAt === null ? null : undefined,
|
||||
@@ -373,11 +405,12 @@ export const createGatewayProfileRepository = (prisma: GatewayPrismaClient): Gat
|
||||
});
|
||||
return mapProfile(row);
|
||||
},
|
||||
async updateScenario(profileName: string, scenario: string): Promise<GatewayProfileRecord | null> {
|
||||
async updateCurrentScenario(profileName: string, scenario: string | null): Promise<GatewayProfileRecord | null> {
|
||||
const row = await prisma.gatewayProfile.update({
|
||||
where: { profileName },
|
||||
data: {
|
||||
scenario,
|
||||
currentScenario: scenario,
|
||||
scenario: scenario ?? 'default',
|
||||
},
|
||||
});
|
||||
return row ? mapProfile(row) : null;
|
||||
@@ -700,7 +733,8 @@ export const createGatewayProfileRepository = (prisma: GatewayPrismaClient): Gat
|
||||
return tx.gatewayProfile.update({
|
||||
where: { profileName },
|
||||
data: {
|
||||
scenario: patch.scenario,
|
||||
currentScenario: patch.currentScenario,
|
||||
scenario: patch.currentScenario === undefined ? undefined : (patch.currentScenario ?? 'default'),
|
||||
status: patch.status,
|
||||
buildStatus: patch.buildStatus,
|
||||
buildCommitSha: patch.buildCommitSha,
|
||||
|
||||
@@ -3,8 +3,8 @@ export const GATEWAY_PROFILE_ORDER = ['che', 'kwe', 'pwe', 'twe', 'nya', 'pya',
|
||||
const gatewayProfileOrder = new Map<string, number>(GATEWAY_PROFILE_ORDER.map((profile, index) => [profile, index]));
|
||||
|
||||
export const compareGatewayProfiles = (
|
||||
left: { profile: string; scenario: string },
|
||||
right: { profile: string; scenario: string }
|
||||
left: { profile: string; instanceKey: string },
|
||||
right: { profile: string; instanceKey: string }
|
||||
): number => {
|
||||
const unknownRank = GATEWAY_PROFILE_ORDER.length;
|
||||
const profileOrder =
|
||||
@@ -14,8 +14,8 @@ export const compareGatewayProfiles = (
|
||||
|
||||
const profileNameOrder = left.profile.localeCompare(right.profile);
|
||||
if (profileNameOrder !== 0) return profileNameOrder;
|
||||
return left.scenario.localeCompare(right.scenario);
|
||||
return left.instanceKey.localeCompare(right.instanceKey);
|
||||
};
|
||||
|
||||
export const orderGatewayProfiles = <T extends { profile: string; scenario: string }>(profiles: readonly T[]): T[] =>
|
||||
export const orderGatewayProfiles = <T extends { profile: string; instanceKey: string }>(profiles: readonly T[]): T[] =>
|
||||
[...profiles].sort(compareGatewayProfiles);
|
||||
|
||||
Reference in New Issue
Block a user