From 7679e8f5b71ea9169a356f0e5a9c5d8b3f74d247 Mon Sep 17 00:00:00 2001 From: hided62 Date: Sun, 26 Jul 2026 05:37:40 +0000 Subject: [PATCH] Batch rank persistence for large scenario startup --- app/game-engine/src/turn/databaseHooks.ts | 43 +++++++++++-------- .../e2e/general-icon-lifecycle.spec.ts | 8 +++- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/app/game-engine/src/turn/databaseHooks.ts b/app/game-engine/src/turn/databaseHooks.ts index 996b78e..3351648 100644 --- a/app/game-engine/src/turn/databaseHooks.ts +++ b/app/game-engine/src/turn/databaseHooks.ts @@ -1,6 +1,6 @@ import { createGamePostgresConnector, - type GamePrisma, + GamePrisma, type InputJsonValue, type TurnEngineCityUpdateInput, type TurnEngineDiplomacyCreateManyInput, @@ -379,6 +379,29 @@ const buildInitialRankRows = ( ): Array<{ generalId: number; nationId: number; type: string; value: number }> => buildRankRows(general).map((row) => ({ ...row, nationId: 0, value: 0 })); +const RANK_DATA_UPSERT_BATCH_SIZE = 1_000; + +const upsertRankRows = async ( + prisma: GamePrisma.TransactionClient, + rows: Array<{ generalId: number; nationId: number; type: string; value: number }> +): Promise => { + for (let offset = 0; offset < rows.length; offset += RANK_DATA_UPSERT_BATCH_SIZE) { + const batch = rows.slice(offset, offset + RANK_DATA_UPSERT_BATCH_SIZE); + await prisma.$executeRaw(GamePrisma.sql` + INSERT INTO "rank_data" ("general_id", "nation_id", "type", "value") + VALUES ${GamePrisma.join( + batch.map( + (row) => GamePrisma.sql`(${row.generalId}, ${row.nationId}, ${row.type}, ${row.value})` + ) + )} + ON CONFLICT ("general_id", "type") DO UPDATE + SET + "nation_id" = EXCLUDED."nation_id", + "value" = EXCLUDED."value" + `); + } +}; + const buildGeneralUpdate = ( general: ReturnType['generals'][number] ): TurnEngineGeneralUpdateInput => ({ @@ -955,23 +978,7 @@ export const createDatabaseTurnHooks = async ( ...createdGenerals.flatMap(buildInitialRankRows), ...rankTargets.flatMap(buildRankRows), ]; - await Promise.all( - rankRows.map((row) => - prisma.rankData.upsert({ - where: { - generalId_type: { - generalId: row.generalId, - type: row.type, - }, - }, - update: { - nationId: row.nationId, - value: row.value, - }, - create: row, - }) - ) - ); + await upsertRankRows(prisma, rankRows); } if (logs.length > 0) { diff --git a/app/gateway-frontend/e2e/general-icon-lifecycle.spec.ts b/app/gateway-frontend/e2e/general-icon-lifecycle.spec.ts index 8549eae..5d92ade 100644 --- a/app/gateway-frontend/e2e/general-icon-lifecycle.spec.ts +++ b/app/gateway-frontend/e2e/general-icon-lifecycle.spec.ts @@ -46,9 +46,15 @@ const resetScenario = async (page: Page, scenarioId: string, sourceCommit: strin await expect(latestOperation.locator('td').nth(3)).toHaveText('SUCCEEDED', { timeout: 300_000, }); - await expect(page.getByTestId('selected-profile-status').locator('.text-emerald-400')).toHaveCount(2, { + const profileStatus = page.getByTestId('selected-profile-status'); + await expect(profileStatus.locator(':scope > div').nth(0)).toContainText('RUNNING', { timeout: 30_000, }); + await expect(profileStatus.locator(':scope > div').nth(1)).toContainText('SUCCEEDED'); + await expect(profileStatus.locator('.text-emerald-400')).toHaveCount(3, { + timeout: 30_000, + }); + await expect(profileStatus.locator('..').locator('.text-red-400')).toHaveCount(0); }; const createGeneralAndInspectIcons = async (