fix(gateway): 릴리스 이력 시각을 instant로 저장

운영 DB 세션 시간대와 무관하게 Gateway 릴리스 작업 및 로그 시각을 저장하고, 기존 raw 값은 현재 UTC 해석으로 보존한다. Gateway Prisma 세션을 UTC로 고정하고 KST 직접 입력 회귀 테스트 및 manifest head를 갱신한다.
This commit is contained in:
2026-08-21 17:32:47 +00:00
parent 24afa7f466
commit e3f28b3c48
8 changed files with 114 additions and 18 deletions
@@ -0,0 +1,34 @@
-- Gateway release history is an absolute control-plane timeline. Preserve every
-- existing raw value as its current UTC interpretation (including historical bad
-- rows), then make future CURRENT_TIMESTAMP writes independent of session timezone.
ALTER TABLE "gateway_release_operation"
ALTER COLUMN "created_at" DROP DEFAULT;
ALTER TABLE "gateway_release_log"
ALTER COLUMN "created_at" DROP DEFAULT;
ALTER TABLE "gateway_release_state"
ALTER COLUMN "last_successful_at" TYPE TIMESTAMPTZ(3)
USING "last_successful_at" AT TIME ZONE 'UTC',
ALTER COLUMN "updated_at" TYPE TIMESTAMPTZ(3)
USING "updated_at" AT TIME ZONE 'UTC';
ALTER TABLE "gateway_release_operation"
ALTER COLUMN "started_at" TYPE TIMESTAMPTZ(3)
USING "started_at" AT TIME ZONE 'UTC',
ALTER COLUMN "completed_at" TYPE TIMESTAMPTZ(3)
USING "completed_at" AT TIME ZONE 'UTC',
ALTER COLUMN "created_at" TYPE TIMESTAMPTZ(3)
USING "created_at" AT TIME ZONE 'UTC',
ALTER COLUMN "updated_at" TYPE TIMESTAMPTZ(3)
USING "updated_at" AT TIME ZONE 'UTC';
ALTER TABLE "gateway_release_log"
ALTER COLUMN "created_at" TYPE TIMESTAMPTZ(3)
USING "created_at" AT TIME ZONE 'UTC';
ALTER TABLE "gateway_release_operation"
ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;
ALTER TABLE "gateway_release_log"
ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP;
+9 -9
View File
@@ -315,9 +315,9 @@ model GatewayReleaseState {
activeWorkspace String? @map("active_workspace")
previousCommitSha String? @map("previous_commit_sha")
previousWorkspace String? @map("previous_workspace")
lastSuccessfulAt DateTime? @map("last_successful_at")
lastSuccessfulAt DateTime? @map("last_successful_at") @db.Timestamptz(3)
lastError String? @map("last_error")
updatedAt DateTime @updatedAt @map("updated_at")
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
@@map("gateway_release_state")
}
@@ -334,15 +334,15 @@ model GatewayReleaseOperation {
payload Json @default(dbgenerated("'{}'::jsonb"))
reason String?
requestedBy String @map("requested_by")
startedAt DateTime? @map("started_at")
completedAt DateTime? @map("completed_at")
startedAt DateTime? @map("started_at") @db.Timestamptz(3)
completedAt DateTime? @map("completed_at") @db.Timestamptz(3)
error String?
leaseOwner String? @map("lease_owner")
leaseUntil DateTime? @map("lease_until")
heartbeatAt DateTime? @map("heartbeat_at")
leaseUntil DateTime? @map("lease_until") @db.Timestamptz(6)
heartbeatAt DateTime? @map("heartbeat_at") @db.Timestamptz(6)
attempts Int @default(0)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
logs GatewayReleaseLog[]
@@index([status, leaseUntil, createdAt])
@@ -356,7 +356,7 @@ model GatewayReleaseLog {
level String
phase String
message String @db.Text
createdAt DateTime @default(now()) @map("created_at")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
operation GatewayReleaseOperation @relation(fields: [operationId], references: [id], onDelete: Cascade)
@@index([operationId, id])
+1 -1
View File
@@ -6,4 +6,4 @@ import type { PostgresConfig, PostgresConnector } from './postgres.js';
import { createPostgresConnector } from './postgres.js';
export const createGatewayPostgresConnector = (config: PostgresConfig): PostgresConnector<GatewayPrismaClient> =>
createPostgresConnector(config, (options) => new GatewayPrismaClient(options));
createPostgresConnector({ ...config, sessionTimezone: 'UTC' }, (options) => new GatewayPrismaClient(options));
+18 -6
View File
@@ -14,6 +14,7 @@ export interface PostgresConfig {
url: string;
log?: PostgresLogOption[];
maxConnections?: number;
sessionTimezone?: 'UTC';
}
export interface PostgresPoolStats {
@@ -60,21 +61,32 @@ interface SharedPoolEntry {
const sharedPools = new Map<string, SharedPoolEntry>();
const buildSharedPoolKey = (url: string, schema: string | undefined, maxConnections: number): string =>
JSON.stringify([url, schema ?? '', maxConnections]);
const buildSharedPoolKey = (
url: string,
schema: string | undefined,
maxConnections: number,
sessionTimezone: 'UTC' | undefined
): string => JSON.stringify([url, schema ?? '', maxConnections, sessionTimezone ?? '']);
const acquireSharedPool = (
url: string,
schema: string | undefined,
maxConnections: number
maxConnections: number,
sessionTimezone: 'UTC' | undefined
): { entry: SharedPoolEntry; release: () => Promise<void> } => {
const key = buildSharedPoolKey(url, schema, maxConnections);
const key = buildSharedPoolKey(url, schema, maxConnections, sessionTimezone);
let entry = sharedPools.get(key);
if (!entry) {
const connectionOptions = [
schema ? `-c search_path=${schema}` : undefined,
sessionTimezone ? `-c timezone=${sessionTimezone}` : undefined,
]
.filter((option): option is string => option !== undefined)
.join(' ');
const pool = new pg.Pool({
connectionString: url,
max: maxConnections,
...(schema ? { options: `-c search_path=${schema}` } : {}),
...(connectionOptions ? { options: connectionOptions } : {}),
});
entry = { pool, references: 0, maxConnections };
sharedPools.set(key, entry);
@@ -161,7 +173,7 @@ export const createPostgresConnector = <TClient>(
const schema =
extractSchemaFromDatabaseUrl(config.url) ?? process.env.POSTGRES_SCHEMA ?? process.env.DATABASE_SCHEMA;
const maxConnections = resolvePostgresPoolMax(config.maxConnections ?? process.env.POSTGRES_POOL_MAX);
const sharedPool = acquireSharedPool(config.url, schema, maxConnections);
const sharedPool = acquireSharedPool(config.url, schema, maxConnections, config.sessionTimezone);
const adapter = new PrismaPg(sharedPool.entry.pool, schema ? { schema } : undefined);
const prisma = createClient({
adapter,