import type { RunAttemptRecord, RunAttemptStatus, RunEventRecord, RunRecord, } from '../../domain/run'; import { EXECUTION_ORIGINS, RUN_ATTEMPT_STATUSES, RUN_CANCELLATION_REASONS, RUN_EVENT_ACTOR_TYPES, RUN_STATUSES, } from '../../domain/run'; import { assertRunRetryPolicyRecord, RUN_RETRY_SAFETIES, type RunRetryPolicyRecord, } from '../../domain/runRetryPolicy'; import { DuplicateIdempotencyKeyError, DuplicateRunAttemptError, DuplicateRunEventError, RunEventPayloadTooLargeError, RunRepositoryBusyError, RunRepositoryConstraintError, RunRepositoryError, RunRepositoryOperationError, } from '../../domain/repositoryErrors'; import type { RunRepository, RunRepositoryReader, RunRepositoryTransaction, } from '../../ports/runRepository'; import type { PostgresClient as PostgresRunClient, PostgresPool as PostgresRunPool, PostgresQueryable as PostgresRunQueryable, PostgresQueryResult as PostgresRunQueryResult, } from '@qinglong/runtime-core'; export type { PostgresClient as PostgresRunClient, PostgresPool as PostgresRunPool, PostgresQueryable as PostgresRunQueryable, PostgresQueryResult as PostgresRunQueryResult, } from '@qinglong/runtime-core'; import { MAX_CANCELLATION_RECOVERY_PAGE_SIZE, MAX_RUN_EVENT_PAGE_SIZE, MAX_RUN_EVENT_PAYLOAD_BYTES, } from '../../ports/runRepository'; interface ColumnDefinition { readonly column: string; readonly property: string; } type QueryRow = Record; const POSTGRES_RUNTIME_STATEMENT_TIMEOUT_MS = 5_000; const POSTGRES_RUNTIME_LOCK_TIMEOUT_MS = 1_000; const POSTGRES_RUNTIME_IDLE_TRANSACTION_TIMEOUT_MS = 10_000; const RUN_COLUMNS: readonly ColumnDefinition[] = Object.freeze([ { column: 'id', property: 'id' }, { column: 'project_id', property: 'projectId' }, { column: 'task_id', property: 'taskId' }, { column: 'task_revision', property: 'taskRevision' }, { column: 'task_name', property: 'taskName' }, { column: 'task_snapshot_ref', property: 'taskSnapshotRef' }, { column: 'legacy_cron_id', property: 'legacyCronId' }, { column: 'parent_run_id', property: 'parentRunId' }, { column: 'retry_of_run_id', property: 'retryOfRunId' }, { column: 'trigger_id', property: 'triggerId' }, { column: 'trigger_type', property: 'triggerType' }, { column: 'execution_origin', property: 'executionOrigin' }, { column: 'execution_owner', property: 'executionOwner' }, { column: 'triggered_by', property: 'triggeredBy' }, { column: 'request_id', property: 'requestId' }, { column: 'scheduled_for_ms', property: 'scheduledForMs' }, { column: 'status', property: 'status' }, { column: 'version', property: 'version' }, { column: 'event_sequence', property: 'eventSequence' }, { column: 'priority', property: 'priority' }, { column: 'idempotency_key', property: 'idempotencyKey' }, { column: 'input_ref', property: 'inputRef' }, { column: 'output_ref', property: 'outputRef' }, { column: 'created_at_ms', property: 'createdAtMs' }, { column: 'queued_at_ms', property: 'queuedAtMs' }, { column: 'started_at_ms', property: 'startedAtMs' }, { column: 'finished_at_ms', property: 'finishedAtMs' }, { column: 'cancel_requested_at_ms', property: 'cancelRequestedAtMs' }, { column: 'cancel_reason', property: 'cancelReason' }, { column: 'error_code', property: 'errorCode' }, { column: 'error_summary', property: 'errorSummary' }, ]); const ATTEMPT_COLUMNS: readonly ColumnDefinition[] = Object.freeze([ { column: 'id', property: 'id' }, { column: 'run_id', property: 'runId' }, { column: 'step_run_id', property: 'stepRunId' }, { column: 'attempt', property: 'attempt' }, { column: 'status', property: 'status' }, { column: 'executor_type', property: 'executorType' }, { column: 'worker_id', property: 'workerId' }, { column: 'executor_handle', property: 'executorHandle' }, { column: 'pid', property: 'pid' }, { column: 'log_artifact_id', property: 'logArtifactId' }, { column: 'lease_token', property: 'leaseToken' }, { column: 'lease_expires_at_ms', property: 'leaseExpiresAtMs' }, { column: 'deadline_at_ms', property: 'deadlineAtMs' }, { column: 'callback_token_hash', property: 'callbackTokenHash' }, { column: 'callback_sequence', property: 'callbackSequence' }, { column: 'created_at_ms', property: 'createdAtMs' }, { column: 'started_at_ms', property: 'startedAtMs' }, { column: 'finished_at_ms', property: 'finishedAtMs' }, { column: 'exit_code', property: 'exitCode' }, { column: 'error_code', property: 'errorCode' }, { column: 'error_summary', property: 'errorSummary' }, ]); const EVENT_COLUMNS: readonly ColumnDefinition[] = Object.freeze([ { column: 'id', property: 'id' }, { column: 'run_id', property: 'runId' }, { column: 'sequence', property: 'sequence' }, { column: 'type', property: 'type' }, { column: 'dedupe_key', property: 'dedupeKey' }, { column: 'actor_type', property: 'actorType' }, { column: 'actor_id', property: 'actorId' }, { column: 'attempt_id', property: 'attemptId' }, { column: 'step_run_id', property: 'stepRunId' }, { column: 'payload', property: 'payload' }, { column: 'created_at_ms', property: 'createdAtMs' }, ]); const RETRY_POLICY_COLUMNS: readonly ColumnDefinition[] = Object.freeze([ { column: 'run_id', property: 'runId' }, { column: 'max_attempts', property: 'maxAttempts' }, { column: 'retry_on_lost', property: 'retryOnLost' }, { column: 'safety', property: 'safety' }, { column: 'backoff_base_ms', property: 'backoffBaseMs' }, { column: 'backoff_max_ms', property: 'backoffMaxMs' }, { column: 'next_attempt_at_ms', property: 'nextAttemptAtMs' }, { column: 'version', property: 'version' }, { column: 'created_at_ms', property: 'createdAtMs' }, { column: 'updated_at_ms', property: 'updatedAtMs' }, ]); const TERMINAL_RUN_STATUSES = Object.freeze([ 'succeeded', 'failed', 'cancelled', 'timed_out', ]); const BUSY_SQL_STATES = new Set([ '08000', '08001', '08003', '08004', '08006', '08007', '08P01', '40001', '40P01', '55P03', '57014', '57P01', '57P02', '57P03', ]); const RUN_IDEMPOTENCY_CONSTRAINT = 'ql3_runs_project_idempotency_uidx'; const ATTEMPT_NUMBER_CONSTRAINT = 'ql3_run_attempts_run_attempt_uidx'; const EVENT_SEQUENCE_CONSTRAINT = 'ql3_run_events_run_sequence_uidx'; const EVENT_DEDUPE_CONSTRAINT = 'ql3_run_events_run_dedupe_uidx'; function quoted(identifier: string): string { return `"${identifier}"`; } function selectColumns(columns: readonly ColumnDefinition[]): string { return columns .map(({ column, property }) => `${quoted(column)} AS ${quoted(property)}`) .join(', '); } function insertSql( tableName: string, columns: readonly ColumnDefinition[], ): string { return `INSERT INTO "ql3".${quoted(tableName)} (${columns .map(({ column }) => quoted(column)) .join(', ')}) VALUES (${columns .map((_, index) => `$${index + 1}`) .join(', ')})`; } function updateSql( tableName: string, columns: readonly ColumnDefinition[], predicate: string, ): string { const mutableColumns = columns.slice(1); return `UPDATE "ql3".${quoted(tableName)} SET ${mutableColumns .map(({ column }, index) => `${quoted(column)} = $${index + 2}`) .join(', ')} WHERE ${predicate} RETURNING ${quoted(columns[0].column)}`; } function writeValues( record: object, columns: readonly ColumnDefinition[], ): unknown[] { const values = record as Record; return columns.map(({ property }) => values[property] ?? null); } function requiredString(row: QueryRow, property: string): string { const value = row[property]; if (typeof value !== 'string' || value.length === 0) { throw new RunRepositoryConstraintError( `PostgreSQL Run row has an invalid ${property}`, ); } return value; } function optionalString(row: QueryRow, property: string): string | undefined { const value = row[property]; if (value === null || value === undefined) return undefined; if (typeof value !== 'string') { throw new RunRepositoryConstraintError( `PostgreSQL Run row has an invalid ${property}`, ); } return value; } function requiredInteger(row: QueryRow, property: string): number { const value = row[property]; if (typeof value === 'number' && Number.isSafeInteger(value)) return value; if (typeof value === 'string' && /^-?(0|[1-9]\d*)$/.test(value)) { const parsed = Number(value); if (Number.isSafeInteger(parsed)) return parsed; } throw new RunRepositoryConstraintError( `PostgreSQL Run row has an invalid ${property}`, ); } function optionalInteger(row: QueryRow, property: string): number | undefined { if (row[property] === null || row[property] === undefined) return undefined; return requiredInteger(row, property); } function requiredBoolean(row: QueryRow, property: string): boolean { const value = row[property]; if (typeof value !== 'boolean') { throw new RunRepositoryConstraintError( `PostgreSQL Run row has an invalid ${property}`, ); } return value; } function requiredEnum( row: QueryRow, property: string, allowed: readonly T[], ): T { const value = requiredString(row, property); if (!allowed.includes(value as T)) { throw new RunRepositoryConstraintError( `PostgreSQL Run row has an unsupported ${property}`, ); } return value as T; } function assignOptional( record: T, key: K, value: T[K] | undefined, ): void { if (value !== undefined) record[key] = value; } function rowToRun(row: QueryRow): RunRecord { const run: RunRecord = { id: requiredString(row, 'id'), projectId: requiredString(row, 'projectId'), taskId: requiredString(row, 'taskId'), taskRevision: requiredString(row, 'taskRevision'), triggerType: requiredString(row, 'triggerType'), executionOrigin: requiredEnum(row, 'executionOrigin', EXECUTION_ORIGINS), executionOwner: requiredEnum(row, 'executionOwner', [ 'legacy', 'runtime', ] as const), status: requiredEnum(row, 'status', RUN_STATUSES), version: requiredInteger(row, 'version'), eventSequence: requiredInteger(row, 'eventSequence'), priority: requiredInteger(row, 'priority'), createdAtMs: requiredInteger(row, 'createdAtMs'), }; assignOptional(run, 'taskName', optionalString(row, 'taskName')); assignOptional( run, 'taskSnapshotRef', optionalString(row, 'taskSnapshotRef'), ); assignOptional(run, 'legacyCronId', optionalInteger(row, 'legacyCronId')); assignOptional(run, 'parentRunId', optionalString(row, 'parentRunId')); assignOptional(run, 'retryOfRunId', optionalString(row, 'retryOfRunId')); assignOptional(run, 'triggerId', optionalString(row, 'triggerId')); assignOptional(run, 'triggeredBy', optionalString(row, 'triggeredBy')); assignOptional(run, 'requestId', optionalString(row, 'requestId')); assignOptional(run, 'scheduledForMs', optionalInteger(row, 'scheduledForMs')); assignOptional(run, 'idempotencyKey', optionalString(row, 'idempotencyKey')); assignOptional(run, 'inputRef', optionalString(row, 'inputRef')); assignOptional(run, 'outputRef', optionalString(row, 'outputRef')); assignOptional(run, 'queuedAtMs', optionalInteger(row, 'queuedAtMs')); assignOptional(run, 'startedAtMs', optionalInteger(row, 'startedAtMs')); assignOptional(run, 'finishedAtMs', optionalInteger(row, 'finishedAtMs')); assignOptional( run, 'cancelRequestedAtMs', optionalInteger(row, 'cancelRequestedAtMs'), ); if (row.cancelReason !== null && row.cancelReason !== undefined) { run.cancelReason = requiredEnum( row, 'cancelReason', RUN_CANCELLATION_REASONS, ); } assignOptional(run, 'errorCode', optionalString(row, 'errorCode')); assignOptional(run, 'errorSummary', optionalString(row, 'errorSummary')); return run; } function rowToAttempt(row: QueryRow): RunAttemptRecord { const attempt: RunAttemptRecord = { id: requiredString(row, 'id'), runId: requiredString(row, 'runId'), attempt: requiredInteger(row, 'attempt'), status: requiredEnum(row, 'status', RUN_ATTEMPT_STATUSES), executorType: requiredString(row, 'executorType'), callbackSequence: requiredInteger(row, 'callbackSequence'), createdAtMs: requiredInteger(row, 'createdAtMs'), }; assignOptional(attempt, 'stepRunId', optionalString(row, 'stepRunId')); assignOptional(attempt, 'workerId', optionalString(row, 'workerId')); assignOptional( attempt, 'executorHandle', optionalString(row, 'executorHandle'), ); assignOptional(attempt, 'pid', optionalInteger(row, 'pid')); assignOptional( attempt, 'logArtifactId', optionalString(row, 'logArtifactId'), ); assignOptional(attempt, 'leaseToken', optionalString(row, 'leaseToken')); assignOptional( attempt, 'leaseExpiresAtMs', optionalInteger(row, 'leaseExpiresAtMs'), ); assignOptional(attempt, 'deadlineAtMs', optionalInteger(row, 'deadlineAtMs')); assignOptional( attempt, 'callbackTokenHash', optionalString(row, 'callbackTokenHash'), ); assignOptional(attempt, 'startedAtMs', optionalInteger(row, 'startedAtMs')); assignOptional(attempt, 'finishedAtMs', optionalInteger(row, 'finishedAtMs')); assignOptional(attempt, 'exitCode', optionalInteger(row, 'exitCode')); assignOptional(attempt, 'errorCode', optionalString(row, 'errorCode')); assignOptional(attempt, 'errorSummary', optionalString(row, 'errorSummary')); return attempt; } function normalizePayload(payload: unknown): Readonly> { let value = payload; if (typeof value === 'string') { try { value = JSON.parse(value); } catch (error) { throw new RunRepositoryConstraintError( 'PostgreSQL RunEvent payload is invalid JSON', error, ); } } if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new RunRepositoryConstraintError( 'PostgreSQL RunEvent payload is not a JSON object', ); } return value as Readonly>; } function rowToEvent(row: QueryRow): RunEventRecord { const event: RunEventRecord = { id: requiredString(row, 'id'), runId: requiredString(row, 'runId'), sequence: requiredInteger(row, 'sequence'), type: requiredString(row, 'type'), actorType: requiredEnum(row, 'actorType', RUN_EVENT_ACTOR_TYPES), payload: normalizePayload(row.payload), createdAtMs: requiredInteger(row, 'createdAtMs'), }; assignOptional(event, 'dedupeKey', optionalString(row, 'dedupeKey')); assignOptional(event, 'actorId', optionalString(row, 'actorId')); assignOptional(event, 'attemptId', optionalString(row, 'attemptId')); assignOptional(event, 'stepRunId', optionalString(row, 'stepRunId')); return event; } function rowToRetryPolicy(row: QueryRow): RunRetryPolicyRecord { const policy: RunRetryPolicyRecord = { runId: requiredString(row, 'runId'), maxAttempts: requiredInteger(row, 'maxAttempts'), retryOnLost: requiredBoolean(row, 'retryOnLost'), safety: requiredEnum(row, 'safety', RUN_RETRY_SAFETIES), backoffBaseMs: requiredInteger(row, 'backoffBaseMs'), backoffMaxMs: requiredInteger(row, 'backoffMaxMs'), version: requiredInteger(row, 'version'), createdAtMs: requiredInteger(row, 'createdAtMs'), updatedAtMs: requiredInteger(row, 'updatedAtMs'), }; assignOptional( policy, 'nextAttemptAtMs', optionalInteger(row, 'nextAttemptAtMs'), ); assertRunRetryPolicyRecord(policy); return policy; } function sqlState(error: unknown): string | undefined { if (!error || typeof error !== 'object') return undefined; const value = (error as { code?: unknown }).code; return typeof value === 'string' ? value : undefined; } function constraintName(error: unknown): string | undefined { if (!error || typeof error !== 'object') return undefined; const value = (error as { constraint?: unknown }).constraint; return typeof value === 'string' ? value : undefined; } function mapPostgresError(error: unknown): RunRepositoryError { if (error instanceof RunRepositoryError) return error; const state = sqlState(error); if (state && BUSY_SQL_STATES.has(state)) { return new RunRepositoryBusyError(error); } if (state?.startsWith('23')) { return new RunRepositoryConstraintError( 'PostgreSQL Run repository constraint violation', error, ); } return new RunRepositoryOperationError(error); } function affectedOneOrNone(result: PostgresRunQueryResult): boolean { const count = result.rowCount ?? result.rows.length; if (count === 0) return false; if (count === 1) return true; throw new RunRepositoryConstraintError( 'PostgreSQL compare-and-set affected more than one row', ); } function assertEventPayloadSize(event: RunEventRecord): void { let serialized: string; try { serialized = JSON.stringify(event.payload); } catch (error) { throw new RunRepositoryConstraintError( 'RunEvent payload is not JSON serializable', error, ); } const bytes = Buffer.byteLength(serialized, 'utf8'); if (bytes > MAX_RUN_EVENT_PAYLOAD_BYTES) { throw new RunEventPayloadTooLargeError(bytes, MAX_RUN_EVENT_PAYLOAD_BYTES); } } async function queryMapped( queryable: PostgresRunQueryable, text: string, values?: readonly unknown[], ): Promise> { try { return await queryable.query(text, values); } catch (error) { throw mapPostgresError(error); } } function singleRow( result: PostgresRunQueryResult, ): TRow | null { if (result.rows.length === 0) return null; if (result.rows.length !== 1) { throw new RunRepositoryConstraintError( 'PostgreSQL Run repository returned duplicate identity rows', ); } return result.rows[0]; } const RUN_SELECT = selectColumns(RUN_COLUMNS); const ATTEMPT_SELECT = selectColumns(ATTEMPT_COLUMNS); const EVENT_SELECT = selectColumns(EVENT_COLUMNS); const RETRY_POLICY_SELECT = selectColumns(RETRY_POLICY_COLUMNS); const INSERT_RUN_SQL = insertSql('runs', RUN_COLUMNS); const INSERT_ATTEMPT_SQL = insertSql('run_attempts', ATTEMPT_COLUMNS); const INSERT_EVENT_SQL = insertSql('run_events', EVENT_COLUMNS); const INSERT_RETRY_POLICY_SQL = insertSql( 'run_retry_policies', RETRY_POLICY_COLUMNS, ); const UPDATE_RUN_SQL = updateSql( 'runs', RUN_COLUMNS, `"id" = $1 AND "version" = $${RUN_COLUMNS.length + 1}`, ); const UPDATE_ATTEMPT_SQL = updateSql( 'run_attempts', ATTEMPT_COLUMNS, `"id" = $1 AND "status" = $${ ATTEMPT_COLUMNS.length + 1 } AND "callback_sequence" = $${ATTEMPT_COLUMNS.length + 2}`, ); const UPDATE_RETRY_POLICY_SQL = updateSql( 'run_retry_policies', RETRY_POLICY_COLUMNS, `"run_id" = $1 AND "version" = $${RETRY_POLICY_COLUMNS.length + 1}`, ); class PostgresRunReader implements RunRepositoryReader { constructor(protected readonly queryable: PostgresRunQueryable) {} async findRunById(runId: string): Promise { const row = singleRow( await queryMapped( this.queryable, `SELECT ${RUN_SELECT} FROM "ql3"."runs" WHERE "id" = $1`, [runId], ), ); return row ? rowToRun(row) : null; } async findAttemptById(attemptId: string): Promise { const row = singleRow( await queryMapped( this.queryable, `SELECT ${ATTEMPT_SELECT} FROM "ql3"."run_attempts" WHERE "id" = $1`, [attemptId], ), ); return row ? rowToAttempt(row) : null; } async findLatestAttemptByRunId( runId: string, ): Promise { const row = singleRow( await queryMapped( this.queryable, `SELECT ${ATTEMPT_SELECT} FROM "ql3"."run_attempts" WHERE "run_id" = $1 ORDER BY "attempt" DESC, "id" DESC LIMIT 1`, [runId], ), ); return row ? rowToAttempt(row) : null; } async findRetryPolicyByRunId( runId: string, ): Promise { const row = singleRow( await queryMapped( this.queryable, `SELECT ${RETRY_POLICY_SELECT} FROM "ql3"."run_retry_policies" WHERE "run_id" = $1`, [runId], ), ); return row ? rowToRetryPolicy(row) : null; } async listEvents( runId: string, options: { afterSequence?: number; limit?: number } = {}, ): Promise { const afterSequence = options.afterSequence ?? 0; const limit = options.limit ?? 100; if (!Number.isInteger(afterSequence) || afterSequence < 0) { throw new RangeError('afterSequence must be a non-negative integer'); } if ( !Number.isInteger(limit) || limit < 1 || limit > MAX_RUN_EVENT_PAGE_SIZE ) { throw new RangeError( 'limit must be between 1 and MAX_RUN_EVENT_PAGE_SIZE', ); } const result = await queryMapped( this.queryable, `SELECT ${EVENT_SELECT} FROM "ql3"."run_events" WHERE "run_id" = $1 AND "sequence" > $2 ORDER BY "sequence", "id" LIMIT $3`, [runId, afterSequence, limit], ); return result.rows.map(rowToEvent); } async listCancellationRequested( options: { beforeMs?: number; limit?: number } = {}, ): Promise { const beforeMs = options.beforeMs; const limit = options.limit ?? 100; if ( beforeMs !== undefined && (!Number.isSafeInteger(beforeMs) || beforeMs < 0) ) { throw new RangeError('beforeMs must be a non-negative safe integer'); } if ( !Number.isSafeInteger(limit) || limit < 1 || limit > MAX_CANCELLATION_RECOVERY_PAGE_SIZE ) { throw new RangeError( 'limit must be between 1 and MAX_CANCELLATION_RECOVERY_PAGE_SIZE', ); } const result = await queryMapped( this.queryable, `SELECT ${RUN_SELECT} FROM "ql3"."runs" WHERE "status" <> ALL($1::text[]) AND "cancel_requested_at_ms" IS NOT NULL AND ($2::bigint IS NULL OR "cancel_requested_at_ms" <= $2) ORDER BY "cancel_requested_at_ms", "id" LIMIT $3`, [TERMINAL_RUN_STATUSES, beforeMs ?? null, limit], ); return result.rows.map(rowToRun); } } export class PostgresRunTransaction extends PostgresRunReader implements RunRepositoryTransaction { async insertRun(run: RunRecord): Promise { try { await this.queryable.query(INSERT_RUN_SQL, writeValues(run, RUN_COLUMNS)); } catch (error) { if ( sqlState(error) === '23505' && constraintName(error) === RUN_IDEMPOTENCY_CONSTRAINT && run.idempotencyKey ) { throw new DuplicateIdempotencyKeyError( run.projectId, run.idempotencyKey, ); } throw mapPostgresError(error); } } async insertAttempt(attempt: RunAttemptRecord): Promise { try { await this.queryable.query( INSERT_ATTEMPT_SQL, writeValues(attempt, ATTEMPT_COLUMNS), ); } catch (error) { if ( sqlState(error) === '23505' && constraintName(error) === ATTEMPT_NUMBER_CONSTRAINT ) { throw new DuplicateRunAttemptError(attempt.runId, attempt.attempt); } throw mapPostgresError(error); } } async insertRetryPolicy(policy: RunRetryPolicyRecord): Promise { assertRunRetryPolicyRecord(policy); try { await this.queryable.query( INSERT_RETRY_POLICY_SQL, writeValues(policy, RETRY_POLICY_COLUMNS), ); } catch (error) { throw mapPostgresError(error); } } async compareAndSetRun( run: RunRecord, expectedVersion: number, ): Promise { if (run.version !== expectedVersion + 1) { throw new RunRepositoryConstraintError( 'A compare-and-set Run write must increment version exactly once', ); } const result = await queryMapped(this.queryable, UPDATE_RUN_SQL, [ ...writeValues(run, RUN_COLUMNS), expectedVersion, ]); return affectedOneOrNone(result); } async compareAndSetAttempt( attempt: RunAttemptRecord, expected: { status: RunAttemptStatus; callbackSequence: number; }, ): Promise { const result = await queryMapped(this.queryable, UPDATE_ATTEMPT_SQL, [ ...writeValues(attempt, ATTEMPT_COLUMNS), expected.status, expected.callbackSequence, ]); return affectedOneOrNone(result); } async compareAndSetRetryPolicy( policy: RunRetryPolicyRecord, expectedVersion: number, ): Promise { if (policy.version !== expectedVersion + 1) { throw new RunRepositoryConstraintError( 'A compare-and-set retry policy write must increment version exactly once', ); } assertRunRetryPolicyRecord(policy); const result = await queryMapped(this.queryable, UPDATE_RETRY_POLICY_SQL, [ ...writeValues(policy, RETRY_POLICY_COLUMNS), expectedVersion, ]); return affectedOneOrNone(result); } async appendEvent(event: RunEventRecord): Promise { assertEventPayloadSize(event); try { await this.queryable.query( INSERT_EVENT_SQL, writeValues(event, EVENT_COLUMNS), ); } catch (error) { if ( sqlState(error) === '23505' && (constraintName(error) === EVENT_SEQUENCE_CONSTRAINT || constraintName(error) === EVENT_DEDUPE_CONSTRAINT) ) { throw new DuplicateRunEventError(event.runId, event.dedupeKey); } throw mapPostgresError(error); } } } /** * Driver-neutral PostgreSQL Run Repository. The cluster-only package owns the * concrete pg.Pool binding; edge/standalone builds never import the driver. */ export class PostgresRunRepository extends PostgresRunReader implements RunRepository { constructor(private readonly pool: PostgresRunPool) { super(pool); } async transaction( work: (transaction: RunRepositoryTransaction) => Promise, ): Promise { let client: PostgresRunClient; try { client = await this.pool.connect(); } catch (error) { throw mapPostgresError(error); } let began = false; let phase: 'begin' | 'work' | 'commit' = 'begin'; try { await client.query('BEGIN'); began = true; await client.query('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'); await client.query(`SELECT set_config('statement_timeout', $1, true)`, [ `${POSTGRES_RUNTIME_STATEMENT_TIMEOUT_MS}ms`, ]); await client.query(`SELECT set_config('lock_timeout', $1, true)`, [ `${POSTGRES_RUNTIME_LOCK_TIMEOUT_MS}ms`, ]); await client.query( `SELECT set_config('idle_in_transaction_session_timeout', $1, true)`, [`${POSTGRES_RUNTIME_IDLE_TRANSACTION_TIMEOUT_MS}ms`], ); phase = 'work'; const result = await work(new PostgresRunTransaction(client)); phase = 'commit'; await client.query('COMMIT'); began = false; return result; } catch (error) { if (began) { try { await client.query('ROLLBACK'); } catch { // Preserve the work/commit failure; release discards broken clients. } } if (phase === 'work') throw error; throw mapPostgresError(error); } finally { client.release(); } } }