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,650 @@
// PostgreSQL adapter owned by Plugin Package publication and recovery.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
InvalidPluginPackageAutomationPublicationError,
MAX_PLUGIN_PACKAGE_AUTOMATION_PUBLICATION_BYTES,
PluginPackageAutomationPublicationConflictError,
PluginPackageAutomationPublicationUnavailableError,
assertPluginPackageAutomationPublicationRecoveryPageSize,
assertPluginPackageAutomationPublicationSuccessor,
normalizePluginPackageAutomationPublication,
normalizePluginPackageAutomationPublicationRecoveryCursor,
type PluginPackageAutomationPublication,
type PluginPackageAutomationPublicationRecoveryPage,
type PluginPackageAutomationPublicationRepository,
type PluginPackageAutomationPublicationRecoverySource,
type PluginPackageAutomationPublicationStartGuard,
} from '@qinglong/runtime-core/plugin-package-automation-publication';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const PACKAGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
const DIGEST = /^[0-9a-f]{64}$/;
function invalid(message: string): never {
throw new InvalidPluginPackageAutomationPublicationError(message);
}
function unavailable(
error?: unknown,
): PluginPackageAutomationPublicationUnavailableError {
return new PluginPackageAutomationPublicationUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
function targetIdentity(projectId: unknown, packageName: unknown): {
readonly projectId: string;
readonly packageName: string;
} {
if (typeof projectId !== 'string' || !IDENTIFIER.test(projectId)) {
return invalid('projectId is invalid');
}
if (typeof packageName !== 'string' || !PACKAGE_NAME.test(packageName)) {
return invalid('packageName is invalid');
}
return { projectId, packageName };
}
function publicationDigest(value: unknown): string {
if (typeof value !== 'string' || !DIGEST.test(value)) {
return invalid('publicationDigest is invalid');
}
return value;
}
function serialize(
publication: Readonly<PluginPackageAutomationPublication>,
): string {
const value = JSON.stringify(publication);
if (
Buffer.byteLength(value, 'utf8') >
MAX_PLUGIN_PACKAGE_AUTOMATION_PUBLICATION_BYTES
) {
return invalid('publication exceeds the durable JSON budget');
}
return value;
}
function mapStorageError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageAutomationPublicationError ||
error instanceof PluginPackageAutomationPublicationConflictError ||
error instanceof PluginPackageAutomationPublicationUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (
state === '23503' ||
state === '23505' ||
state === '23514' ||
state === '40001'
) {
return new PluginPackageAutomationPublicationConflictError(
'durable publication chain changed',
);
}
return unavailable(error);
}
export class PostgresPluginPackageAutomationPublicationRepository
implements
PluginPackageAutomationPublicationRepository,
PluginPackageAutomationPublicationRecoverySource,
PluginPackageAutomationPublicationStartGuard
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError(
'PostgreSQL automation publication repository is invalid',
);
}
}
#parse(row: Row): Readonly<PluginPackageAutomationPublication> {
try {
const publication = normalizePluginPackageAutomationPublication(
postgresRequiredJsonObject(
row.publicationJson,
unavailable,
) as unknown as PluginPackageAutomationPublication,
);
if (
publication.publicationDigest !==
postgresRequiredString(row.publicationDigest, unavailable) ||
publication.target.projectId !==
postgresRequiredString(row.projectId, unavailable) ||
publication.target.packageName !==
postgresRequiredString(row.packageName, unavailable) ||
publication.target.installationId !==
postgresRequiredString(row.installationId, unavailable) ||
publication.target.lockDigest !==
postgresRequiredString(row.lockDigest, unavailable) ||
publication.target.generation !==
postgresRequiredInteger(row.generation, unavailable) ||
publication.target.generationDigest !==
postgresRequiredString(row.generationDigest, unavailable) ||
publication.target.materializedRevisionDigest !==
postgresRequiredString(row.materializedRevisionDigest, unavailable) ||
publication.state !==
postgresRequiredString(row.state, unavailable) ||
publication.version !==
postgresRequiredInteger(row.version, unavailable) ||
publication.publishedAtMs !==
postgresRequiredInteger(row.publishedAtMs, unavailable)
) {
throw unavailable();
}
return publication;
} catch (error) {
if (
error instanceof PluginPackageAutomationPublicationUnavailableError
) {
throw error;
}
throw unavailable(error);
}
}
async #findByDigest(
queryable: Queryable,
digest: string,
): Promise<Readonly<PluginPackageAutomationPublication> | null> {
const result = await queryable.query<Row>(
`SELECT publication_digest AS "publicationDigest",
project_id AS "projectId",
package_name AS "packageName",
installation_id AS "installationId",
lock_digest AS "lockDigest",
generation,
generation_digest AS "generationDigest",
materialized_revision_digest AS "materializedRevisionDigest",
state,
version,
published_at_ms AS "publishedAtMs",
publication_json AS "publicationJson"
FROM "ql3"."plugin_package_automation_publications"
WHERE publication_digest = $1
LIMIT 2`,
[digest],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return this.#parse(result.rows[0]!);
}
async #findCurrent(
queryable: Queryable,
projectId: string,
packageName: string,
lock = false,
): Promise<Readonly<PluginPackageAutomationPublication> | null> {
const result = await queryable.query<Row>(
`SELECT publication.publication_digest AS "publicationDigest",
publication.project_id AS "projectId",
publication.package_name AS "packageName",
publication.installation_id AS "installationId",
publication.lock_digest AS "lockDigest",
publication.generation,
publication.generation_digest AS "generationDigest",
publication.materialized_revision_digest
AS "materializedRevisionDigest",
publication.state,
publication.version,
publication.published_at_ms AS "publishedAtMs",
publication.publication_json AS "publicationJson"
FROM "ql3"."plugin_package_automation_publication_heads" AS head
JOIN "ql3"."plugin_package_automation_publications" AS publication
ON publication.publication_digest = head.publication_digest
WHERE head.project_id = $1 AND head.package_name = $2
LIMIT 2${lock ? ' FOR UPDATE OF head' : ''}`,
[projectId, packageName],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return this.#parse(result.rows[0]!);
}
async findCurrent(
projectIdValue: string,
packageNameValue: string,
): Promise<Readonly<PluginPackageAutomationPublication> | null> {
const { projectId, packageName } = targetIdentity(
projectIdValue,
packageNameValue,
);
try {
return await this.#findCurrent(this.pool, projectId, packageName);
} catch (error) {
throw mapStorageError(error);
}
}
async findByDigest(
publicationDigestValue: string,
): Promise<Readonly<PluginPackageAutomationPublication> | null> {
const digest = publicationDigest(publicationDigestValue);
try {
return await this.#findByDigest(this.pool, digest);
} catch (error) {
throw mapStorageError(error);
}
}
async isStartAllowed(
projectIdValue: string,
packageNameValue: string,
publicationDigestValue: string,
): Promise<boolean> {
const { projectId, packageName } = targetIdentity(
projectIdValue,
packageNameValue,
);
const digest = publicationDigest(publicationDigestValue);
try {
const result = await this.pool.query<Row>(
`SELECT "ql3"."plugin_package_automation_start_allowed"(
$1::varchar, $2::varchar, $3::char(64)
) AS "allowed"`,
[projectId, packageName, digest],
);
if (
result.rows.length !== 1 ||
typeof result.rows[0]?.allowed !== 'boolean'
) {
throw unavailable();
}
return result.rows[0].allowed;
} catch (error) {
throw mapStorageError(error);
}
}
async listPendingPage(options: {
readonly limit: number;
readonly after?: Readonly<{
readonly projectId: string;
readonly packageName: string;
}>;
}): Promise<Readonly<PluginPackageAutomationPublicationRecoveryPage>> {
if (!options || typeof options !== 'object' || Array.isArray(options)) {
throw new InvalidPluginPackageAutomationPublicationError(
'pending page options are invalid',
);
}
assertPluginPackageAutomationPublicationRecoveryPageSize(options.limit);
const after =
options.after === undefined
? undefined
: normalizePluginPackageAutomationPublicationRecoveryCursor(
options.after,
);
try {
const result = await this.pool.query<Row>(
`SELECT install.project_id AS "projectId",
install.package_name AS "packageName"
FROM "ql3"."plugin_package_install_heads" AS install_head
JOIN "ql3"."plugin_package_installs" AS install
ON install.installation_id = install_head.installation_id
JOIN "ql3"."plugin_package_materialized_revisions" AS revision
ON revision.project_id = install.project_id
AND revision.package_name = install.package_name
AND revision.generation = install.target_generation
AND revision.lock_digest = install.lock_digest
LEFT JOIN
"ql3"."plugin_package_automation_publication_heads" AS publication
ON publication.project_id = install.project_id
AND publication.package_name = install.package_name
WHERE install.state = 'active'
AND install.active_lock_digest = install.lock_digest
AND NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
WHERE quarantine.project_id = install.project_id
AND quarantine.package_name = install.package_name
AND quarantine.installation_id = install.installation_id
AND quarantine.lock_digest = install.lock_digest
)
AND (
publication.generation_digest IS NULL OR
publication.generation_digest <> revision.generation_digest
)
AND NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
JOIN "ql3"."plugin_package_publisher_revocation_receipts" AS revoked
ON revoked.publisher = provenance.publisher
AND revoked.key_id = provenance.key_id
WHERE provenance.installation_id = install.installation_id
AND provenance.lock_digest = install.lock_digest
)
AND (
$1::varchar IS NULL OR install.project_id > $1 OR
(install.project_id = $1 AND install.package_name > $2)
)
ORDER BY install.project_id, install.package_name
LIMIT $3`,
[
after?.projectId ?? null,
after?.packageName ?? null,
options.limit + 1,
],
);
const truncated = result.rows.length > options.limit;
const candidates = result.rows.slice(0, options.limit).map((row) =>
Object.freeze({
projectId: postgresRequiredString(row.projectId, unavailable),
packageName: postgresRequiredString(row.packageName, unavailable),
}),
);
const last = candidates.at(-1);
return Object.freeze({
candidates: Object.freeze(candidates),
truncated,
...(truncated && last
? {
next: Object.freeze({
projectId: last.projectId,
packageName: last.packageName,
}),
}
: {}),
});
} catch (error) {
throw mapStorageError(error);
}
}
async findCurrentInTransaction(
client: PostgresClient,
projectIdValue: string,
packageNameValue: string,
): Promise<Readonly<PluginPackageAutomationPublication> | null> {
const { projectId, packageName } = targetIdentity(
projectIdValue,
packageNameValue,
);
if (!client || typeof client.query !== 'function') {
throw new TypeError(
'PostgreSQL automation publication transaction is invalid',
);
}
return this.#findCurrent(client, projectId, packageName, true);
}
async publishInTransaction(
client: PostgresClient,
value: Readonly<PluginPackageAutomationPublication>,
): Promise<
Readonly<{
status: 'created' | 'existing';
publication: Readonly<PluginPackageAutomationPublication>;
}>
> {
if (!client || typeof client.query !== 'function') {
throw new TypeError(
'PostgreSQL automation publication transaction is invalid',
);
}
const publication = normalizePluginPackageAutomationPublication(value);
const publicationJson = serialize(publication);
const existing = await this.#findByDigest(
client,
publication.publicationDigest,
);
if (existing) {
if (serialize(existing) !== publicationJson) {
throw new PluginPackageAutomationPublicationConflictError(
'publication digest is bound to another semantic publication',
);
}
return Object.freeze({
status: 'existing' as const,
publication: existing,
});
}
const current = await this.#findCurrent(
client,
publication.target.projectId,
publication.target.packageName,
true,
);
if (publication.version === 1) {
if (current) {
throw new PluginPackageAutomationPublicationConflictError(
'Package already has an automation publication head',
);
}
} else {
if (!current) {
throw new PluginPackageAutomationPublicationConflictError(
'previous automation publication head is absent',
);
}
try {
assertPluginPackageAutomationPublicationSuccessor(
current,
publication,
);
} catch (error) {
if (error instanceof InvalidPluginPackageAutomationPublicationError) {
throw new PluginPackageAutomationPublicationConflictError(
'automation publication does not succeed the current head',
);
}
throw error;
}
}
const revision = await client.query<Row>(
`SELECT revision_digest AS "revisionDigest",
project_id AS "projectId",
package_name AS "packageName",
generation,
lock_digest AS "lockDigest"
FROM "ql3"."plugin_package_materialized_revisions"
WHERE generation_digest = $1
LIMIT 2`,
[publication.target.generationDigest],
);
if (
revision.rows.length !== 1 ||
postgresRequiredString(
revision.rows[0]!.revisionDigest,
unavailable,
) !== publication.target.materializedRevisionDigest ||
postgresRequiredString(revision.rows[0]!.projectId, unavailable) !==
publication.target.projectId ||
postgresRequiredString(
revision.rows[0]!.packageName,
unavailable,
) !== publication.target.packageName ||
postgresRequiredInteger(
revision.rows[0]!.generation,
unavailable,
) !== publication.target.generation ||
postgresRequiredString(revision.rows[0]!.lockDigest, unavailable) !==
publication.target.lockDigest
) {
throw new PluginPackageAutomationPublicationConflictError(
'materialized revision fence does not match publication target',
);
}
const securityFence = await client.query<Row>(
`SELECT
EXISTS (
SELECT 1
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
WHERE quarantine.project_id = $1
AND quarantine.package_name = $2
AND quarantine.installation_id = $3
AND quarantine.lock_digest = $4
) OR EXISTS (
SELECT 1
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
JOIN "ql3"."plugin_package_publisher_revocation_receipts" AS revoked
ON revoked.publisher = provenance.publisher
AND revoked.key_id = provenance.key_id
WHERE provenance.installation_id = $3
AND provenance.lock_digest = $4
) AS "blocked"`,
[
publication.target.projectId,
publication.target.packageName,
publication.target.installationId,
publication.target.lockDigest,
],
);
if (
securityFence.rows.length !== 1 ||
typeof securityFence.rows[0]?.blocked !== 'boolean'
) {
throw unavailable();
}
if (securityFence.rows[0].blocked) {
throw new PluginPackageAutomationPublicationConflictError(
'security-fenced Package generation cannot publish automation',
);
}
await client.query(
`INSERT INTO "ql3"."plugin_package_automation_publications" (
publication_digest, project_id, package_name, installation_id,
lock_digest, generation, generation_digest,
materialized_revision_digest, state, version,
previous_publication_digest, lifecycle_event_digest,
published_at_ms, publication_json
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
$14::jsonb
)`,
[
publication.publicationDigest,
publication.target.projectId,
publication.target.packageName,
publication.target.installationId,
publication.target.lockDigest,
publication.target.generation,
publication.target.generationDigest,
publication.target.materializedRevisionDigest,
publication.state,
publication.version,
publication.previousPublicationDigest,
publication.lifecycleEventDigest,
publication.publishedAtMs,
publicationJson,
],
);
if (!current) {
await client.query(
`INSERT INTO "ql3"."plugin_package_automation_publication_heads" (
project_id, package_name, publication_digest,
generation_digest, state, version, updated_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[
publication.target.projectId,
publication.target.packageName,
publication.publicationDigest,
publication.target.generationDigest,
publication.state,
publication.version,
publication.publishedAtMs,
],
);
} else {
const updated = await client.query(
`UPDATE "ql3"."plugin_package_automation_publication_heads"
SET publication_digest = $1, generation_digest = $2, state = $3,
version = $4, updated_at_ms = $5
WHERE project_id = $6 AND package_name = $7
AND publication_digest = $8 AND version = $9`,
[
publication.publicationDigest,
publication.target.generationDigest,
publication.state,
publication.version,
publication.publishedAtMs,
publication.target.projectId,
publication.target.packageName,
current.publicationDigest,
current.version,
],
);
if (updated.rowCount !== 1) {
throw new PluginPackageAutomationPublicationConflictError(
'automation publication head changed',
);
}
}
return Object.freeze({
status: 'created' as const,
publication,
});
}
async publish(
value: Readonly<PluginPackageAutomationPublication>,
): Promise<
Readonly<{
status: 'created' | 'existing';
publication: Readonly<PluginPackageAutomationPublication>;
}>
> {
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
if (attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS) continue;
throw unavailable(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const result = await this.publishInTransaction(client, value);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS &&
(state === undefined ||
state.startsWith('08') ||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state))
) {
continue;
}
throw mapStorageError(error);
} finally {
client.release();
}
}
throw unavailable();
}
}
@@ -0,0 +1,598 @@
// PostgreSQL adapter owned by Plugin Package publication and recovery.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
InvalidPluginPackageTaskReconciliationError,
PluginPackageTaskReconciliationConflictError,
PluginPackageTaskReconciliationUnavailableError,
normalizePluginPackageTaskReconciliationReceipt,
planPluginPackageTaskReconciliation,
pluginPackageTaskReconciliationTaskIds,
type PluginPackageTaskOwnershipFact,
type PluginPackageTaskReconciliationReceipt,
type PluginPackageTaskReconciliationRepository,
} from '@qinglong/runtime-core/plugin-package-task-reconciliation';
import {
assertPluginPackageTaskPublicationRecoveryPageSize,
normalizePluginPackageTaskPublicationRecoveryCursor,
type PluginPackageTaskPublicationRecoveryPage,
type PluginPackageTaskPublicationRecoverySource,
} from '@qinglong/runtime-core/plugin-package-task-publication';
import {
normalizePluginPackageMaterializedRevision,
type PluginPackageMaterializedRevision,
} from '@qinglong/runtime-core/plugin-package-resource-materialization';
import {
normalizePluginPackageResourceGeneration,
type PluginPackageResourceGenerationSource,
} from '@qinglong/runtime-core/plugin-package-resource-generation';
import {
normalizeTaskDefinitionRecord,
type TaskDefinitionRecord,
} from '@qinglong/runtime-core/task-definition';
import {
compileClusterCommandTaskDefinition,
type ClusterTaskExecutionRevision,
} from '@qinglong/runtime-core/cluster-execution-revision';
import {
BUILT_IN_COMMAND_TASK_SPEC_SCHEMA,
TaskSpecSemanticRegistry,
createBuiltInTaskSpecSemanticRegistry,
} from '@qinglong/runtime-core/task-spec-semantic';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredBoolean,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
const SELECT_TASK_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(): PluginPackageTaskReconciliationUnavailableError {
return new PluginPackageTaskReconciliationUnavailableError();
}
function taskRecord(row: Row): Readonly<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 PluginPackageTaskReconciliationUnavailableError) {
throw error;
}
throw unavailable();
}
}
function executionPlanJson(
value: Readonly<ClusterTaskExecutionRevision>,
): Readonly<Record<string, unknown>> {
return Object.freeze({
command: value.command,
environment: value.environment,
...(value.workingDirectory === undefined
? {}
: { workingDirectory: value.workingDirectory }),
...(value.timeoutMs === undefined ? {} : { timeoutMs: value.timeoutMs }),
...(value.placement === undefined
? {}
: { placement: value.placement }),
});
}
function executionJson(
value: Readonly<ClusterTaskExecutionRevision>,
): Readonly<Record<string, unknown>> {
return Object.freeze({
projectId: value.projectId,
taskId: value.taskId,
sourceRevision: value.sourceRevision,
taskRevision: value.taskRevision,
sourceContentDigest: value.sourceContentDigest,
executorType: value.executorType,
planSchema: value.planSchema,
planJson: executionPlanJson(value),
contentDigest: value.contentDigest,
createdAtMs: value.createdAtMs,
});
}
function mappedError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageTaskReconciliationError ||
error instanceof PluginPackageTaskReconciliationConflictError ||
error instanceof PluginPackageTaskReconciliationUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (
state === '23503' ||
state === '23505' ||
state === '23514' ||
state === '40001'
) {
return new PluginPackageTaskReconciliationConflictError(
'durable generation or TaskDefinition fence changed',
);
}
return new PluginPackageTaskReconciliationUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
export class PostgresPluginPackageTaskReconciliationRepository
implements
PluginPackageTaskReconciliationRepository,
PluginPackageTaskPublicationRecoverySource
{
readonly #registry: TaskSpecSemanticRegistry;
constructor(
private readonly pool: PostgresPool,
registry = createBuiltInTaskSpecSemanticRegistry(),
) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function' ||
!(registry instanceof TaskSpecSemanticRegistry)
) {
throw new TypeError(
'PostgreSQL Package Task reconciliation repository options are invalid',
);
}
this.#registry = registry;
}
async #findStored(
queryable: Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>,
generationDigest: string,
): Promise<Readonly<PluginPackageTaskReconciliationReceipt> | null> {
const result = await queryable.query<Row>(
`SELECT receipt_json AS "receiptJson"
FROM "ql3"."plugin_package_task_reconciliations"
WHERE generation_digest = $1
LIMIT 2`,
[generationDigest],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
try {
return normalizePluginPackageTaskReconciliationReceipt(
postgresRequiredJsonObject(
result.rows[0]!.receiptJson,
unavailable,
) as unknown as PluginPackageTaskReconciliationReceipt,
);
} catch (error) {
if (error instanceof PluginPackageTaskReconciliationUnavailableError) {
throw error;
}
throw unavailable();
}
}
async find(
generationDigest: string,
): Promise<Readonly<PluginPackageTaskReconciliationReceipt> | null> {
if (
typeof generationDigest !== 'string' ||
!/^[0-9a-f]{64}$/.test(generationDigest)
) {
throw new InvalidPluginPackageTaskReconciliationError(
'generationDigest is invalid',
);
}
try {
return await this.#findStored(this.pool, generationDigest);
} catch (error) {
throw mappedError(error);
}
}
async listPendingPage(options: {
readonly limit: number;
readonly after?: Readonly<{
readonly projectId: string;
readonly packageName: string;
}>;
}): Promise<Readonly<PluginPackageTaskPublicationRecoveryPage>> {
if (!options || typeof options !== 'object' || Array.isArray(options)) {
throw new InvalidPluginPackageTaskReconciliationError(
'pending page options are invalid',
);
}
assertPluginPackageTaskPublicationRecoveryPageSize(options.limit);
const after =
options.after === undefined
? undefined
: normalizePluginPackageTaskPublicationRecoveryCursor(options.after);
try {
const result = await this.pool.query<Row>(
`SELECT head.project_id AS "projectId",
head.package_name AS "packageName"
FROM "ql3"."plugin_package_install_heads" AS head
JOIN "ql3"."plugin_package_installs" AS install
ON install.installation_id = head.installation_id
LEFT JOIN "ql3"."plugin_package_task_reconciliations" AS receipt
ON receipt.project_id = install.project_id
AND receipt.package_name = install.package_name
AND receipt.generation = install.target_generation
AND receipt.lock_digest = install.lock_digest
WHERE install.state = 'active'
AND install.active_lock_digest = install.lock_digest
AND receipt.generation_digest IS NULL
AND NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
JOIN "ql3"."plugin_package_publisher_revocation_receipts" AS revoked
ON revoked.publisher = provenance.publisher
AND revoked.key_id = provenance.key_id
WHERE provenance.installation_id = install.installation_id
AND provenance.lock_digest = install.lock_digest
)
AND (
$1::varchar IS NULL OR head.project_id > $1 OR
(head.project_id = $1 AND head.package_name > $2)
)
ORDER BY head.project_id, head.package_name
LIMIT $3`,
[
after?.projectId ?? null,
after?.packageName ?? null,
options.limit + 1,
],
);
const truncated = result.rows.length > options.limit;
const candidates = result.rows.slice(0, options.limit).map((row) =>
Object.freeze({
projectId: postgresRequiredString(row.projectId, unavailable),
packageName: postgresRequiredString(row.packageName, unavailable),
}),
);
const last = candidates.at(-1);
return Object.freeze({
candidates: Object.freeze(candidates),
truncated,
...(truncated && last
? {
next: Object.freeze({
projectId: last.projectId,
packageName: last.packageName,
}),
}
: {}),
});
} catch (error) {
throw mappedError(error);
}
}
async reconcile(
revisionValue: Readonly<PluginPackageMaterializedRevision>,
activeGenerationSource: PluginPackageResourceGenerationSource,
): Promise<
Readonly<{
status: 'created' | 'existing';
receipt: Readonly<PluginPackageTaskReconciliationReceipt>;
}>
> {
const revision = normalizePluginPackageMaterializedRevision(
revisionValue,
this.#registry,
);
if (
!activeGenerationSource ||
typeof activeGenerationSource.findActiveResourceGeneration !== 'function'
) {
throw new InvalidPluginPackageTaskReconciliationError(
'active generation source is invalid',
);
}
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;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const existing = await this.#findStored(
client,
revision.generation.generationDigest,
);
if (existing) {
if (
existing.materializedRevisionDigest !== revision.revisionDigest ||
existing.projectId !== revision.generation.projectId ||
existing.packageName !== revision.generation.packageName
) {
throw new PluginPackageTaskReconciliationConflictError(
'generation is bound to another materialized revision',
);
}
await client.query('COMMIT');
began = false;
return Object.freeze({
status: 'existing',
receipt: existing,
});
}
const materialized = await client.query<Row>(
`SELECT revision_digest AS "revisionDigest"
FROM "ql3"."plugin_package_materialized_revisions"
WHERE generation_digest = $1`,
[revision.generation.generationDigest],
);
if (
materialized.rows.length !== 1 ||
postgresRequiredString(
materialized.rows[0]!.revisionDigest,
unavailable,
) !== revision.revisionDigest
) {
throw new PluginPackageTaskReconciliationConflictError(
'materialized revision is not durably published',
);
}
const install = await client.query<Row>(
`SELECT install.installation_id AS "installationId",
install.state,
install.target_generation AS "targetGeneration",
install.lock_digest AS "lockDigest",
install.previous_active_lock_digest AS "previousLockDigest"
FROM "ql3"."plugin_package_install_heads" AS head
JOIN "ql3"."plugin_package_installs" AS install
ON install.installation_id = head.installation_id
WHERE head.project_id = $1 AND head.package_name = $2
AND NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
JOIN "ql3"."plugin_package_publisher_revocation_receipts" AS revoked
ON revoked.publisher = provenance.publisher
AND revoked.key_id = provenance.key_id
WHERE provenance.installation_id = install.installation_id
AND provenance.lock_digest = install.lock_digest
)
FOR UPDATE OF install`,
[
revision.generation.projectId,
revision.generation.packageName,
],
);
const installRow = install.rows[0];
if (
install.rows.length !== 1 ||
!installRow ||
postgresRequiredString(installRow.installationId, unavailable) !==
revision.generation.installationId ||
postgresRequiredString(installRow.state, unavailable) !== 'active' ||
postgresRequiredInteger(installRow.targetGeneration, unavailable) !==
revision.generation.generation ||
postgresRequiredString(installRow.lockDigest, unavailable) !==
revision.generation.lockDigest ||
(installRow.previousLockDigest === null
? null
: postgresRequiredString(
installRow.previousLockDigest,
unavailable,
)) !== revision.generation.previousActiveLockDigest
) {
throw new PluginPackageTaskReconciliationConflictError(
'Package install head is not the materialized generation',
);
}
const previousResult =
revision.generation.generation === 1
? { rows: [] as Row[] }
: await client.query<Row>(
`SELECT receipt_json AS "receiptJson"
FROM "ql3"."plugin_package_task_reconciliations"
WHERE project_id = $1 AND package_name = $2
AND generation = $3`,
[
revision.generation.projectId,
revision.generation.packageName,
revision.generation.generation - 1,
],
);
const previous =
previousResult.rows.length === 0
? null
: normalizePluginPackageTaskReconciliationReceipt(
postgresRequiredJsonObject(
previousResult.rows[0]!.receiptJson,
unavailable,
) as unknown as PluginPackageTaskReconciliationReceipt,
);
if (
previousResult.rows.length > 1 ||
(revision.generation.generation > 1 && previous === null)
) {
throw new PluginPackageTaskReconciliationConflictError(
'previous generation receipt is missing',
);
}
const taskIds = pluginPackageTaskReconciliationTaskIds(
revision,
previous,
this.#registry,
);
const facts: PluginPackageTaskOwnershipFact[] = [];
for (const taskId of taskIds) {
const result = await client.query<Row>(
`SELECT
${SELECT_TASK_FIELDS},
ownership.package_name AS "ownerPackageName"
FROM (SELECT 1) AS seed
LEFT JOIN "ql3"."task_definitions" AS head
ON head.project_id = $1 AND head.task_id = $2
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
LEFT JOIN "ql3"."plugin_package_task_ownerships" AS ownership
ON ownership.project_id = $1 AND ownership.task_id = $2`,
[revision.generation.projectId, taskId],
);
if (result.rows.length !== 1) throw unavailable();
const row = result.rows[0]!;
facts.push(
Object.freeze({
taskId,
packageName:
row.ownerPackageName === null
? null
: postgresRequiredString(
row.ownerPackageName,
unavailable,
),
current: row.revision === null ? null : taskRecord(row),
}),
);
}
const clock = await client.query<Row>(
`SELECT floor(
extract(epoch FROM clock_timestamp()) * 1000
)::bigint AS "nowMs"`,
);
if (clock.rows.length !== 1) throw unavailable();
const plan = planPluginPackageTaskReconciliation({
revision,
previousReceipt: previous,
facts: Object.freeze(facts),
committedAtMs: postgresRequiredInteger(
clock.rows[0]!.nowMs,
unavailable,
),
taskSpecSemanticRegistry: this.#registry,
});
const activeValue =
await activeGenerationSource.findActiveResourceGeneration(
revision.generation.projectId,
revision.generation.packageName,
);
if (
activeValue === null ||
normalizePluginPackageResourceGeneration(activeValue)
.generationDigest !== revision.generation.generationDigest
) {
throw new PluginPackageTaskReconciliationConflictError(
'active generation changed during reconciliation',
);
}
const executions = plan.writes.flatMap(({ definition }) =>
definition.enabled &&
definition.kind === 'command' &&
definition.spec.schema === BUILT_IN_COMMAND_TASK_SPEC_SCHEMA
? [
executionJson(
compileClusterCommandTaskDefinition(
definition,
this.#registry,
),
),
]
: [],
);
const committed = await client.query<Row>(
`SELECT "ql3"."commit_plugin_package_task_reconciliation"(
$1::char(64), $2::char(64), $3::jsonb, $4::jsonb, $5::jsonb
) AS "created"`,
[
revision.generation.generationDigest,
revision.revisionDigest,
JSON.stringify(plan.receipt),
JSON.stringify(plan.writes),
JSON.stringify(executions),
],
);
if (
committed.rows.length !== 1 ||
typeof committed.rows[0]!.created !== 'boolean'
) {
throw unavailable();
}
await client.query('COMMIT');
began = false;
return Object.freeze({
status: committed.rows[0]!.created ? 'created' : 'existing',
receipt: plan.receipt,
});
} 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;
}
throw mappedError(error);
} finally {
client.release();
}
}
throw unavailable();
}
}