feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,440 @@
// PostgreSQL authorized administration authority for Task and Trigger definitions.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
InvalidTaskDefinitionAdministrationReadError,
TaskDefinitionAdministrationAuthorizationFenceConflictError,
TaskDefinitionAdministrationMutationConflictError,
TaskDefinitionAdministrationReadConflictError,
normalizeAuthorizedTaskDefinitionInspection,
normalizeAuthorizedTaskDefinitionList,
normalizeAuthorizedTaskDefinitionRevisionMutation,
type AuthorizedTaskDefinitionInspection,
type AuthorizedTaskDefinitionList,
type AuthorizedTaskDefinitionRevisionMutation,
type TaskDefinitionAdministrationRepository,
type TaskDefinitionAdministrationSource,
} from '@qinglong/runtime-core/task-definition-administration';
import {
InvalidTriggerAdministrationReadError,
TriggerAdministrationAuthorizationFenceConflictError,
TriggerAdministrationMutationConflictError,
TriggerAdministrationReadConflictError,
normalizeAuthorizedTriggerInspection,
normalizeAuthorizedTriggerList,
normalizeAuthorizedTriggerRevisionMutation,
type AuthorizedTriggerInspection,
type AuthorizedTriggerList,
type AuthorizedTriggerRevisionMutation,
type TriggerAdministrationRepository,
type TriggerAdministrationSource,
} from '@qinglong/runtime-core/trigger-administration';
import {
TaskDefinitionUnavailableError,
type TaskDefinitionPage,
type TaskDefinitionRecord,
} from '@qinglong/runtime-core/task-definition';
import {
TriggerUnavailableError,
type TriggerPage,
type TriggerRecord,
} from '@qinglong/runtime-core/trigger';
import {
ADMINISTRATION_AUDIT_SELECT,
auditFromRow,
configureAdministrationTransaction,
insertAdministrationAudit,
requiredInteger,
requiredString,
retryableAdministrationError,
rollbackAdministrationTransaction,
sameAdministrationReplayAudit,
type AdministrationAuditRow,
type AdministrationRow,
} from '../repository/administrationSupport';
import {
PostgresTaskDefinitionRepository,
PostgresTaskDefinitionSource,
} from './taskDefinitionRepository';
import {
PostgresTriggerRepository,
PostgresTriggerSource,
} from '../scheduling/triggerRepository';
type FenceConflict = new () => Error;
type MutationConflict = new () => Error;
type ReadConflict = new () => Error;
type StorageUnavailable = new () => Error;
type InvalidRead = new (message: string) => Error;
const AUTHORIZED_READ_TRANSACTION_ATTEMPTS = 3;
function assertPool(pool: PostgresPool): void {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError('PostgreSQL automation administration pool is invalid');
}
}
async function confirmAuthorizationFence(
client: PostgresClient,
mutation: Readonly<{
command: Readonly<{ projectId: string }>;
actor: Readonly<{ type: string; id: string }>;
fence: Readonly<{ projectVersion: number; bindingVersion: number | null }>;
}>,
FenceConflictError: FenceConflict,
): Promise<void> {
try {
const project = await client.query<AdministrationRow>(
`SELECT status, version
FROM "ql3"."projects"
WHERE id = $1`,
[mutation.command.projectId],
);
const binding = await client.query<AdministrationRow>(
`SELECT version, state
FROM "ql3"."project_role_bindings"
WHERE project_id = $1 AND subject_type = $2 AND subject_id = $3
ORDER BY version DESC
LIMIT 1`,
[
mutation.command.projectId,
mutation.actor.type,
mutation.actor.id,
],
);
if (
project.rows.length !== 1 ||
requiredString(project.rows[0]!, 'status') !== 'active' ||
requiredInteger(project.rows[0]!, 'version') !==
mutation.fence.projectVersion ||
binding.rows.length !== 1 ||
requiredString(binding.rows[0]!, 'state') !== 'active' ||
requiredInteger(binding.rows[0]!, 'version') !==
mutation.fence.bindingVersion
) {
throw new FenceConflictError();
}
} catch (error) {
if (error instanceof FenceConflictError) throw error;
throw new FenceConflictError();
}
}
async function confirmOrAppendAllowedAudit(
client: PostgresClient,
replay: boolean,
audit: AuthorizedTaskDefinitionRevisionMutation['audit'],
MutationConflictError: MutationConflict,
): Promise<void> {
const result = await client.query<AdministrationAuditRow>(
`SELECT ${ADMINISTRATION_AUDIT_SELECT}
FROM "ql3"."security_audit_events" AS audit
WHERE audit.event_id = $1
LIMIT 2`,
[audit.eventId],
);
if (replay) {
if (result.rows.length !== 1) throw new MutationConflictError();
try {
if (!sameAdministrationReplayAudit(auditFromRow(result.rows[0]!), audit)) {
throw new MutationConflictError();
}
} catch (error) {
if (error instanceof MutationConflictError) throw error;
throw new MutationConflictError();
}
return;
}
if (result.rows.length !== 0) throw new MutationConflictError();
await insertAdministrationAudit(client, audit);
}
function clientReadPool(client: PostgresClient): PostgresPool {
return Object.freeze({
query: client.query.bind(client),
async connect() {
return client;
},
});
}
function sqlState(error: unknown): string | undefined {
if (!error || typeof error !== 'object') return undefined;
const code = (error as { code?: unknown }).code;
return typeof code === 'string' ? code : undefined;
}
async function confirmReadAuthorizationFence(
client: PostgresClient,
read: Readonly<{
projectId: string;
actor: Readonly<{ type: string; id: string }>;
fence: Readonly<{ projectVersion: number; bindingVersion: number | null }>;
}>,
FenceConflictError: FenceConflict,
): Promise<void> {
try {
const project = await client.query<AdministrationRow>(
`SELECT status, version
FROM "ql3"."projects"
WHERE id = $1
LIMIT 1`,
[read.projectId],
);
const binding = await client.query<AdministrationRow>(
`SELECT version, state
FROM "ql3"."project_role_bindings"
WHERE project_id = $1 AND subject_type = $2 AND subject_id = $3
ORDER BY version DESC
LIMIT 1`,
[read.projectId, read.actor.type, read.actor.id],
);
if (
project.rows.length !== 1 ||
requiredString(project.rows[0]!, 'status') !== 'active' ||
requiredInteger(project.rows[0]!, 'version') !==
read.fence.projectVersion ||
binding.rows.length !== 1 ||
requiredString(binding.rows[0]!, 'state') !== 'active' ||
requiredInteger(binding.rows[0]!, 'version') !==
read.fence.bindingVersion
) {
throw new FenceConflictError();
}
} catch (error) {
if (error instanceof FenceConflictError) throw error;
throw new FenceConflictError();
}
}
async function assertFreshReadAudit(
client: PostgresClient,
eventId: string,
ReadConflictError: ReadConflict,
): Promise<void> {
const result = await client.query<AdministrationRow>(
`SELECT event_id
FROM "ql3"."security_audit_events"
WHERE event_id = $1
LIMIT 2`,
[eventId],
);
if (result.rows.length !== 0) throw new ReadConflictError();
}
async function executeAuthorizedRead<TResult>(
pool: PostgresPool,
read: Readonly<{
projectId: string;
actor: Readonly<{ type: string; id: string }>;
fence: Readonly<{ projectVersion: number; bindingVersion: number | null }>;
audit: AuthorizedTaskDefinitionInspection['audit'];
}>,
FenceConflictError: FenceConflict,
ReadConflictError: ReadConflict,
InvalidReadError: InvalidRead,
UnavailableError: StorageUnavailable,
operation: (client: PostgresClient) => Promise<TResult>,
): Promise<TResult> {
for (
let attempt = 0;
attempt < AUTHORIZED_READ_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await pool.connect();
} catch {
throw new UnavailableError();
}
let began = false;
try {
await configureAdministrationTransaction(client);
began = true;
await confirmReadAuthorizationFence(client, read, FenceConflictError);
await assertFreshReadAudit(
client,
read.audit.eventId,
ReadConflictError,
);
const result = await operation(client);
await insertAdministrationAudit(client, read.audit);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollbackAdministrationTransaction(client);
if (
error instanceof FenceConflictError ||
error instanceof ReadConflictError ||
error instanceof InvalidReadError ||
error instanceof UnavailableError
) {
throw error;
}
if (sqlState(error) === '23505') throw new ReadConflictError();
if (
retryableAdministrationError(error) &&
attempt + 1 < AUTHORIZED_READ_TRANSACTION_ATTEMPTS
) {
continue;
}
throw new UnavailableError();
} finally {
client.release();
}
}
throw new UnavailableError();
}
/** Least-privilege automation-manager Task mutation adapter. */
export class PostgresTaskDefinitionAdministrationRepository
implements
TaskDefinitionAdministrationRepository,
TaskDefinitionAdministrationSource
{
private readonly taskDefinitions: PostgresTaskDefinitionRepository;
constructor(private readonly pool: PostgresPool) {
assertPool(pool);
this.taskDefinitions = new PostgresTaskDefinitionRepository(pool);
}
appendAuthorizedTaskDefinitionRevision(
input: AuthorizedTaskDefinitionRevisionMutation,
) {
const mutation = normalizeAuthorizedTaskDefinitionRevisionMutation(input);
return this.taskDefinitions.appendTaskDefinitionRevision(
mutation.command,
async (client, { replay }) => {
await confirmAuthorizationFence(
client,
mutation,
TaskDefinitionAdministrationAuthorizationFenceConflictError,
);
await confirmOrAppendAllowedAudit(
client,
replay !== null,
mutation.audit,
TaskDefinitionAdministrationMutationConflictError,
);
},
);
}
findAuthorizedCurrentTaskDefinition(
input: AuthorizedTaskDefinitionInspection,
): Promise<TaskDefinitionRecord | null> {
const inspection = normalizeAuthorizedTaskDefinitionInspection(input);
return executeAuthorizedRead(
this.pool,
inspection,
TaskDefinitionAdministrationAuthorizationFenceConflictError,
TaskDefinitionAdministrationReadConflictError,
InvalidTaskDefinitionAdministrationReadError,
TaskDefinitionUnavailableError,
(client) =>
new PostgresTaskDefinitionSource(
clientReadPool(client),
).findCurrentTaskDefinition(inspection.projectId, inspection.taskId),
);
}
listAuthorizedTaskDefinitions(
input: AuthorizedTaskDefinitionList,
): Promise<TaskDefinitionPage> {
const query = normalizeAuthorizedTaskDefinitionList(input);
return executeAuthorizedRead(
this.pool,
query,
TaskDefinitionAdministrationAuthorizationFenceConflictError,
TaskDefinitionAdministrationReadConflictError,
InvalidTaskDefinitionAdministrationReadError,
TaskDefinitionUnavailableError,
(client) =>
new PostgresTaskDefinitionSource(clientReadPool(client)).listTaskDefinitions(
{
projectId: query.projectId,
limit: query.limit,
...(query.after ? { after: query.after } : {}),
},
),
);
}
}
/** Least-privilege automation-manager Trigger mutation adapter. */
export class PostgresTriggerAdministrationRepository
implements TriggerAdministrationRepository, TriggerAdministrationSource
{
private readonly triggers: PostgresTriggerRepository;
constructor(private readonly pool: PostgresPool) {
assertPool(pool);
this.triggers = new PostgresTriggerRepository(pool);
}
appendAuthorizedTriggerRevision(input: AuthorizedTriggerRevisionMutation) {
const mutation = normalizeAuthorizedTriggerRevisionMutation(input);
return this.triggers.appendTriggerRevision(
mutation.command,
async (client, { replay }) => {
await confirmAuthorizationFence(
client,
mutation,
TriggerAdministrationAuthorizationFenceConflictError,
);
await confirmOrAppendAllowedAudit(
client,
replay !== null,
mutation.audit,
TriggerAdministrationMutationConflictError,
);
},
);
}
findAuthorizedCurrentTrigger(
input: AuthorizedTriggerInspection,
): Promise<TriggerRecord | null> {
const inspection = normalizeAuthorizedTriggerInspection(input);
return executeAuthorizedRead(
this.pool,
inspection,
TriggerAdministrationAuthorizationFenceConflictError,
TriggerAdministrationReadConflictError,
InvalidTriggerAdministrationReadError,
TriggerUnavailableError,
(client) =>
new PostgresTriggerSource(clientReadPool(client)).findCurrentTrigger(
inspection.projectId,
inspection.triggerId,
),
);
}
listAuthorizedTriggers(input: AuthorizedTriggerList): Promise<TriggerPage> {
const query = normalizeAuthorizedTriggerList(input);
return executeAuthorizedRead(
this.pool,
query,
TriggerAdministrationAuthorizationFenceConflictError,
TriggerAdministrationReadConflictError,
InvalidTriggerAdministrationReadError,
TriggerUnavailableError,
(client) =>
new PostgresTriggerSource(clientReadPool(client)).listTriggers({
projectId: query.projectId,
limit: query.limit,
...(query.after ? { after: query.after } : {}),
}),
);
}
}
@@ -0,0 +1,710 @@
// PostgreSQL authority for Task Definition publication and execution revisions.
import {
InvalidTaskDefinitionError,
TaskDefinitionConflictError,
TaskDefinitionUnavailableError,
assertTaskDefinitionIdentifier,
assertTaskDefinitionPageSize,
assertTaskDefinitionRevision,
createTaskDefinitionRecord,
normalizeAppendTaskDefinitionRevisionCommand,
normalizeTaskDefinitionCursor,
normalizeTaskDefinitionRecord,
type AppendTaskDefinitionRevisionCommand,
type TaskDefinitionPage,
type TaskDefinitionRecord,
type TaskDefinitionRepository,
type TaskDefinitionSource,
} from '@qinglong/runtime-core/task-definition';
import {
BUILT_IN_COMMAND_TASK_SPEC_SCHEMA,
TaskSpecSemanticRegistry,
createBuiltInTaskSpecSemanticRegistry,
} from '@qinglong/runtime-core/task-spec-semantic';
import {
compileClusterCommandTaskDefinition,
normalizeClusterTaskExecutionRevision,
type ClusterTaskExecutionRevision,
type ClusterTaskExecutionRevisionSource,
} from '@qinglong/runtime-core/cluster-execution-revision';
import type {
PostgresClient,
PostgresPool,
PostgresQueryResult,
} from '@qinglong/runtime-core';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredBoolean,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../repository/definitionRepositorySupport';
type TaskDefinitionRow = Record<string, unknown>;
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
export interface PostgresTaskDefinitionRevisionTransactionContext {
readonly command: Readonly<AppendTaskDefinitionRevisionCommand>;
readonly replay: Readonly<TaskDefinitionRecord> | null;
readonly record: Readonly<TaskDefinitionRecord>;
}
export type PostgresTaskDefinitionRevisionTransactionHook = (
client: PostgresClient,
context: Readonly<PostgresTaskDefinitionRevisionTransactionContext>,
) => Promise<void>;
const SELECT_FIELDS = `
head.project_id AS "projectId",
head.task_id AS "taskId",
revision.revision,
revision.mutation_id AS "mutationId",
revision.name,
revision.description,
revision.kind,
revision.spec_json AS "specJson",
revision.labels_json AS "labelsJson",
revision.enabled,
revision.content_digest AS "contentDigest",
head.created_at_ms AS "createdAtMs",
revision.created_at_ms AS "updatedAtMs"`;
function unavailable(): TaskDefinitionUnavailableError {
return new TaskDefinitionUnavailableError();
}
function taskDefinitionRecord(
row: TaskDefinitionRow,
): TaskDefinitionRecord {
try {
const description = row.description;
if (description !== null && typeof description !== 'string') {
throw unavailable();
}
return normalizeTaskDefinitionRecord({
projectId: postgresRequiredString(row.projectId, unavailable),
taskId: postgresRequiredString(row.taskId, unavailable),
revision: postgresRequiredInteger(row.revision, unavailable),
mutationId: postgresRequiredString(row.mutationId, unavailable),
name: postgresRequiredString(row.name, unavailable),
...(description === null ? {} : { description }),
kind: postgresRequiredString(
row.kind,
unavailable,
) as TaskDefinitionRecord['kind'],
spec: postgresRequiredJsonObject(
row.specJson,
unavailable,
) as unknown as TaskDefinitionRecord['spec'],
labels: postgresRequiredJsonObject(
row.labelsJson,
unavailable,
) as TaskDefinitionRecord['labels'],
enabled: postgresRequiredBoolean(row.enabled, unavailable),
contentDigest: postgresRequiredString(row.contentDigest, unavailable),
createdAtMs: postgresRequiredInteger(row.createdAtMs, unavailable),
updatedAtMs: postgresRequiredInteger(row.updatedAtMs, unavailable),
});
} catch (error) {
if (error instanceof TaskDefinitionUnavailableError) throw error;
throw unavailable();
}
}
function sameRecord(
left: TaskDefinitionRecord,
right: TaskDefinitionRecord,
): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function executionPlanJson(
revision: ClusterTaskExecutionRevision,
): Readonly<Record<string, unknown>> {
return Object.freeze({
command: revision.command,
environment: revision.environment,
...(revision.workingDirectory === undefined
? {}
: { workingDirectory: revision.workingDirectory }),
...(revision.timeoutMs === undefined
? {}
: { timeoutMs: revision.timeoutMs }),
...(revision.placement === undefined
? {}
: { placement: revision.placement }),
});
}
function executionRevision(row: TaskDefinitionRow): ClusterTaskExecutionRevision {
try {
const plan = postgresRequiredJsonObject(row.planJson, unavailable);
const keys = Object.keys(plan);
if (
!keys.includes('command') ||
!keys.includes('environment') ||
keys.some(
(key) =>
!['command', 'environment', 'placement', 'timeoutMs', 'workingDirectory'].includes(
key,
),
)
) {
throw unavailable();
}
return normalizeClusterTaskExecutionRevision({
projectId: postgresRequiredString(row.projectId, unavailable),
taskId: postgresRequiredString(row.taskId, unavailable),
sourceRevision: postgresRequiredInteger(row.sourceRevision, unavailable),
taskRevision: postgresRequiredString(row.taskRevision, unavailable),
sourceContentDigest: postgresRequiredString(
row.sourceContentDigest,
unavailable,
),
executorType: postgresRequiredString(
row.executorType,
unavailable,
) as ClusterTaskExecutionRevision['executorType'],
planSchema: postgresRequiredString(
row.planSchema,
unavailable,
) as ClusterTaskExecutionRevision['planSchema'],
command: plan.command as ClusterTaskExecutionRevision['command'],
environment:
plan.environment as ClusterTaskExecutionRevision['environment'],
...(plan.workingDirectory === undefined
? {}
: { workingDirectory: plan.workingDirectory as string }),
...(plan.timeoutMs === undefined
? {}
: { timeoutMs: plan.timeoutMs as number }),
...(plan.placement === undefined
? {}
: {
placement:
plan.placement as unknown as NonNullable<
ClusterTaskExecutionRevision['placement']
>,
}),
contentDigest: postgresRequiredString(row.contentDigest, unavailable),
createdAtMs: postgresRequiredInteger(row.createdAtMs, unavailable),
});
} catch (error) {
if (error instanceof TaskDefinitionUnavailableError) throw error;
throw unavailable();
}
}
export async function findExecutionRevision(
queryable: Queryable,
projectId: string,
taskId: string,
sourceRevision: number,
): Promise<ClusterTaskExecutionRevision | null> {
const result = await queryable.query<TaskDefinitionRow>(
`SELECT project_id AS "projectId", task_id AS "taskId",
source_revision AS "sourceRevision",
task_revision AS "taskRevision",
source_content_digest AS "sourceContentDigest",
executor_type AS "executorType", plan_schema AS "planSchema",
plan_json AS "planJson", content_digest AS "contentDigest",
created_at_ms AS "createdAtMs"
FROM "ql3"."task_execution_revisions"
WHERE project_id = $1 AND task_id = $2 AND source_revision = $3
AND executor_type = 'remote_worker'
LIMIT 2`,
[projectId, taskId, sourceRevision],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return executionRevision(result.rows[0]!);
}
function sameExecutionRevision(
left: ClusterTaskExecutionRevision | null,
right: ClusterTaskExecutionRevision | null,
): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function mappedError(error: unknown): Error {
if (
error instanceof TaskDefinitionConflictError ||
error instanceof TaskDefinitionUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new TaskDefinitionConflictError();
}
return unavailable();
}
async function findCurrent(
queryable: Queryable,
projectId: string,
taskId: string,
): Promise<TaskDefinitionRecord | null> {
const result = await queryable.query<TaskDefinitionRow>(
`SELECT ${SELECT_FIELDS}
FROM "ql3"."task_definitions" AS head
LEFT JOIN "ql3"."task_definition_revisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
WHERE head.project_id = $1 AND head.task_id = $2
LIMIT 2`,
[projectId, taskId],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return taskDefinitionRecord(result.rows[0]!);
}
async function findRevision(
queryable: Queryable,
projectId: string,
taskId: string,
revision: number,
): Promise<TaskDefinitionRecord | null> {
const result = await queryable.query<TaskDefinitionRow>(
`SELECT ${SELECT_FIELDS}
FROM "ql3"."task_definitions" AS head
JOIN "ql3"."task_definition_revisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
WHERE head.project_id = $1 AND head.task_id = $2
AND revision.revision = $3
LIMIT 2`,
[projectId, taskId, revision],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return taskDefinitionRecord(result.rows[0]!);
}
async function findByMutation(
queryable: Queryable,
mutationId: string,
): Promise<TaskDefinitionRecord | null> {
const result = await queryable.query<TaskDefinitionRow>(
`SELECT ${SELECT_FIELDS}
FROM "ql3"."task_definition_revisions" AS revision
JOIN "ql3"."task_definitions" AS head
ON head.project_id = revision.project_id
AND head.task_id = revision.task_id
WHERE revision.mutation_id = $1
LIMIT 2`,
[mutationId],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return taskDefinitionRecord(result.rows[0]!);
}
/** Read-only PostgreSQL TaskDefinition port for the runtime role. */
export class PostgresTaskDefinitionSource implements TaskDefinitionSource {
constructor(protected readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError('PostgreSQL TaskDefinition pool is invalid');
}
}
async findCurrentTaskDefinition(
projectId: string,
taskId: string,
): Promise<TaskDefinitionRecord | null> {
assertTaskDefinitionIdentifier(projectId, 'projectId');
assertTaskDefinitionIdentifier(taskId, 'taskId');
try {
return await findCurrent(this.pool, projectId, taskId);
} catch (error) {
throw mappedError(error);
}
}
async findTaskDefinitionRevision(
projectId: string,
taskId: string,
revision: number,
): Promise<TaskDefinitionRecord | null> {
assertTaskDefinitionIdentifier(projectId, 'projectId');
assertTaskDefinitionIdentifier(taskId, 'taskId');
assertTaskDefinitionRevision(revision);
try {
return await findRevision(this.pool, projectId, taskId, revision);
} catch (error) {
throw mappedError(error);
}
}
async listTaskDefinitions(options: {
readonly projectId: string;
readonly limit: number;
readonly after?: { readonly taskId: string };
}): Promise<TaskDefinitionPage> {
if (!options || typeof options !== 'object' || Array.isArray(options)) {
throw new InvalidTaskDefinitionError('list options are invalid');
}
const keys = Object.keys(options);
if (
!keys.includes('limit') ||
!keys.includes('projectId') ||
keys.some((key) => !['after', 'limit', 'projectId'].includes(key))
) {
throw new InvalidTaskDefinitionError(
'list options have an invalid shape',
);
}
assertTaskDefinitionIdentifier(options.projectId, 'projectId');
assertTaskDefinitionPageSize(options.limit);
const after = options.after
? normalizeTaskDefinitionCursor(options.after)
: undefined;
try {
const result = await this.pool.query<TaskDefinitionRow>(
`SELECT ${SELECT_FIELDS}
FROM "ql3"."task_definitions" AS head
LEFT JOIN "ql3"."task_definition_revisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
WHERE head.project_id = $1 AND head.task_id > $2
ORDER BY head.task_id
LIMIT $3`,
[options.projectId, after?.taskId ?? '', options.limit + 1],
);
const truncated = result.rows.length > options.limit;
const definitions = Object.freeze(
result.rows.slice(0, options.limit).map(taskDefinitionRecord),
);
const last = definitions.at(-1);
return Object.freeze({
definitions,
truncated,
...(truncated && last
? { next: Object.freeze({ taskId: last.taskId }) }
: {}),
});
} catch (error) {
throw mappedError(error);
}
}
}
/** Read-only, digest-verifying remote Worker execution revision source. */
export class PostgresTaskExecutionRevisionSource
implements ClusterTaskExecutionRevisionSource
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError('PostgreSQL Task execution revision pool is invalid');
}
}
async resolveClusterTaskExecutionRevision(identity: {
readonly projectId: string;
readonly taskId: string;
readonly sourceRevision: number;
}): Promise<ClusterTaskExecutionRevision | null> {
if (!identity || typeof identity !== 'object' || Array.isArray(identity)) {
throw new TypeError('Cluster Task execution revision identity is invalid');
}
assertTaskDefinitionIdentifier(identity.projectId, 'projectId');
assertTaskDefinitionIdentifier(identity.taskId, 'taskId');
assertTaskDefinitionRevision(identity.sourceRevision);
try {
return await findExecutionRevision(
this.pool,
identity.projectId,
identity.taskId,
identity.sourceRevision,
);
} catch (error) {
throw mappedError(error);
}
}
}
/** Administration-only TaskDefinition publisher; execution revisions are separate. */
export class PostgresTaskDefinitionRepository
extends PostgresTaskDefinitionSource
implements TaskDefinitionRepository
{
constructor(
pool: PostgresPool,
private readonly semanticRegistry: TaskSpecSemanticRegistry =
createBuiltInTaskSpecSemanticRegistry(),
) {
super(pool);
}
async appendTaskDefinitionRevision(
input: AppendTaskDefinitionRevisionCommand,
transactionHook?: PostgresTaskDefinitionRevisionTransactionHook,
): Promise<
Readonly<{
status: 'created' | 'updated' | 'existing';
definition: TaskDefinitionRecord;
}>
> {
if (
transactionHook !== undefined &&
typeof transactionHook !== 'function'
) {
throw new InvalidTaskDefinitionError('transaction hook is invalid');
}
const normalized = normalizeAppendTaskDefinitionRevisionCommand(input);
const command = Object.freeze({
...normalized,
spec: this.semanticRegistry.normalize({
projectId: normalized.projectId,
taskId: normalized.taskId,
kind: normalized.kind,
spec: normalized.spec,
}),
});
const compiledExecution =
command.enabled &&
command.kind === 'command' &&
command.spec.schema === BUILT_IN_COMMAND_TASK_SPEC_SCHEMA
? compileClusterCommandTaskDefinition(
createTaskDefinitionRecord(
command,
command.occurredAtMs,
),
this.semanticRegistry,
)
: null;
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch {
throw unavailable();
}
let began = false;
let transactionHookError: unknown;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const ownership = await client.query(
`SELECT 1
FROM "ql3"."plugin_package_task_ownerships"
WHERE project_id = $1 AND task_id = $2`,
[command.projectId, command.taskId],
);
if (ownership.rows.length !== 0) {
throw new TaskDefinitionConflictError();
}
const replay = await findByMutation(client, command.mutationId);
if (replay) {
const expected = createTaskDefinitionRecord(
command,
replay.createdAtMs,
);
if (!sameRecord(replay, expected)) {
throw new TaskDefinitionConflictError();
}
const persistedExecution = await findExecutionRevision(
client,
replay.projectId,
replay.taskId,
replay.revision,
);
if (!sameExecutionRevision(persistedExecution, compiledExecution)) {
throw unavailable();
}
if (transactionHook) {
try {
const hookResult = await transactionHook(
client,
Object.freeze({ command, replay, record: replay }),
);
if (hookResult !== undefined) {
throw new InvalidTaskDefinitionError(
'transaction hook must not return a value',
);
}
} catch (error) {
transactionHookError = error;
throw error;
}
}
await client.query('COMMIT');
began = false;
return Object.freeze({ status: 'existing', definition: replay });
}
const project = await client.query<{ status: unknown }>(
`SELECT status FROM "ql3"."projects"
WHERE id = $1`,
[command.projectId],
);
if (
project.rows.length !== 1 ||
project.rows[0]?.status !== 'active'
) {
throw new TaskDefinitionConflictError();
}
const inserted = await client.query(
`INSERT INTO "ql3"."task_definitions" (
project_id, task_id, current_revision, created_at_ms, updated_at_ms
) VALUES ($1, $2, 1, $3, $3)
ON CONFLICT (project_id, task_id) DO NOTHING
RETURNING task_id`,
[command.projectId, command.taskId, command.occurredAtMs],
);
if (inserted.rows.length > 1) throw unavailable();
const created = inserted.rows.length === 1;
const head = await client.query<TaskDefinitionRow>(
`SELECT current_revision AS "currentRevision",
created_at_ms AS "createdAtMs",
updated_at_ms AS "updatedAtMs"
FROM "ql3"."task_definitions"
WHERE project_id = $1 AND task_id = $2
FOR UPDATE`,
[command.projectId, command.taskId],
);
if (head.rows.length !== 1) throw unavailable();
const currentRevision = postgresRequiredInteger(
head.rows[0]!.currentRevision,
unavailable,
);
const createdAtMs = postgresRequiredInteger(
head.rows[0]!.createdAtMs,
unavailable,
);
const previousUpdatedAtMs = postgresRequiredInteger(
head.rows[0]!.updatedAtMs,
unavailable,
);
if (
(created ? command.expectedRevision !== null :
currentRevision !== command.expectedRevision) ||
command.occurredAtMs < previousUpdatedAtMs
) {
throw new TaskDefinitionConflictError();
}
const definition = createTaskDefinitionRecord(command, createdAtMs);
await client.query(
`INSERT INTO "ql3"."task_definition_revisions" (
project_id, task_id, revision, mutation_id, name, description,
kind, spec_json, labels_json, enabled, content_digest, created_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9::jsonb,
$10, $11, $12)`,
[
definition.projectId,
definition.taskId,
definition.revision,
definition.mutationId,
definition.name,
definition.description ?? null,
definition.kind,
JSON.stringify(definition.spec),
JSON.stringify(definition.labels),
definition.enabled,
definition.contentDigest,
definition.updatedAtMs,
],
);
if (compiledExecution) {
await client.query(
`INSERT INTO "ql3"."task_execution_revisions" (
project_id, task_id, source_revision, task_revision,
source_content_digest, executor_type, plan_schema, plan_json,
content_digest, created_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10)`,
[
compiledExecution.projectId,
compiledExecution.taskId,
compiledExecution.sourceRevision,
compiledExecution.taskRevision,
compiledExecution.sourceContentDigest,
compiledExecution.executorType,
compiledExecution.planSchema,
JSON.stringify(executionPlanJson(compiledExecution)),
compiledExecution.contentDigest,
compiledExecution.createdAtMs,
],
);
}
if (!created) {
const update = await client.query(
`UPDATE "ql3"."task_definitions"
SET current_revision = $1, updated_at_ms = $2
WHERE project_id = $3 AND task_id = $4
AND current_revision = $5`,
[
definition.revision,
definition.updatedAtMs,
definition.projectId,
definition.taskId,
command.expectedRevision,
],
);
if (update.rowCount !== 1) throw new TaskDefinitionConflictError();
}
if (transactionHook) {
try {
const hookResult = await transactionHook(
client,
Object.freeze({ command, replay: null, record: definition }),
);
if (hookResult !== undefined) {
throw new InvalidTaskDefinitionError(
'transaction hook must not return a value',
);
}
} catch (error) {
transactionHookError = error;
throw error;
}
}
await client.query('COMMIT');
began = false;
return Object.freeze({
status: created ? 'created' : 'updated',
definition,
});
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
if (error === transactionHookError && error instanceof Error) {
throw error;
}
throw mappedError(error);
} finally {
client.release();
}
}
throw unavailable();
}
}