perf: 접속 점수를 Redis 배치로 지연 반영

대시보드 벌점을 read-model source와 outbox에서 제외하고 수동 projection의 가점을 Redis에 합산한다. 5초 worker가 월별 traffic과 접속 점수를 멱등 PostgreSQL batch로 반영하며 메인 제한 gate는 Redis TTL marker만 조회한다.
This commit is contained in:
2026-08-17 14:50:34 +00:00
parent fb6add83a5
commit 5bc1e3473f
17 changed files with 1022 additions and 83 deletions
@@ -12,7 +12,7 @@ import type { DatabaseClient } from '../context.js';
export const DASHBOARD_SOURCE_REVISION_COVERAGE_VERSION = 1;
const SOURCE_REVISION_LENGTH = 22;
const SOURCE_REVISION_CODE_VERSION = 'dashboard-private-slices-v2';
const SOURCE_REVISION_CODE_VERSION = 'dashboard-private-slices-v3';
export type DashboardSourceSlice = 'context' | 'commandTable' | 'boardAccess';
@@ -36,7 +36,6 @@ interface DashboardSourceRevisionRow {
cityRevision: bigint;
nationRevision: bigint;
worldRevision: bigint;
accessRevision: bigint;
}
type RevisionTuple = readonly [domain: string, entityId: number, revision: string];
@@ -46,7 +45,6 @@ type DashboardRevisionVector = {
city: string;
nation: string;
world: string;
access: string;
};
export interface DashboardAuthSource {
@@ -124,11 +122,10 @@ const buildSourceRevisions = (
const city = ['city.content', identity.cityId, identity.cityId > 0 ? revisions.city : '0'] as const;
const nation = ['nation.content', identity.nationId, identity.nationId > 0 ? revisions.nation : '0'] as const;
const world = ['world.content', 0, revisions.world] as const;
const access = ['access.general', identity.generalId, revisions.access] as const;
const auth = ['auth.context', 0, authSourceRevision] as const;
return {
context: digestSourceRevision('context', [global, general, city, nation, world, access, auth]),
context: digestSourceRevision('context', [global, general, city, nation, world, auth]),
commandTable: digestSourceRevision('commandTable', [global, general, city, nation, world]),
boardAccess: digestSourceRevision('boardAccess', [general, nation]),
};
@@ -139,15 +136,11 @@ const buildSourceRevisions = (
* dependency heads in one indexed statement. Missing revision rows are revision 0.
* A missing actor/meta row, query failure, or malformed result disables the optimization.
*/
export const readDashboardSourceRevisionState = async (
const readDashboardSourceRevisionStateByActor = async (
db: Pick<DatabaseClient, '$queryRaw'>,
generalId: number,
actorCondition: GamePrisma.Sql,
authSource?: DashboardAuthSource
): Promise<DashboardSourceRevisionState | null> => {
if (!Number.isSafeInteger(generalId) || generalId <= 0) {
return null;
}
let rows: DashboardSourceRevisionRow[];
try {
rows = await db.$queryRaw<DashboardSourceRevisionRow[]>(GamePrisma.sql`
@@ -160,8 +153,7 @@ export const readDashboardSourceRevisionState = async (
COALESCE(general_revision."revision", 0) AS "generalRevision",
COALESCE(city_revision."revision", 0) AS "cityRevision",
COALESCE(nation_revision."revision", 0) AS "nationRevision",
COALESCE(world_revision."revision", 0) AS "worldRevision",
COALESCE(access_revision."revision", 0) AS "accessRevision"
COALESCE(world_revision."revision", 0) AS "worldRevision"
FROM "general" AS actor
CROSS JOIN "read_model_revision_meta" AS meta
LEFT JOIN "read_model_revision" AS global_revision
@@ -179,10 +171,7 @@ export const readDashboardSourceRevisionState = async (
LEFT JOIN "read_model_revision" AS world_revision
ON world_revision."domain" = 'world.content'
AND world_revision."entity_id" = 0
LEFT JOIN "read_model_revision" AS access_revision
ON access_revision."domain" = 'access.general'
AND access_revision."entity_id" = actor."id"
WHERE actor."id" = ${generalId}
WHERE ${actorCondition}
AND meta."id" = 1
`);
} catch {
@@ -206,10 +195,9 @@ export const readDashboardSourceRevisionState = async (
city: parseNonNegativeRevision(row.cityRevision),
nation: parseNonNegativeRevision(row.nationRevision),
world: parseNonNegativeRevision(row.worldRevision),
access: parseNonNegativeRevision(row.accessRevision),
};
if (
identity.generalId !== generalId ||
identity.generalId === null ||
identity.cityId === null ||
identity.nationId === null ||
coverageVersion === null ||
@@ -219,7 +207,7 @@ export const readDashboardSourceRevisionState = async (
}
const validIdentity = {
generalId,
generalId: identity.generalId,
cityId: identity.cityId,
nationId: identity.nationId,
};
@@ -234,6 +222,28 @@ export const readDashboardSourceRevisionState = async (
};
};
export const readDashboardSourceRevisionState = async (
db: Pick<DatabaseClient, '$queryRaw'>,
generalId: number,
authSource?: DashboardAuthSource
): Promise<DashboardSourceRevisionState | null> => {
if (!Number.isSafeInteger(generalId) || generalId <= 0) {
return null;
}
return readDashboardSourceRevisionStateByActor(db, GamePrisma.sql`actor."id" = ${generalId}`, authSource);
};
export const readDashboardSourceRevisionStateForUser = async (
db: Pick<DatabaseClient, '$queryRaw'>,
userId: string,
authSource?: DashboardAuthSource
): Promise<DashboardSourceRevisionState | null> => {
if (!userId) {
return null;
}
return readDashboardSourceRevisionStateByActor(db, GamePrisma.sql`actor."user_id" = ${userId}`, authSource);
};
export const canUseDashboardSourceRevision = (options: {
state: DashboardSourceRevisionState | null;
slice: DashboardSourceSlice;