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,594 @@
import type { DatabaseSync } from 'node:sqlite';
import {
InvalidPluginPackageAutomationPublicationError,
MAX_PLUGIN_PACKAGE_AUTOMATION_PUBLICATION_BYTES,
PluginPackageAutomationPublicationConflictError,
PluginPackageAutomationPublicationUnavailableError,
assertPluginPackageAutomationPublicationSuccessor,
assertPluginPackageAutomationPublicationRecoveryPageSize,
normalizePluginPackageAutomationPublication,
normalizePluginPackageAutomationPublicationRecoveryCursor,
type PluginPackageAutomationPublication,
type PluginPackageAutomationPublicationRecoveryPage,
type PluginPackageAutomationPublicationRepository,
type PluginPackageAutomationPublicationRecoverySource,
type PluginPackageAutomationPublicationStartGuard,
} from '@qinglong/runtime-core/plugin-package-automation-publication';
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
type Row = Record<string, unknown>;
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 text(row: Row, key: string): string {
const value = row[key];
if (typeof value !== 'string') {
throw new PluginPackageAutomationPublicationUnavailableError();
}
return value;
}
function integer(row: Row, key: string): number {
const value = row[key];
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new PluginPackageAutomationPublicationUnavailableError();
}
return value as number;
}
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;
}
if (
error &&
typeof error === 'object' &&
'code' in error &&
typeof error.code === 'string' &&
error.code.startsWith('SQLITE_CONSTRAINT')
) {
return new PluginPackageAutomationPublicationConflictError(
'durable publication chain changed',
);
}
return new PluginPackageAutomationPublicationUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
export class LocalSqlitePluginPackageAutomationPublicationRepository
implements
PluginPackageAutomationPublicationRepository,
PluginPackageAutomationPublicationRecoverySource,
PluginPackageAutomationPublicationStartGuard
{
readonly #authority: LocalSqliteOperationAuthority;
constructor(authority: LocalSqliteOperationAuthority | DatabaseSync) {
this.#authority =
authority instanceof LocalSqliteOperationAuthority
? authority
: new LocalSqliteOperationAuthority(authority);
}
#parse(row: Row): Readonly<PluginPackageAutomationPublication> {
try {
const publication = normalizePluginPackageAutomationPublication(
JSON.parse(text(row, 'publicationJson')) as
PluginPackageAutomationPublication,
);
if (
publication.publicationDigest !==
text(row, 'publicationDigest') ||
publication.target.projectId !== text(row, 'projectId') ||
publication.target.packageName !== text(row, 'packageName') ||
publication.target.installationId !== text(row, 'installationId') ||
publication.target.lockDigest !== text(row, 'lockDigest') ||
publication.target.generation !== integer(row, 'generation') ||
publication.target.generationDigest !==
text(row, 'generationDigest') ||
publication.target.materializedRevisionDigest !==
text(row, 'materializedRevisionDigest') ||
publication.state !== text(row, 'state') ||
publication.version !== integer(row, 'version') ||
publication.publishedAtMs !== integer(row, 'publishedAtMs')
) {
throw new PluginPackageAutomationPublicationUnavailableError();
}
return publication;
} catch (error) {
if (
error instanceof PluginPackageAutomationPublicationUnavailableError
) {
throw error;
}
throw new PluginPackageAutomationPublicationUnavailableError();
}
}
#select(
where: string,
values: readonly (string | number | null)[],
): Row | undefined {
return this.#authority.client
.prepare(
`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 "QingLong3PluginPackageAutomationPublications"
WHERE ${where}`,
)
.get(...values) as Row | undefined;
}
#findByDigest(
digest: string,
): Readonly<PluginPackageAutomationPublication> | null {
const row = this.#select('publication_digest = ?', [digest]);
return row ? this.#parse(row) : null;
}
#findCurrent(
projectId: string,
packageName: string,
): Readonly<PluginPackageAutomationPublication> | null {
const row = this.#select(
`publication_digest = (
SELECT publication_digest
FROM "QingLong3PluginPackageAutomationPublicationHeads"
WHERE project_id = ? AND package_name = ?
)`,
[projectId, packageName],
);
return row ? this.#parse(row) : null;
}
#enqueue<T>(work: () => T | Promise<T>): Promise<T> {
return this.#authority.enqueue(
async () => {
try {
return await work();
} catch (error) {
throw mapStorageError(error);
}
},
() => new PluginPackageAutomationPublicationUnavailableError(),
);
}
findCurrent(
projectIdValue: string,
packageNameValue: string,
): Promise<Readonly<PluginPackageAutomationPublication> | null> {
const { projectId, packageName } = targetIdentity(
projectIdValue,
packageNameValue,
);
return this.#enqueue(() => this.#findCurrent(projectId, packageName));
}
findByDigest(
publicationDigestValue: string,
): Promise<Readonly<PluginPackageAutomationPublication> | null> {
const digest = publicationDigest(publicationDigestValue);
return this.#enqueue(() => this.#findByDigest(digest));
}
isStartAllowed(
projectIdValue: string,
packageNameValue: string,
publicationDigestValue: string,
): Promise<boolean> {
const { projectId, packageName } = targetIdentity(
projectIdValue,
packageNameValue,
);
const digest = publicationDigest(publicationDigestValue);
return this.#enqueue(() => {
const row = this.#authority.client
.prepare(
`SELECT EXISTS (
SELECT 1
FROM "QingLong3PluginPackageAutomationPublicationHeads" AS head
JOIN "QingLong3PluginPackageAutomationPublications" AS publication
ON publication.publication_digest = head.publication_digest
JOIN "QingLong3PluginPackageInstallHeads" AS install_head
ON install_head.project_id = publication.project_id
AND install_head.package_name = publication.package_name
AND install_head.installation_id =
publication.installation_id
JOIN "QingLong3PluginPackageInstalls" AS install
ON install.installation_id = install_head.installation_id
AND install.lock_digest = publication.lock_digest
LEFT JOIN "QingLong3PluginPackageLifecycleHeads" AS lifecycle
ON lifecycle.project_id = publication.project_id
AND lifecycle.package_name = publication.package_name
WHERE head.project_id = ?
AND head.package_name = ?
AND head.publication_digest = ?
AND publication.state = 'active'
AND install.state = 'active'
AND install.active_lock_digest = publication.lock_digest
AND (
lifecycle.event_digest IS NULL OR
lifecycle.disposition = 'active'
)
AND NOT EXISTS (
SELECT 1
FROM "QingLong3PluginPackageQuarantineEvents" AS quarantine
WHERE quarantine.project_id = publication.project_id
AND quarantine.package_name = publication.package_name
AND quarantine.installation_id =
publication.installation_id
AND quarantine.lock_digest = publication.lock_digest
)
) AS "allowed"`,
)
.get(projectId, packageName, digest) as Row | undefined;
if (!row || (row.allowed !== 0 && row.allowed !== 1)) {
throw new PluginPackageAutomationPublicationUnavailableError();
}
return row.allowed === 1;
});
}
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)) {
return Promise.reject(
new InvalidPluginPackageAutomationPublicationError(
'pending page options are invalid',
),
);
}
assertPluginPackageAutomationPublicationRecoveryPageSize(options.limit);
const after =
options.after === undefined
? undefined
: normalizePluginPackageAutomationPublicationRecoveryCursor(
options.after,
);
return this.#enqueue(() => {
const rows = this.#authority.client
.prepare(
`SELECT install.project_id AS "projectId",
install.package_name AS "packageName"
FROM "QingLong3PluginPackageInstallHeads" AS install_head
JOIN "QingLong3PluginPackageInstalls" AS install
ON install.installation_id = install_head.installation_id
JOIN "QingLong3PluginPackageMaterializedRevisions" 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
"QingLong3PluginPackageAutomationPublicationHeads" 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 "QingLong3PluginPackageQuarantineEvents" 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 (
? IS NULL OR install.project_id > ? OR
(install.project_id = ? AND install.package_name > ?)
)
ORDER BY install.project_id, install.package_name
LIMIT ?`,
)
.all(
after?.projectId ?? null,
after?.projectId ?? null,
after?.projectId ?? null,
after?.packageName ?? null,
options.limit + 1,
) as Row[];
const truncated = rows.length > options.limit;
const candidates = rows.slice(0, options.limit).map((row) =>
Object.freeze({
projectId: text(row, 'projectId'),
packageName: text(row, 'packageName'),
}),
);
const last = candidates.at(-1);
return Object.freeze({
candidates: Object.freeze(candidates),
truncated,
...(truncated && last
? {
next: Object.freeze({
projectId: last.projectId,
packageName: last.packageName,
}),
}
: {}),
});
});
}
findCurrentInTransaction(
projectIdValue: string,
packageNameValue: string,
): Readonly<PluginPackageAutomationPublication> | null {
const { projectId, packageName } = targetIdentity(
projectIdValue,
packageNameValue,
);
if (!this.#authority.client.isTransaction) {
throw new PluginPackageAutomationPublicationUnavailableError();
}
return this.#findCurrent(projectId, packageName);
}
publishInTransaction(
value: Readonly<PluginPackageAutomationPublication>,
): Readonly<{
status: 'created' | 'existing';
publication: Readonly<PluginPackageAutomationPublication>;
}> {
const client = this.#authority.client;
if (!client.isTransaction) {
throw new PluginPackageAutomationPublicationUnavailableError();
}
const publication = normalizePluginPackageAutomationPublication(value);
const publicationJson = serialize(publication);
const existing = this.#findByDigest(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 = this.#findCurrent(
publication.target.projectId,
publication.target.packageName,
);
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 = client
.prepare(
`SELECT revision_digest AS "revisionDigest",
project_id AS "projectId",
package_name AS "packageName",
generation,
lock_digest AS "lockDigest"
FROM "QingLong3PluginPackageMaterializedRevisions"
WHERE generation_digest = ?`,
)
.get(publication.target.generationDigest) as Row | undefined;
if (
!revision ||
text(revision, 'revisionDigest') !==
publication.target.materializedRevisionDigest ||
text(revision, 'projectId') !== publication.target.projectId ||
text(revision, 'packageName') !== publication.target.packageName ||
integer(revision, 'generation') !== publication.target.generation ||
text(revision, 'lockDigest') !== publication.target.lockDigest
) {
throw new PluginPackageAutomationPublicationConflictError(
'materialized revision fence does not match publication target',
);
}
const securityFence = client
.prepare(
`SELECT EXISTS (
SELECT 1
FROM "QingLong3PluginPackageQuarantineEvents" AS quarantine
WHERE quarantine.project_id = ?
AND quarantine.package_name = ?
AND quarantine.installation_id = ?
AND quarantine.lock_digest = ?
) AS "blocked"`,
)
.get(
publication.target.projectId,
publication.target.packageName,
publication.target.installationId,
publication.target.lockDigest,
) as Row | undefined;
if (
!securityFence ||
(securityFence.blocked !== 0 && securityFence.blocked !== 1)
) {
throw new PluginPackageAutomationPublicationUnavailableError();
}
if (securityFence.blocked === 1) {
throw new PluginPackageAutomationPublicationConflictError(
'quarantined Package generation cannot publish automation',
);
}
client
.prepare(
`INSERT INTO "QingLong3PluginPackageAutomationPublications" (
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
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) {
client
.prepare(
`INSERT INTO "QingLong3PluginPackageAutomationPublicationHeads" (
project_id, package_name, publication_digest,
generation_digest, state, version, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
)
.run(
publication.target.projectId,
publication.target.packageName,
publication.publicationDigest,
publication.target.generationDigest,
publication.state,
publication.version,
publication.publishedAtMs,
);
} else {
const updated = client
.prepare(
`UPDATE "QingLong3PluginPackageAutomationPublicationHeads"
SET publication_digest = ?, generation_digest = ?, state = ?,
version = ?, updated_at_ms = ?
WHERE project_id = ? AND package_name = ?
AND publication_digest = ? AND version = ?`,
)
.run(
publication.publicationDigest,
publication.target.generationDigest,
publication.state,
publication.version,
publication.publishedAtMs,
publication.target.projectId,
publication.target.packageName,
current.publicationDigest,
current.version,
);
if (updated.changes !== 1) {
throw new PluginPackageAutomationPublicationConflictError(
'automation publication head changed',
);
}
}
return Object.freeze({
status: 'created' as const,
publication,
});
}
publish(
value: Readonly<PluginPackageAutomationPublication>,
): Promise<
Readonly<{
status: 'created' | 'existing';
publication: Readonly<PluginPackageAutomationPublication>;
}>
> {
return this.#enqueue(() => {
const client = this.#authority.client;
client.exec('BEGIN IMMEDIATE');
try {
const result = this.publishInTransaction(value);
client.exec('COMMIT');
return result;
} catch (error) {
if (client.isTransaction) client.exec('ROLLBACK');
throw error;
}
});
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,230 @@
import type { DatabaseSync } from 'node:sqlite';
import {
InvalidPluginPackageResourceMaterializationError,
MAX_PLUGIN_PACKAGE_MATERIALIZED_REVISION_JSON_BYTES,
PluginPackageResourceMaterializationConflictError,
PluginPackageResourceMaterializationUnavailableError,
normalizePluginPackageMaterializedRevision,
type PluginPackageMaterializedRevision,
type PluginPackageMaterializedRevisionRepository,
} from '@qinglong/runtime-core/plugin-package-resource-materialization';
import {
TaskSpecSemanticRegistry,
createBuiltInTaskSpecSemanticRegistry,
} from '@qinglong/runtime-core/task-spec-semantic';
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
type Row = Record<string, unknown>;
const DIGEST = /^[0-9a-f]{64}$/;
function invalid(message: string): never {
throw new InvalidPluginPackageResourceMaterializationError(message);
}
function text(row: Row, key: string): string {
const value = row[key];
if (typeof value !== 'string') {
throw new PluginPackageResourceMaterializationUnavailableError();
}
return value;
}
function integer(row: Row, key: string): number {
const value = row[key];
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new PluginPackageResourceMaterializationUnavailableError();
}
return value as number;
}
function generationDigest(value: unknown): string {
if (typeof value !== 'string' || !DIGEST.test(value)) {
return invalid('generation digest is invalid');
}
return value;
}
function serialize(
revision: Readonly<PluginPackageMaterializedRevision>,
): string {
const value = JSON.stringify(revision);
if (
Buffer.byteLength(value, 'utf8') >
MAX_PLUGIN_PACKAGE_MATERIALIZED_REVISION_JSON_BYTES
) {
return invalid('materialized revision exceeds the durable JSON budget');
}
return value;
}
function mapStorageError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageResourceMaterializationError ||
error instanceof PluginPackageResourceMaterializationConflictError ||
error instanceof PluginPackageResourceMaterializationUnavailableError
) {
return error;
}
if (
error &&
typeof error === 'object' &&
'code' in error &&
typeof error.code === 'string' &&
error.code.startsWith('SQLITE_CONSTRAINT')
) {
return new PluginPackageResourceMaterializationConflictError(
'durable revision identity is already bound',
);
}
return new PluginPackageResourceMaterializationUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
export class LocalSqlitePluginPackageMaterializedRevisionRepository
implements PluginPackageMaterializedRevisionRepository
{
private readonly authority: LocalSqliteOperationAuthority;
private readonly taskSpecSemanticRegistry: TaskSpecSemanticRegistry;
constructor(
authority: LocalSqliteOperationAuthority | DatabaseSync,
taskSpecSemanticRegistry = createBuiltInTaskSpecSemanticRegistry(),
) {
if (!(taskSpecSemanticRegistry instanceof TaskSpecSemanticRegistry)) {
throw new TypeError('TaskSpec semantic registry is invalid');
}
this.authority =
authority instanceof LocalSqliteOperationAuthority
? authority
: new LocalSqliteOperationAuthority(authority);
this.taskSpecSemanticRegistry = taskSpecSemanticRegistry;
}
private parse(row: Row): Readonly<PluginPackageMaterializedRevision> {
try {
const revision = normalizePluginPackageMaterializedRevision(
JSON.parse(text(row, 'revisionJson')) as PluginPackageMaterializedRevision,
this.taskSpecSemanticRegistry,
);
if (
revision.generation.generationDigest !==
text(row, 'generationDigest') ||
revision.generation.projectId !== text(row, 'projectId') ||
revision.generation.packageName !== text(row, 'packageName') ||
revision.generation.generation !== integer(row, 'generation') ||
revision.generation.lockDigest !== text(row, 'lockDigest') ||
revision.manifestDigest !== text(row, 'manifestDigest') ||
revision.revisionDigest !== text(row, 'revisionDigest')
) {
throw new PluginPackageResourceMaterializationUnavailableError();
}
integer(row, 'createdAtMs');
return revision;
} catch (error) {
if (error instanceof PluginPackageResourceMaterializationUnavailableError) {
throw error;
}
throw new PluginPackageResourceMaterializationUnavailableError();
}
}
private findStored(
digest: string,
): Readonly<PluginPackageMaterializedRevision> | null {
const row = this.authority.client
.prepare(
`SELECT
generation_digest AS "generationDigest",
project_id AS "projectId",
package_name AS "packageName",
generation,
lock_digest AS "lockDigest",
manifest_digest AS "manifestDigest",
revision_digest AS "revisionDigest",
revision_json AS "revisionJson",
created_at_ms AS "createdAtMs"
FROM "QingLong3PluginPackageMaterializedRevisions"
WHERE generation_digest = ?`,
)
.get(digest) as Row | undefined;
return row ? this.parse(row) : null;
}
private enqueue<T>(work: () => T): Promise<T> {
return this.authority.enqueue(
async () => {
try {
return work();
} catch (error) {
throw mapStorageError(error);
}
},
() => new PluginPackageResourceMaterializationUnavailableError(),
);
}
async find(
digest: string,
): Promise<Readonly<PluginPackageMaterializedRevision> | null> {
const normalizedDigest = generationDigest(digest);
return await this.enqueue(() => this.findStored(normalizedDigest));
}
publish(
value: Readonly<PluginPackageMaterializedRevision>,
): Promise<
Readonly<{
status: 'created' | 'existing';
revision: Readonly<PluginPackageMaterializedRevision>;
}>
> {
const revision = normalizePluginPackageMaterializedRevision(
value,
this.taskSpecSemanticRegistry,
);
const revisionJson = serialize(revision);
return this.enqueue(() => {
const result = this.authority.client
.prepare(
`INSERT INTO "QingLong3PluginPackageMaterializedRevisions" (
generation_digest, project_id, package_name, generation,
lock_digest, manifest_digest, revision_digest, revision_json,
created_at_ms
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
CAST(unixepoch('subsec') * 1000 AS INTEGER))
ON CONFLICT (generation_digest) DO NOTHING`,
)
.run(
revision.generation.generationDigest,
revision.generation.projectId,
revision.generation.packageName,
revision.generation.generation,
revision.generation.lockDigest,
revision.manifestDigest,
revision.revisionDigest,
revisionJson,
);
const stored = this.findStored(revision.generation.generationDigest);
if (!stored) {
throw new PluginPackageResourceMaterializationUnavailableError();
}
if (
stored.revisionDigest !== revision.revisionDigest ||
JSON.stringify(stored) !== revisionJson
) {
throw new PluginPackageResourceMaterializationConflictError(
'generation digest is bound to another semantic revision',
);
}
return Object.freeze({
status: result.changes === 1 ? 'created' : 'existing',
revision: stored,
});
});
}
}
@@ -0,0 +1,325 @@
import type { DatabaseSync } from 'node:sqlite';
import {
PluginPackageInstallProposalConflictError,
PluginPackageInstallProposalUnavailableError,
normalizePluginPackageInstallProposal,
type CreatePluginPackageInstallProposalCommand,
type CreatePluginPackageInstallProposalResult,
type PluginPackageInstallProposal,
type PluginPackageInstallProposalRepository,
} from '@qinglong/runtime-core/plugin-package-proposal';
import {
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
} from '@qinglong/runtime-core/security-audit';
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
type Row = Record<string, unknown>;
function text(row: Row, key: string): string {
const value = row[key];
if (typeof value !== 'string') {
throw new PluginPackageInstallProposalUnavailableError();
}
return value;
}
function nullableText(row: Row, key: string): string | null {
const value = row[key];
if (value !== null && typeof value !== 'string') {
throw new PluginPackageInstallProposalUnavailableError();
}
return value;
}
function integer(row: Row, key: string): number {
const value = row[key];
if (!Number.isSafeInteger(value)) {
throw new PluginPackageInstallProposalUnavailableError();
}
return value as number;
}
function nullableInteger(row: Row, key: string): number | null {
const value = row[key];
if (value !== null && !Number.isSafeInteger(value)) {
throw new PluginPackageInstallProposalUnavailableError();
}
return value as number | null;
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function parseProposal(row: Row): Readonly<PluginPackageInstallProposal> {
try {
const proposal = normalizePluginPackageInstallProposal(
JSON.parse(text(row, 'proposalJson')) as PluginPackageInstallProposal,
);
if (proposal.proposalDigest !== text(row, 'proposalDigest')) {
throw new PluginPackageInstallProposalUnavailableError();
}
return proposal;
} catch (error) {
if (error instanceof PluginPackageInstallProposalUnavailableError) {
throw error;
}
throw new PluginPackageInstallProposalUnavailableError();
}
}
export function findLocalPluginPackageInstallProposal(
client: DatabaseSync,
actionRef: string,
): Readonly<PluginPackageInstallProposal> | null {
const row = client
.prepare(
`SELECT "proposal_json" AS "proposalJson",
"proposal_digest" AS "proposalDigest"
FROM "QingLong3PluginPackageInstallProposals"
WHERE "action_ref" = ?`,
)
.get(actionRef) as Row | undefined;
return row ? parseProposal(row) : null;
}
function parseAudit(row: Row): Readonly<SecurityAuditRecord> {
try {
const subjectType = nullableText(row, 'subjectType');
const subjectId = nullableText(row, 'subjectId');
const projectVersion = nullableInteger(row, 'projectVersion');
return normalizeSecurityAuditRecord({
eventId: text(row, 'eventId'),
requestId: text(row, 'requestId'),
operationId: text(row, 'operationId'),
projectId: nullableText(row, 'projectId'),
subject:
subjectType === null || subjectId === null
? null
: { type: subjectType, id: subjectId },
authenticationId: nullableText(row, 'authenticationId'),
outcome: text(row, 'outcome'),
reasons: JSON.parse(text(row, 'reasonsJson')),
fence:
projectVersion === null
? null
: {
projectVersion,
bindingVersion: nullableInteger(row, 'bindingVersion'),
},
occurredAtMs: integer(row, 'occurredAtMs'),
} as SecurityAuditRecord);
} catch (error) {
if (error instanceof PluginPackageInstallProposalUnavailableError) {
throw error;
}
throw new PluginPackageInstallProposalUnavailableError();
}
}
function storageError(error: unknown): Error {
if (
error instanceof PluginPackageInstallProposalConflictError ||
error instanceof PluginPackageInstallProposalUnavailableError
) {
return error;
}
if (
error &&
typeof error === 'object' &&
'code' in error &&
typeof error.code === 'string' &&
error.code.startsWith('SQLITE_CONSTRAINT')
) {
return new PluginPackageInstallProposalConflictError();
}
return new PluginPackageInstallProposalUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
export class LocalSqlitePluginPackageInstallProposalRepository
implements PluginPackageInstallProposalRepository
{
readonly #authority: LocalSqliteOperationAuthority;
readonly #client: DatabaseSync;
constructor(authority: LocalSqliteOperationAuthority | DatabaseSync) {
this.#authority =
authority instanceof LocalSqliteOperationAuthority
? authority
: new LocalSqliteOperationAuthority(authority);
this.#client = this.#authority.client;
}
#enqueue<T>(work: () => T | Promise<T>): Promise<T> {
return this.#authority.enqueue(
async () => {
try {
return await work();
} catch (error) {
throw storageError(error);
}
},
() => new PluginPackageInstallProposalUnavailableError(),
);
}
#proposal(actionRef: string): Readonly<PluginPackageInstallProposal> | null {
return findLocalPluginPackageInstallProposal(this.#client, actionRef);
}
#audit(eventId: string): Readonly<SecurityAuditRecord> | null {
const row = this.#client
.prepare(
`SELECT "event_id" AS "eventId", "request_id" AS "requestId",
"operation_id" AS "operationId", "project_id" AS "projectId",
"subject_type" AS "subjectType", "subject_id" AS "subjectId",
"authentication_id" AS "authenticationId",
"outcome" AS "outcome", "reasons_json" AS "reasonsJson",
"fence_project_version" AS "projectVersion",
"fence_binding_version" AS "bindingVersion",
"occurred_at_ms" AS "occurredAtMs"
FROM "QingLong3SecurityAuditEvents"
WHERE "event_id" = ?`,
)
.get(eventId) as Row | undefined;
return row ? parseAudit(row) : null;
}
findProposalByActionRef(
actionRef: string,
): Promise<Readonly<PluginPackageInstallProposal> | null> {
return this.#enqueue(() => this.#proposal(actionRef));
}
createProposal(
command: CreatePluginPackageInstallProposalCommand,
): Promise<Readonly<CreatePluginPackageInstallProposalResult>> {
const proposal = normalizePluginPackageInstallProposal(command.proposal);
const audit = normalizeSecurityAuditRecord(command.audit);
return this.#enqueue(() => {
this.#client.exec('BEGIN IMMEDIATE');
try {
if (
audit.requestId !== proposal.actionRef ||
audit.operationId !== 'plugin_package.propose' ||
audit.projectId !== proposal.projectId ||
audit.subject?.type !== proposal.proposedBy.type ||
audit.subject.id !== proposal.proposedBy.id ||
audit.authenticationId === null ||
audit.outcome !== 'allowed' ||
!same(audit.reasons, ['package_proposal']) ||
audit.fence?.projectVersion !==
proposal.proposalFence.projectVersion ||
audit.fence.bindingVersion !==
proposal.proposalFence.bindingVersion ||
audit.occurredAtMs !== proposal.createdAtMs
) {
throw new PluginPackageInstallProposalConflictError();
}
const existing = this.#proposal(proposal.actionRef);
if (existing) {
const existingAudit = this.#audit(audit.eventId);
if (
!same(existing, proposal) ||
!existingAudit ||
!same(existingAudit, audit)
) {
throw new PluginPackageInstallProposalConflictError();
}
this.#client.exec('COMMIT');
return Object.freeze({
status: 'existing' as const,
proposal,
});
}
const fence = this.#client
.prepare(
`SELECT project."status" AS "status",
project."version" AS "projectVersion",
(
SELECT max(binding."version")
FROM "QingLong3ProjectRoleBindings" AS binding
WHERE binding."project_id" = project."id"
AND binding."subject_type" = ?
AND binding."subject_id" = ?
) AS "bindingVersion"
FROM "QingLong3Projects" AS project
WHERE project."id" = ?`,
)
.get(
proposal.proposedBy.type,
proposal.proposedBy.id,
proposal.projectId,
) as Row | undefined;
if (
!fence ||
fence.status !== 'active' ||
integer(fence, 'projectVersion') !==
proposal.proposalFence.projectVersion ||
nullableInteger(fence, 'bindingVersion') !==
proposal.proposalFence.bindingVersion
) {
throw new PluginPackageInstallProposalConflictError();
}
this.#client
.prepare(
`INSERT INTO "QingLong3PluginPackageInstallProposals" (
"action_ref", "project_id", "action_type", "permission",
"action_digest", "preview_digest", "proposed_by_type",
"proposed_by_id", "fence_project_version",
"fence_binding_version", "created_at_ms", "proposal_json",
"proposal_digest"
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
proposal.actionRef,
proposal.projectId,
proposal.actionType,
proposal.permission,
proposal.actionDigest,
proposal.previewDigest,
proposal.proposedBy.type,
proposal.proposedBy.id,
proposal.proposalFence.projectVersion,
proposal.proposalFence.bindingVersion,
proposal.createdAtMs,
JSON.stringify(proposal),
proposal.proposalDigest,
);
this.#client
.prepare(
`INSERT INTO "QingLong3SecurityAuditEvents" (
"event_id", "request_id", "operation_id", "project_id",
"subject_type", "subject_id", "authentication_id", "outcome",
"reasons_json", "fence_project_version",
"fence_binding_version", "occurred_at_ms"
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
audit.eventId,
audit.requestId,
audit.operationId,
audit.projectId,
audit.subject?.type ?? null,
audit.subject?.id ?? null,
audit.authenticationId,
audit.outcome,
JSON.stringify(audit.reasons),
audit.fence?.projectVersion ?? null,
audit.fence?.bindingVersion ?? null,
audit.occurredAtMs,
);
this.#client.exec('COMMIT');
return Object.freeze({ status: 'created' as const, proposal });
} catch (error) {
if (this.#client.isTransaction) this.#client.exec('ROLLBACK');
throw error;
}
});
}
}
@@ -0,0 +1,890 @@
import type { DatabaseSync } from 'node:sqlite';
import {
InvalidPluginPackageQuarantineError,
MAX_PLUGIN_PACKAGE_QUARANTINE_TASK_WITHDRAWALS,
PluginPackageQuarantineConflictError,
PluginPackageQuarantineUnavailableError,
assertPluginPackageWithdrawalMatchesEvent,
createPluginPackageWithdrawalReceipt,
normalizePluginPackageQuarantineEvent,
normalizePluginPackageWithdrawalReceipt,
pluginPackageQuarantineTaskMutationId,
type PluginPackageQuarantineEvent,
type PluginPackageQuarantineRepository,
type PluginPackageQuarantineTarget,
type PluginPackageQuarantineTaskWithdrawal,
type PluginPackageWithdrawalReceipt,
} from '@qinglong/runtime-core/plugin-package-quarantine';
import {
normalizePluginPackageInstallRecord,
type PluginPackageInstallRecord,
} from '@qinglong/runtime-core/plugin-package-install';
import {
createProjectToolDefinitionSnapshot,
normalizeProjectToolDefinitionSnapshot,
projectToolDefinitionActiveVectorDigest,
projectToolDefinitionSnapshotContribution,
type ProjectToolDefinitionSnapshot,
type ProjectToolDefinitionSnapshotContribution,
type ProjectToolDefinitionSnapshotSource,
} from '@qinglong/runtime-core/project-tool-definition-snapshot';
import {
createTaskDefinitionRecord,
normalizeTaskDefinitionRecord,
type TaskDefinitionRecord,
} from '@qinglong/runtime-core/task-definition';
import {
TaskSpecSemanticRegistry,
createBuiltInTaskSpecSemanticRegistry,
} from '@qinglong/runtime-core/task-spec-semantic';
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
type Row = Record<string, unknown>;
export const EDGE_PLUGIN_PACKAGE_QUARANTINE_ACTIVE_SOURCE_LIMIT = 4;
export const STANDALONE_PLUGIN_PACKAGE_QUARANTINE_ACTIVE_SOURCE_LIMIT = 16;
function text(row: Row, key: string): string {
const value = row[key];
if (typeof value !== 'string') {
throw new PluginPackageQuarantineUnavailableError();
}
return value;
}
function integer(row: Row, key: string): number {
const value = row[key];
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new PluginPackageQuarantineUnavailableError();
}
return value as number;
}
function json(row: Row, key: string): unknown {
try {
return JSON.parse(text(row, key));
} catch {
throw new PluginPackageQuarantineUnavailableError();
}
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function mapStorageError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageQuarantineError ||
error instanceof PluginPackageQuarantineConflictError ||
error instanceof PluginPackageQuarantineUnavailableError
) {
return error;
}
if (
error &&
typeof error === 'object' &&
'code' in error &&
typeof error.code === 'string' &&
error.code.startsWith('SQLITE_CONSTRAINT')
) {
return new PluginPackageQuarantineConflictError(
'durable quarantine identity is already bound',
);
}
return new PluginPackageQuarantineUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
function taskRecord(row: Row): Readonly<TaskDefinitionRecord> {
try {
return normalizeTaskDefinitionRecord({
projectId: text(row, 'projectId'),
taskId: text(row, 'taskId'),
revision: integer(row, 'revision'),
mutationId: text(row, 'mutationId'),
name: text(row, 'name'),
...(row.description === null
? {}
: { description: text(row, 'description') }),
kind: text(row, 'kind') as TaskDefinitionRecord['kind'],
spec: json(row, 'specJson') as TaskDefinitionRecord['spec'],
labels: json(row, 'labelsJson') as TaskDefinitionRecord['labels'],
enabled: integer(row, 'enabled') === 1,
contentDigest: text(row, 'contentDigest'),
createdAtMs: integer(row, 'createdAtMs'),
updatedAtMs: integer(row, 'updatedAtMs'),
});
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) throw error;
throw new PluginPackageQuarantineUnavailableError();
}
}
function activeSource(
contribution: Readonly<ProjectToolDefinitionSnapshotContribution>,
): Readonly<ProjectToolDefinitionSnapshotSource> {
return Object.freeze({
installationId: contribution.generation.installationId,
packageName: contribution.generation.packageName,
generation: contribution.generation.generation,
generationDigest: contribution.generation.generationDigest,
lockDigest: contribution.generation.lockDigest,
revisionDigest: contribution.revisionDigest,
});
}
export class LocalSqlitePluginPackageQuarantineRepository
implements PluginPackageQuarantineRepository
{
readonly #authority: LocalSqliteOperationAuthority;
readonly #registry: TaskSpecSemanticRegistry;
readonly #activeSourceLimit: number;
constructor(
authority: LocalSqliteOperationAuthority | DatabaseSync,
options: Readonly<{
registry?: TaskSpecSemanticRegistry;
activeSourceLimit?: number;
}> = {},
) {
const registry =
options.registry ?? createBuiltInTaskSpecSemanticRegistry();
const activeSourceLimit =
options.activeSourceLimit ??
STANDALONE_PLUGIN_PACKAGE_QUARANTINE_ACTIVE_SOURCE_LIMIT;
if (!(registry instanceof TaskSpecSemanticRegistry)) {
throw new TypeError('TaskSpec semantic registry is invalid');
}
if (
!Number.isSafeInteger(activeSourceLimit) ||
activeSourceLimit < 1 ||
activeSourceLimit >
STANDALONE_PLUGIN_PACKAGE_QUARANTINE_ACTIVE_SOURCE_LIMIT
) {
throw new RangeError('active source limit is invalid');
}
this.#authority =
authority instanceof LocalSqliteOperationAuthority
? authority
: new LocalSqliteOperationAuthority(authority);
this.#registry = registry;
this.#activeSourceLimit = activeSourceLimit;
}
#enqueue<T>(work: () => T | Promise<T>): Promise<T> {
return this.#authority.enqueue(
async () => {
try {
return await work();
} catch (error) {
throw mapStorageError(error);
}
},
() => new PluginPackageQuarantineUnavailableError(),
);
}
#eventByDigest(
eventDigest: string,
): Readonly<PluginPackageQuarantineEvent> | null {
const row = this.#authority.client
.prepare(
`SELECT event_json AS "eventJson"
FROM "QingLong3PluginPackageQuarantineEvents"
WHERE event_digest = ?`,
)
.get(eventDigest) as Row | undefined;
if (!row) return null;
try {
return normalizePluginPackageQuarantineEvent(
json(row, 'eventJson') as PluginPackageQuarantineEvent,
);
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) throw error;
throw new PluginPackageQuarantineUnavailableError();
}
}
#receiptByEvent(
event: Readonly<PluginPackageQuarantineEvent>,
): Readonly<PluginPackageWithdrawalReceipt> | null {
const row = this.#authority.client
.prepare(
`SELECT receipt_json AS "receiptJson"
FROM "QingLong3PluginPackageWithdrawalReceipts"
WHERE event_digest = ?`,
)
.get(event.eventDigest) as Row | undefined;
if (!row) return null;
try {
const receipt = normalizePluginPackageWithdrawalReceipt(
json(row, 'receiptJson') as PluginPackageWithdrawalReceipt,
);
assertPluginPackageWithdrawalMatchesEvent(event, receipt);
this.#assertReceiptRelations(receipt);
return receipt;
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) throw error;
throw new PluginPackageQuarantineUnavailableError();
}
}
#assertReceiptRelations(
receipt: Readonly<PluginPackageWithdrawalReceipt>,
): void {
const rows = this.#authority.client
.prepare(
`SELECT item.project_id AS "projectId",
item.task_id AS "taskId",
item.previous_revision AS "previousRevision",
item.disabled_revision AS "disabledRevision",
item.previous_content_digest AS "previousContentDigest",
item.disabled_content_digest AS "disabledContentDigest",
previous.content_digest AS "storedPreviousContentDigest",
disabled.content_digest AS "storedDisabledContentDigest"
FROM "QingLong3PluginPackageWithdrawalTasks" AS item
JOIN "QingLong3TaskDefinitionRevisions" AS previous
ON previous.project_id = item.project_id
AND previous.task_id = item.task_id
AND previous.revision = item.previous_revision
JOIN "QingLong3TaskDefinitionRevisions" AS disabled
ON disabled.project_id = item.project_id
AND disabled.task_id = item.task_id
AND disabled.revision = item.disabled_revision
WHERE item.event_digest = ?
ORDER BY item.task_id COLLATE BINARY`,
)
.all(receipt.eventDigest) as Row[];
const withdrawals = rows.map((row) =>
Object.freeze({
taskId: text(row, 'taskId'),
previousRevision: integer(row, 'previousRevision'),
disabledRevision: integer(row, 'disabledRevision'),
previousContentDigest: text(row, 'previousContentDigest'),
disabledContentDigest: text(row, 'disabledContentDigest'),
}),
);
if (
rows.some(
(row) =>
text(row, 'projectId') !== receipt.target.projectId ||
text(row, 'previousContentDigest') !==
text(row, 'storedPreviousContentDigest') ||
text(row, 'disabledContentDigest') !==
text(row, 'storedDisabledContentDigest'),
) ||
!same(withdrawals, receipt.capability.taskWithdrawals)
) {
throw new PluginPackageQuarantineUnavailableError();
}
if (receipt.capability.status === 'not_active') return;
const snapshotRow = this.#authority.client
.prepare(
`SELECT snapshot_json AS "snapshotJson"
FROM "QingLong3ProjectToolDefinitionSnapshots"
WHERE project_id = ? AND active_vector_digest = ?
AND snapshot_digest = ?`,
)
.get(
receipt.target.projectId,
receipt.capability.currentActiveVectorDigest,
receipt.capability.currentToolSnapshotDigest,
) as Row | undefined;
if (!snapshotRow) {
throw new PluginPackageQuarantineUnavailableError();
}
try {
const snapshot = normalizeProjectToolDefinitionSnapshot(
json(snapshotRow, 'snapshotJson') as ProjectToolDefinitionSnapshot,
);
if (
snapshot.sources.length !== receipt.capability.retainedSourceCount ||
snapshot.sources.some(
(source) =>
source.packageName === receipt.target.packageName &&
source.lockDigest === receipt.target.lockDigest,
)
) {
throw new PluginPackageQuarantineUnavailableError();
}
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) throw error;
throw new PluginPackageQuarantineUnavailableError();
}
}
#findStored(
eventDigest: string,
): Readonly<PluginPackageWithdrawalReceipt> | null {
const event = this.#eventByDigest(eventDigest);
if (!event) return null;
const receipt = this.#receiptByEvent(event);
if (!receipt) {
throw new PluginPackageQuarantineUnavailableError();
}
return receipt;
}
findTargetsByLockDigest(
lockDigest: string,
): Promise<readonly Readonly<PluginPackageQuarantineTarget>[]> {
if (typeof lockDigest !== 'string' || !/^[0-9a-f]{64}$/.test(lockDigest)) {
throw new InvalidPluginPackageQuarantineError('lockDigest is invalid');
}
return this.#enqueue(() => {
const rows = this.#authority.client
.prepare(
`SELECT record_json AS "recordJson"
FROM "QingLong3PluginPackageInstalls"
WHERE lock_digest = ?
AND state IN ('queued','staged','activating','active')
ORDER BY project_id, package_name, installation_id
LIMIT ?`,
)
.all(lockDigest, this.#activeSourceLimit + 1) as Row[];
if (rows.length > this.#activeSourceLimit) {
throw new PluginPackageQuarantineConflictError(
'matching install targets exceed the local Profile limit',
);
}
try {
return Object.freeze(
rows.map((row) => {
const record = normalizePluginPackageInstallRecord(
json(row, 'recordJson') as PluginPackageInstallRecord,
);
return Object.freeze({
projectId: record.projectId,
packageName: record.packageName,
installationId: record.installationId,
lockDigest: record.lockDigest,
installState:
record.state as PluginPackageQuarantineTarget['installState'],
installVersion: record.version,
installRecordDigest: record.recordDigest,
activeLockDigest: record.activeLockDigest,
});
}),
);
} catch (error) {
if (
error instanceof PluginPackageQuarantineConflictError ||
error instanceof PluginPackageQuarantineUnavailableError
) {
throw error;
}
throw new PluginPackageQuarantineUnavailableError();
}
});
}
findByEventDigest(
eventDigest: string,
): Promise<Readonly<PluginPackageWithdrawalReceipt> | null> {
if (
typeof eventDigest !== 'string' ||
!/^[0-9a-f]{64}$/.test(eventDigest)
) {
throw new InvalidPluginPackageQuarantineError('eventDigest is invalid');
}
return this.#enqueue(() => this.#findStored(eventDigest));
}
#install(
event: Readonly<PluginPackageQuarantineEvent>,
): Readonly<PluginPackageInstallRecord> {
const row = this.#authority.client
.prepare(
`SELECT record_json AS "recordJson"
FROM "QingLong3PluginPackageInstalls"
WHERE installation_id = ?`,
)
.get(event.target.installationId) as Row | undefined;
if (!row) {
throw new PluginPackageQuarantineConflictError(
'target install is absent',
);
}
let record: Readonly<PluginPackageInstallRecord>;
try {
record = normalizePluginPackageInstallRecord(
json(row, 'recordJson') as PluginPackageInstallRecord,
);
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) throw error;
throw new PluginPackageQuarantineUnavailableError();
}
if (
record.projectId !== event.target.projectId ||
record.packageName !== event.target.packageName ||
record.lockDigest !== event.target.lockDigest ||
record.state !== event.target.installState ||
record.version !== event.target.installVersion ||
record.recordDigest !== event.target.installRecordDigest ||
record.activeLockDigest !== event.target.activeLockDigest
) {
throw new PluginPackageQuarantineConflictError(
'target install advanced or drifted',
);
}
return record;
}
#activeContributions(
projectId: string,
): readonly Readonly<ProjectToolDefinitionSnapshotContribution>[] {
const rows = this.#authority.client
.prepare(
`SELECT revision.revision_json AS "revisionJson"
FROM "QingLong3PluginPackageInstallHeads" AS head
JOIN "QingLong3PluginPackageInstalls" AS head_install
ON head_install.installation_id = head.installation_id
JOIN "QingLong3PluginPackageInstalls" AS active_install
ON active_install.project_id = head.project_id
AND active_install.package_name = head.package_name
AND active_install.lock_digest = head_install.active_lock_digest
JOIN "QingLong3PluginPackageMaterializedRevisions" AS revision
ON revision.project_id = active_install.project_id
AND revision.package_name = active_install.package_name
AND revision.generation = active_install.target_generation
AND revision.lock_digest = active_install.lock_digest
LEFT JOIN "QingLong3PluginPackageQuarantineEvents" AS quarantine
ON quarantine.project_id = active_install.project_id
AND quarantine.package_name = active_install.package_name
AND quarantine.installation_id = active_install.installation_id
AND quarantine.lock_digest = active_install.lock_digest
WHERE head.project_id = ?
AND head_install.active_lock_digest IS NOT NULL
AND active_install.state = 'active'
AND quarantine.event_digest IS NULL
ORDER BY head.package_name COLLATE BINARY
LIMIT ?`,
)
.all(projectId, this.#activeSourceLimit + 1) as Row[];
if (rows.length > this.#activeSourceLimit) {
throw new PluginPackageQuarantineConflictError(
'active Package sources exceed the local Profile limit',
);
}
try {
return Object.freeze(
rows.map((row) =>
projectToolDefinitionSnapshotContribution(
json(row, 'revisionJson') as never,
this.#registry,
),
),
);
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) throw error;
throw new PluginPackageQuarantineUnavailableError();
}
}
#enabledOwnedTasks(
event: Readonly<PluginPackageQuarantineEvent>,
): readonly Readonly<TaskDefinitionRecord>[] {
const rows = this.#authority.client
.prepare(
`SELECT head.project_id AS "projectId",
head.task_id AS "taskId",
revision.revision AS "revision",
revision.mutation_id AS "mutationId",
revision.name AS "name",
revision.description AS "description",
revision.kind AS "kind",
revision.spec_json AS "specJson",
revision.labels_json AS "labelsJson",
revision.enabled AS "enabled",
revision.content_digest AS "contentDigest",
head.created_at_ms AS "createdAtMs",
revision.created_at_ms AS "updatedAtMs"
FROM "QingLong3PluginPackageTaskOwnerships" AS ownership
JOIN "QingLong3TaskDefinitions" AS head
ON head.project_id = ownership.project_id
AND head.task_id = ownership.task_id
JOIN "QingLong3TaskDefinitionRevisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
WHERE ownership.project_id = ?
AND ownership.package_name = ?
AND revision.enabled = 1
ORDER BY ownership.task_id COLLATE BINARY
LIMIT ?`,
)
.all(
event.target.projectId,
event.target.packageName,
MAX_PLUGIN_PACKAGE_QUARANTINE_TASK_WITHDRAWALS + 1,
) as Row[];
if (rows.length > MAX_PLUGIN_PACKAGE_QUARANTINE_TASK_WITHDRAWALS) {
throw new PluginPackageQuarantineConflictError(
'owned Tasks exceed the quarantine withdrawal limit',
);
}
return Object.freeze(rows.map(taskRecord));
}
#appendDisabledTask(
event: Readonly<PluginPackageQuarantineEvent>,
current: Readonly<TaskDefinitionRecord>,
committedAtMs: number,
): Readonly<PluginPackageQuarantineTaskWithdrawal> {
const disabled = createTaskDefinitionRecord(
{
projectId: current.projectId,
taskId: current.taskId,
expectedRevision: current.revision,
mutationId: pluginPackageQuarantineTaskMutationId(
event.eventDigest,
current.taskId,
),
name: current.name,
...(current.description === undefined
? {}
: { description: current.description }),
kind: current.kind,
spec: current.spec,
labels: current.labels,
enabled: false,
occurredAtMs: committedAtMs,
},
current.createdAtMs,
);
this.#authority.client
.prepare(
`INSERT INTO "QingLong3TaskDefinitionRevisions" (
project_id, task_id, revision, mutation_id, name, description,
kind, spec_json, labels_json, enabled, content_digest, created_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)`,
)
.run(
disabled.projectId,
disabled.taskId,
disabled.revision,
disabled.mutationId,
disabled.name,
disabled.description ?? null,
disabled.kind,
JSON.stringify(disabled.spec),
JSON.stringify(disabled.labels),
disabled.contentDigest,
disabled.updatedAtMs,
);
const update = this.#authority.client
.prepare(
`UPDATE "QingLong3TaskDefinitions"
SET current_revision = ?, updated_at_ms = ?
WHERE project_id = ? AND task_id = ? AND current_revision = ?`,
)
.run(
disabled.revision,
disabled.updatedAtMs,
disabled.projectId,
disabled.taskId,
current.revision,
);
if (update.changes !== 1) {
throw new PluginPackageQuarantineConflictError(
'TaskDefinition head changed during quarantine',
);
}
return Object.freeze({
taskId: current.taskId,
previousRevision: current.revision,
disabledRevision: disabled.revision,
previousContentDigest: current.contentDigest,
disabledContentDigest: disabled.contentDigest,
});
}
#publishSnapshot(
snapshot: Readonly<ProjectToolDefinitionSnapshot>,
committedAtMs: number,
): void {
const snapshotJson = JSON.stringify(snapshot);
const insert = this.#authority.client
.prepare(
`INSERT INTO "QingLong3ProjectToolDefinitionSnapshots" (
project_id, active_vector_digest, definitions_digest,
snapshot_digest, snapshot_json, committed_at_ms
) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT (project_id, active_vector_digest) DO NOTHING`,
)
.run(
snapshot.projectId,
snapshot.activeVectorDigest,
snapshot.definitionsDigest,
snapshot.snapshotDigest,
snapshotJson,
committedAtMs,
);
if (insert.changes === 1) {
const insertSource = this.#authority.client.prepare(
`INSERT INTO "QingLong3ProjectToolDefinitionSnapshotSources" (
project_id, active_vector_digest, package_name, installation_id,
generation, generation_digest, lock_digest, revision_digest
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
);
for (const source of snapshot.sources) {
insertSource.run(
snapshot.projectId,
snapshot.activeVectorDigest,
source.packageName,
source.installationId,
source.generation,
source.generationDigest,
source.lockDigest,
source.revisionDigest,
);
}
}
const stored = this.#authority.client
.prepare(
`SELECT snapshot_json AS "snapshotJson"
FROM "QingLong3ProjectToolDefinitionSnapshots"
WHERE project_id = ? AND active_vector_digest = ?`,
)
.get(snapshot.projectId, snapshot.activeVectorDigest) as Row | undefined;
if (!stored || text(stored, 'snapshotJson') !== snapshotJson) {
throw new PluginPackageQuarantineConflictError(
'active vector is bound to another Tool snapshot',
);
}
}
#insertEvent(event: Readonly<PluginPackageQuarantineEvent>): void {
this.#authority.client
.prepare(
`INSERT INTO "QingLong3PluginPackageQuarantineEvents" (
event_digest, mutation_id, revocation_receipt_digest, impact_digest,
project_id, package_name, installation_id, lock_digest,
install_state, install_version, install_record_digest,
active_lock_digest, proposer_type, proposer_id, confirmer_type,
confirmer_id, authorization_mode, reason_code, occurred_at_ms,
event_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
event.eventDigest,
event.mutationId,
event.revocationReceiptDigest,
event.impactDigest,
event.target.projectId,
event.target.packageName,
event.target.installationId,
event.target.lockDigest,
event.target.installState,
event.target.installVersion,
event.target.installRecordDigest,
event.target.activeLockDigest,
event.proposer.type,
event.proposer.id,
event.confirmer.type,
event.confirmer.id,
event.authorizationMode,
event.reasonCode,
event.occurredAtMs,
JSON.stringify(event),
);
}
#insertReceipt(receipt: Readonly<PluginPackageWithdrawalReceipt>): void {
const capability = receipt.capability;
this.#authority.client
.prepare(
`INSERT INTO "QingLong3PluginPackageWithdrawalReceipts" (
event_digest, receipt_digest, project_id, capability_status,
task_count, previous_active_vector_digest,
current_active_vector_digest, current_tool_snapshot_digest,
retained_source_count, committed_at_ms, receipt_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
receipt.eventDigest,
receipt.receiptDigest,
receipt.target.projectId,
capability.status,
capability.taskWithdrawals.length,
capability.previousActiveVectorDigest,
capability.currentActiveVectorDigest,
capability.currentToolSnapshotDigest,
capability.retainedSourceCount,
receipt.committedAtMs,
JSON.stringify(receipt),
);
const insertTask = this.#authority.client.prepare(
`INSERT INTO "QingLong3PluginPackageWithdrawalTasks" (
event_digest, project_id, task_id, previous_revision,
disabled_revision, previous_content_digest, disabled_content_digest
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
);
for (const task of capability.taskWithdrawals) {
insertTask.run(
receipt.eventDigest,
receipt.target.projectId,
task.taskId,
task.previousRevision,
task.disabledRevision,
task.previousContentDigest,
task.disabledContentDigest,
);
}
}
quarantine(
eventValue: Readonly<PluginPackageQuarantineEvent>,
confirmAuthorization: () => void | Promise<void>,
): Promise<
Readonly<{
status: 'created' | 'existing';
receipt: Readonly<PluginPackageWithdrawalReceipt>;
}>
> {
const event = normalizePluginPackageQuarantineEvent(eventValue);
if (typeof confirmAuthorization !== 'function') {
throw new InvalidPluginPackageQuarantineError(
'confirmAuthorization is invalid',
);
}
return this.#enqueue(async () => {
const client = this.#authority.client;
client.exec('BEGIN IMMEDIATE');
try {
await confirmAuthorization();
const existingEvent = this.#eventByDigest(event.eventDigest);
if (existingEvent) {
if (!same(existingEvent, event)) {
throw new PluginPackageQuarantineConflictError(
'event digest is bound to another quarantine',
);
}
const receipt = this.#receiptByEvent(existingEvent);
if (!receipt) {
throw new PluginPackageQuarantineUnavailableError();
}
await confirmAuthorization();
client.exec('COMMIT');
return Object.freeze({
status: 'existing' as const,
receipt,
});
}
const targetEvent = client
.prepare(
`SELECT event_digest AS "eventDigest"
FROM "QingLong3PluginPackageQuarantineEvents"
WHERE project_id = ? AND package_name = ?
AND installation_id = ? AND lock_digest = ?`,
)
.get(
event.target.projectId,
event.target.packageName,
event.target.installationId,
event.target.lockDigest,
) as Row | undefined;
if (targetEvent) {
throw new PluginPackageQuarantineConflictError(
'target lock is already quarantined by another event',
);
}
this.#install(event);
const clock = client
.prepare(
`SELECT CAST(unixepoch('subsec') * 1000 AS INTEGER) AS "nowMs"`,
)
.get() as Row;
const committedAtMs = Math.max(
integer(clock, 'nowMs'),
event.occurredAtMs,
);
if (event.target.installState !== 'active') {
this.#insertEvent(event);
const receipt = createPluginPackageWithdrawalReceipt({
eventDigest: event.eventDigest,
target: event.target,
capability: {
status: 'not_active',
taskWithdrawals: [],
previousActiveVectorDigest: null,
currentActiveVectorDigest: null,
currentToolSnapshotDigest: null,
retainedSourceCount: 0,
},
committedAtMs,
});
this.#insertReceipt(receipt);
await confirmAuthorization();
client.exec('COMMIT');
return Object.freeze({
status: 'created' as const,
receipt,
});
}
const previousContributions = this.#activeContributions(
event.target.projectId,
);
const targetIndex = previousContributions.findIndex(
({ generation }) =>
generation.packageName === event.target.packageName &&
generation.installationId === event.target.installationId &&
generation.lockDigest === event.target.lockDigest,
);
if (targetIndex < 0) {
throw new PluginPackageQuarantineConflictError(
'active target is not a complete Tool source',
);
}
const previousSources = Object.freeze(
previousContributions.map(activeSource),
);
const retainedContributions = Object.freeze(
previousContributions.filter((_, index) => index !== targetIndex),
);
const snapshot = createProjectToolDefinitionSnapshot({
projectId: event.target.projectId,
contributions: retainedContributions,
});
const tasks = this.#enabledOwnedTasks(event);
const taskWithdrawals = Object.freeze(
tasks.map((task) =>
this.#appendDisabledTask(event, task, committedAtMs),
),
);
this.#insertEvent(event);
this.#publishSnapshot(snapshot, committedAtMs);
const receipt = createPluginPackageWithdrawalReceipt({
eventDigest: event.eventDigest,
target: event.target,
capability: {
status: 'withdrawn',
taskWithdrawals,
previousActiveVectorDigest: projectToolDefinitionActiveVectorDigest(
event.target.projectId,
previousSources,
),
currentActiveVectorDigest: snapshot.activeVectorDigest,
currentToolSnapshotDigest: snapshot.snapshotDigest,
retainedSourceCount: snapshot.sources.length,
},
committedAtMs,
});
this.#insertReceipt(receipt);
await confirmAuthorization();
client.exec('COMMIT');
return Object.freeze({
status: 'created' as const,
receipt,
});
} catch (error) {
if (client.isTransaction) client.exec('ROLLBACK');
throw error;
}
});
}
}
@@ -0,0 +1,593 @@
import type { DatabaseSync } from 'node:sqlite';
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 {
compileLocalCommandTaskDefinition,
} from '@qinglong/runtime-core/task-definition-execution-compiler';
import {
BUILT_IN_COMMAND_TASK_SPEC_SCHEMA,
TaskSpecSemanticRegistry,
createBuiltInTaskSpecSemanticRegistry,
} from '@qinglong/runtime-core/task-spec-semantic';
import { LocalSqliteDispatchDefinitionStore } from '../task-definition/dispatchDefinitionStore';
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
type Row = Record<string, unknown>;
const SELECT_TASK_FIELDS = `
head."project_id" AS "projectId",
head."task_id" AS "taskId",
revision."revision" AS "revision",
revision."mutation_id" AS "mutationId",
revision."name" AS "name",
revision."description" AS "description",
revision."kind" AS "kind",
revision."spec_json" AS "specJson",
revision."labels_json" AS "labelsJson",
revision."enabled" AS "enabled",
revision."content_digest" AS "contentDigest",
head."created_at_ms" AS "createdAtMs",
revision."created_at_ms" AS "updatedAtMs"`;
function text(row: Row, key: string): string {
const value = row[key];
if (typeof value !== 'string') {
throw new PluginPackageTaskReconciliationUnavailableError();
}
return value;
}
function nullableText(row: Row, key: string): string | null {
const value = row[key];
if (value === null) return null;
return text(row, key);
}
function integer(row: Row, key: string): number {
const value = row[key];
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new PluginPackageTaskReconciliationUnavailableError();
}
return value as number;
}
function json(row: Row, key: string): unknown {
try {
return JSON.parse(text(row, key));
} catch {
throw new PluginPackageTaskReconciliationUnavailableError();
}
}
function taskRecord(row: Row): Readonly<TaskDefinitionRecord> {
try {
return normalizeTaskDefinitionRecord({
projectId: text(row, 'projectId'),
taskId: text(row, 'taskId'),
revision: integer(row, 'revision'),
mutationId: text(row, 'mutationId'),
name: text(row, 'name'),
...(row.description === null
? {}
: { description: text(row, 'description') }),
kind: text(row, 'kind') as TaskDefinitionRecord['kind'],
spec: json(row, 'specJson') as TaskDefinitionRecord['spec'],
labels: json(row, 'labelsJson') as TaskDefinitionRecord['labels'],
enabled: integer(row, 'enabled') === 1,
contentDigest: text(row, 'contentDigest'),
createdAtMs: integer(row, 'createdAtMs'),
updatedAtMs: integer(row, 'updatedAtMs'),
});
} catch (error) {
if (error instanceof PluginPackageTaskReconciliationUnavailableError) {
throw error;
}
throw new PluginPackageTaskReconciliationUnavailableError();
}
}
function mapStorageError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageTaskReconciliationError ||
error instanceof PluginPackageTaskReconciliationConflictError ||
error instanceof PluginPackageTaskReconciliationUnavailableError
) {
return error;
}
if (
error &&
typeof error === 'object' &&
'code' in error &&
typeof error.code === 'string' &&
error.code.startsWith('SQLITE_CONSTRAINT')
) {
return new PluginPackageTaskReconciliationConflictError(
'durable reconciliation identity is already bound',
);
}
return new PluginPackageTaskReconciliationUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
export class LocalSqlitePluginPackageTaskReconciliationRepository
implements
PluginPackageTaskReconciliationRepository,
PluginPackageTaskPublicationRecoverySource
{
readonly #authority: LocalSqliteOperationAuthority;
readonly #dispatchDefinitions: LocalSqliteDispatchDefinitionStore;
readonly #registry: TaskSpecSemanticRegistry;
constructor(
authority: LocalSqliteOperationAuthority | DatabaseSync,
registry = createBuiltInTaskSpecSemanticRegistry(),
) {
if (!(registry instanceof TaskSpecSemanticRegistry)) {
throw new TypeError('TaskSpec semantic registry is invalid');
}
this.#authority =
authority instanceof LocalSqliteOperationAuthority
? authority
: new LocalSqliteOperationAuthority(authority);
this.#dispatchDefinitions = new LocalSqliteDispatchDefinitionStore(
this.#authority.client,
);
this.#registry = registry;
}
#findStored(
generationDigest: string,
): Readonly<PluginPackageTaskReconciliationReceipt> | null {
const row = this.#authority.client
.prepare(
`SELECT receipt_json AS "receiptJson"
FROM "QingLong3PluginPackageTaskReconciliations"
WHERE generation_digest = ?`,
)
.get(generationDigest) as Row | undefined;
if (!row) return null;
try {
return normalizePluginPackageTaskReconciliationReceipt(
json(row, 'receiptJson') as PluginPackageTaskReconciliationReceipt,
);
} catch (error) {
if (error instanceof PluginPackageTaskReconciliationUnavailableError) {
throw error;
}
throw new PluginPackageTaskReconciliationUnavailableError();
}
}
#enqueue<T>(work: () => T | Promise<T>): Promise<T> {
return this.#authority.enqueue(
async () => {
try {
return await work();
} catch (error) {
throw mapStorageError(error);
}
},
() => new PluginPackageTaskReconciliationUnavailableError(),
);
}
find(
generationDigest: string,
): Promise<Readonly<PluginPackageTaskReconciliationReceipt> | null> {
if (typeof generationDigest !== 'string' || !/^[0-9a-f]{64}$/.test(generationDigest)) {
throw new InvalidPluginPackageTaskReconciliationError(
'generationDigest is invalid',
);
}
return this.#enqueue(() => this.#findStored(generationDigest));
}
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);
return this.#enqueue(() => {
const rows = this.#authority.client
.prepare(
`SELECT head.project_id AS "projectId",
head.package_name AS "packageName"
FROM "QingLong3PluginPackageInstallHeads" AS head
JOIN "QingLong3PluginPackageInstalls" AS install
ON install.installation_id = head.installation_id
LEFT JOIN "QingLong3PluginPackageTaskReconciliations" 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 (
? IS NULL OR head.project_id > ? OR
(head.project_id = ? AND head.package_name > ?)
)
ORDER BY head.project_id, head.package_name
LIMIT ?`,
)
.all(
after?.projectId ?? null,
after?.projectId ?? null,
after?.projectId ?? null,
after?.packageName ?? null,
options.limit + 1,
) as Row[];
const truncated = rows.length > options.limit;
const candidates = rows.slice(0, options.limit).map((row) =>
Object.freeze({
projectId: text(row, 'projectId'),
packageName: text(row, 'packageName'),
}),
);
const last = candidates.at(-1);
return Object.freeze({
candidates: Object.freeze(candidates),
truncated,
...(truncated && last
? {
next: Object.freeze({
projectId: last.projectId,
packageName: last.packageName,
}),
}
: {}),
});
});
}
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',
);
}
return this.#enqueue(async () => {
const client = this.#authority.client;
client.exec('BEGIN IMMEDIATE');
try {
const existing = this.#findStored(
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',
);
}
client.exec('COMMIT');
return Object.freeze({
status: 'existing' as const,
receipt: existing,
});
}
const materialized = client
.prepare(
`SELECT revision_digest AS "revisionDigest"
FROM "QingLong3PluginPackageMaterializedRevisions"
WHERE generation_digest = ?`,
)
.get(revision.generation.generationDigest) as Row | undefined;
if (
!materialized ||
text(materialized, 'revisionDigest') !== revision.revisionDigest
) {
throw new PluginPackageTaskReconciliationConflictError(
'materialized revision is not durably published',
);
}
const install = client
.prepare(
`SELECT
install.installation_id AS "installationId",
install.state AS "state",
install.target_generation AS "targetGeneration",
install.lock_digest AS "lockDigest",
install.previous_active_lock_digest AS "previousLockDigest"
FROM "QingLong3PluginPackageInstallHeads" AS head
JOIN "QingLong3PluginPackageInstalls" AS install
ON install.installation_id = head.installation_id
WHERE head.project_id = ? AND head.package_name = ?`,
)
.get(
revision.generation.projectId,
revision.generation.packageName,
) as Row | undefined;
if (
!install ||
text(install, 'installationId') !==
revision.generation.installationId ||
text(install, 'state') !== 'active' ||
integer(install, 'targetGeneration') !==
revision.generation.generation ||
text(install, 'lockDigest') !== revision.generation.lockDigest ||
nullableText(install, 'previousLockDigest') !==
revision.generation.previousActiveLockDigest
) {
throw new PluginPackageTaskReconciliationConflictError(
'Package install head is not the materialized generation',
);
}
const previous =
revision.generation.generation === 1
? null
: (() => {
const row = client
.prepare(
`SELECT receipt_json AS "receiptJson"
FROM "QingLong3PluginPackageTaskReconciliations"
WHERE project_id = ? AND package_name = ?
AND generation = ?`,
)
.get(
revision.generation.projectId,
revision.generation.packageName,
revision.generation.generation - 1,
) as Row | undefined;
return row
? normalizePluginPackageTaskReconciliationReceipt(
json(row, 'receiptJson') as PluginPackageTaskReconciliationReceipt,
)
: null;
})();
const taskIds = pluginPackageTaskReconciliationTaskIds(
revision,
previous,
this.#registry,
);
const facts: PluginPackageTaskOwnershipFact[] = taskIds.map((taskId) => {
const row = client
.prepare(
`SELECT
${SELECT_TASK_FIELDS},
ownership.package_name AS "ownerPackageName"
FROM (SELECT 1) AS seed
LEFT JOIN "QingLong3TaskDefinitions" AS head
ON head.project_id = ? AND head.task_id = ?
LEFT JOIN "QingLong3TaskDefinitionRevisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
LEFT JOIN "QingLong3PluginPackageTaskOwnerships" AS ownership
ON ownership.project_id = ? AND ownership.task_id = ?`,
)
.get(
revision.generation.projectId,
taskId,
revision.generation.projectId,
taskId,
) as Row;
return Object.freeze({
taskId,
packageName:
row.ownerPackageName === null
? null
: text(row, 'ownerPackageName'),
current: row.revision === null ? null : taskRecord(row),
});
});
const clock = client
.prepare(
`SELECT CAST(unixepoch('subsec') * 1000 AS INTEGER) AS "nowMs"`,
)
.get() as Row;
const plan = planPluginPackageTaskReconciliation({
revision,
previousReceipt: previous,
facts: Object.freeze(facts),
committedAtMs: integer(clock, 'nowMs'),
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',
);
}
for (const write of plan.writes) {
const definition = write.definition;
if (write.command.expectedRevision === null) {
client
.prepare(
`INSERT INTO "QingLong3TaskDefinitions" (
project_id, task_id, current_revision,
created_at_ms, updated_at_ms
) VALUES (?, ?, 1, ?, ?)`,
)
.run(
definition.projectId,
definition.taskId,
definition.createdAtMs,
definition.updatedAtMs,
);
client
.prepare(
`INSERT INTO "QingLong3PluginPackageTaskOwnerships" (
project_id, task_id, package_name,
claimed_generation_digest, created_at_ms
) VALUES (?, ?, ?, ?, ?)`,
)
.run(
definition.projectId,
definition.taskId,
revision.generation.packageName,
revision.generation.generationDigest,
plan.receipt.committedAtMs,
);
}
client
.prepare(
`INSERT INTO "QingLong3TaskDefinitionRevisions" (
project_id, task_id, revision, mutation_id, name,
description, kind, spec_json, labels_json, enabled,
content_digest, created_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
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 ? 1 : 0,
definition.contentDigest,
definition.updatedAtMs,
);
if (
definition.enabled &&
definition.kind === 'command' &&
definition.spec.schema === BUILT_IN_COMMAND_TASK_SPEC_SCHEMA
) {
this.#dispatchDefinitions.appendPlan(
compileLocalCommandTaskDefinition(definition, this.#registry),
);
}
if (write.command.expectedRevision !== null) {
const update = client
.prepare(
`UPDATE "QingLong3TaskDefinitions"
SET current_revision = ?, updated_at_ms = ?
WHERE project_id = ? AND task_id = ?
AND current_revision = ?`,
)
.run(
definition.revision,
definition.updatedAtMs,
definition.projectId,
definition.taskId,
write.command.expectedRevision,
);
if (update.changes !== 1) {
throw new PluginPackageTaskReconciliationConflictError(
'TaskDefinition head changed during reconciliation',
);
}
}
}
client
.prepare(
`INSERT INTO "QingLong3PluginPackageTaskReconciliations" (
generation_digest, project_id, package_name, generation,
materialized_revision_digest, lock_digest,
previous_lock_digest, receipt_digest, receipt_json,
committed_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
plan.receipt.generationDigest,
plan.receipt.projectId,
plan.receipt.packageName,
plan.receipt.generation,
plan.receipt.materializedRevisionDigest,
plan.receipt.lockDigest,
plan.receipt.previousLockDigest,
plan.receipt.receiptDigest,
JSON.stringify(plan.receipt),
plan.receipt.committedAtMs,
);
const insertItem = client.prepare(
`INSERT INTO "QingLong3PluginPackageTaskReconciliationItems" (
generation_digest, task_id, revision, disposition, content_digest
) VALUES (?, ?, ?, ?, ?)`,
);
for (const item of plan.receipt.items) {
insertItem.run(
plan.receipt.generationDigest,
item.taskId,
item.revision,
item.disposition,
item.contentDigest,
);
}
client.exec('COMMIT');
return Object.freeze({
status: 'created' as const,
receipt: plan.receipt,
});
} catch (error) {
if (client.isTransaction) client.exec('ROLLBACK');
throw error;
}
});
}
}
@@ -0,0 +1,890 @@
import type { DatabaseSync } from 'node:sqlite';
import {
normalizePluginPackageAutomationPublication,
type PluginPackageAutomationPublication,
} from '@qinglong/runtime-core/plugin-package-automation-publication';
import {
InvalidPluginPackageWorkflowAdministrationMutationError,
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError,
PluginPackageWorkflowAdministrationMutationConflictError,
} from '@qinglong/runtime-core/plugin-package-workflow-administration';
import {
createPluginPackageWorkflowAdmissionBundle,
InvalidPluginPackageWorkflowExecutionPlanError,
normalizePluginPackageWorkflowAdmissionReceipt,
normalizePluginPackageWorkflowExecutionPlan,
pluginPackageWorkflowDefinitionDigest,
PluginPackageWorkflowAdmissionConflictError,
PluginPackageWorkflowAdmissionNotAllowedError,
PluginPackageWorkflowAdmissionUnavailableError,
type PluginPackageWorkflowAdmissionBundle,
type PluginPackageWorkflowAdmissionReceipt,
type PluginPackageWorkflowAdmissionRepository,
type PluginPackageWorkflowExecutionPlan,
} from '@qinglong/runtime-core/plugin-package-workflow-execution-plan';
import {
normalizeStepRunRecord,
type StepRunRecord,
} from '@qinglong/runtime-core/step-run';
import { LocalSqliteOperationAuthority } from '../../authority/operationAuthority';
type Row = Record<string, unknown>;
const IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const WORKFLOW_RUN_STATUSES = new Set([
'running',
'succeeded',
'failed',
'cancelled',
'timed_out',
]);
function identity(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTITY.test(value)) {
throw new InvalidPluginPackageWorkflowExecutionPlanError(
`${label} is invalid`,
);
}
return value;
}
function text(row: Row, key: string): string {
const value = row[key];
if (typeof value !== 'string') {
throw new PluginPackageWorkflowAdmissionUnavailableError();
}
return value;
}
function integer(row: Row, key: string): number {
const value = row[key];
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new PluginPackageWorkflowAdmissionUnavailableError();
}
return value as number;
}
function sqliteConstraint(error: unknown): boolean {
if (!error || typeof error !== 'object') return false;
const code = (error as { code?: unknown }).code;
const errcode = (error as { errcode?: unknown }).errcode;
return (
(typeof code === 'string' &&
(code === 'ERR_SQLITE_CONSTRAINT' ||
code.startsWith('SQLITE_CONSTRAINT') ||
code.startsWith('ERR_SQLITE_CONSTRAINT'))) ||
(typeof errcode === 'number' && (errcode & 0xff) === 19)
);
}
function mapStorageError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageWorkflowAdministrationMutationError ||
error instanceof
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError ||
error instanceof PluginPackageWorkflowAdministrationMutationConflictError ||
error instanceof InvalidPluginPackageWorkflowExecutionPlanError ||
error instanceof PluginPackageWorkflowAdmissionConflictError ||
error instanceof PluginPackageWorkflowAdmissionNotAllowedError ||
error instanceof PluginPackageWorkflowAdmissionUnavailableError
) {
return error;
}
if (sqliteConstraint(error)) {
return new PluginPackageWorkflowAdmissionConflictError(
'durable Run, plan, StepRun, event, or receipt identity changed',
);
}
return new PluginPackageWorkflowAdmissionUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
export interface LocalSqlitePluginPackageWorkflowAdmissionTransactionContext {
readonly replay: boolean;
readonly plan: Readonly<PluginPackageWorkflowExecutionPlan>;
readonly receipt: Readonly<PluginPackageWorkflowAdmissionReceipt>;
}
export type LocalSqlitePluginPackageWorkflowAdmissionTransactionGuard = (
context: Readonly<LocalSqlitePluginPackageWorkflowAdmissionTransactionContext>,
) => void;
function canonicalJson(value: unknown): string {
return JSON.stringify(value);
}
function exactArray(
left: readonly string[],
right: readonly string[],
): boolean {
return (
left.length === right.length &&
left.every((value, index) => value === right[index])
);
}
function insertRun(
client: DatabaseSync,
bundle: Readonly<PluginPackageWorkflowAdmissionBundle>,
): void {
const run = bundle.run;
client
.prepare(
`INSERT INTO "Runs" (
id, project_id, task_id, task_revision, task_snapshot_ref,
trigger_type, execution_origin, execution_owner, request_id,
status, version, event_sequence, priority, idempotency_key,
created_at_ms, started_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
run.id,
run.projectId,
run.taskId,
run.taskRevision,
run.taskSnapshotRef ?? null,
run.triggerType,
run.executionOrigin,
run.executionOwner,
run.requestId ?? null,
run.status,
run.version,
run.eventSequence,
run.priority,
run.idempotencyKey ?? null,
run.createdAtMs,
run.startedAtMs ?? null,
);
}
function insertAdmissionEvent(
client: DatabaseSync,
bundle: Readonly<PluginPackageWorkflowAdmissionBundle>,
): void {
const event = bundle.admissionEvent;
client
.prepare(
`INSERT INTO "RunEvents" (
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
attempt_id, step_run_id, payload, created_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?)`,
)
.run(
event.id,
event.runId,
event.sequence,
event.type,
event.dedupeKey ?? null,
event.actorType,
event.actorId ?? null,
canonicalJson(event.payload),
event.createdAtMs,
);
}
function insertStepEvidence(
client: DatabaseSync,
bundle: Readonly<PluginPackageWorkflowAdmissionBundle>,
): void {
for (const mutation of bundle.stepMutations) {
const stepRun = mutation.stepRun;
const event = mutation.event;
client
.prepare(
`INSERT INTO "StepRuns" (
id, run_id, parent_step_run_id, step_key, kind, definition_ref,
definition_digest, required, status, version, attempt_count,
input_ref, output_ref, approval_request_id, ready_at_ms,
started_at_ms, finished_at_ms, result_code, error_summary,
created_at_ms, updated_at_ms, last_mutation_id, step_run_digest,
step_run_json
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?
)`,
)
.run(
stepRun.id,
stepRun.runId,
stepRun.parentStepRunId,
stepRun.stepKey,
stepRun.kind,
stepRun.definitionRef,
stepRun.definitionDigest,
stepRun.required ? 1 : 0,
stepRun.status,
stepRun.version,
stepRun.attemptCount,
stepRun.inputRef,
stepRun.outputRef,
stepRun.approvalRequestId,
stepRun.readyAtMs,
stepRun.startedAtMs,
stepRun.finishedAtMs,
stepRun.resultCode,
stepRun.errorSummary,
stepRun.createdAtMs,
stepRun.updatedAtMs,
stepRun.lastMutationId,
stepRun.stepRunDigest,
canonicalJson(stepRun),
);
client
.prepare(
`INSERT INTO "RunEvents" (
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
attempt_id, step_run_id, payload, created_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)`,
)
.run(
event.id,
event.runId,
event.sequence,
event.type,
event.dedupeKey ?? null,
event.actorType,
event.actorId ?? null,
stepRun.id,
canonicalJson(event.payload),
event.createdAtMs,
);
client
.prepare(
`INSERT INTO "StepRunMutations" (
mutation_id, mutation_digest, run_id, step_run_id,
step_run_digest, event_id, event_sequence, run_version,
step_run_json, committed_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
mutation.mutationId,
mutation.mutationDigest,
mutation.runId,
stepRun.id,
stepRun.stepRunDigest,
event.id,
event.sequence,
mutation.expectedRunVersion + 1,
canonicalJson(stepRun),
bundle.receipt.admittedAtMs,
);
}
}
function insertAdmission(
client: DatabaseSync,
bundle: Readonly<PluginPackageWorkflowAdmissionBundle>,
): void {
const { plan, receipt } = bundle;
const target = plan.target;
client
.prepare(
`INSERT INTO "QingLong3PluginPackageWorkflowAdmissions" (
plan_digest, plan_id, run_id, project_id, package_name,
installation_id, lock_digest, generation, generation_digest,
materialized_revision_digest, publication_digest, workflow_id,
workflow_definition_digest, step_count, admitted_at_ms,
final_run_version, final_run_event_sequence, receipt_digest,
plan_json, receipt_json
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)`,
)
.run(
plan.planDigest,
plan.planId,
plan.runId,
target.projectId,
target.packageName,
target.installationId,
target.lockDigest,
target.generation,
target.generationDigest,
target.materializedRevisionDigest,
target.publicationDigest,
target.workflowId,
target.workflowDefinitionDigest,
plan.steps.length,
receipt.admittedAtMs,
receipt.finalRunVersion,
receipt.finalRunEventSequence,
receipt.receiptDigest,
canonicalJson(plan),
canonicalJson(receipt),
);
for (const step of plan.steps) {
const mutation = bundle.stepMutations.find(
(candidate) => candidate.stepRun.stepKey === step.stepKey,
);
if (!mutation) {
throw new PluginPackageWorkflowAdmissionUnavailableError();
}
client
.prepare(
`INSERT INTO "QingLong3PluginPackageWorkflowAdmissionSteps" (
plan_digest, run_id, step_key, step_run_id, task_id,
task_definition_ref, task_definition_digest, needs_json,
initial_status, mutation_id, event_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
plan.planDigest,
plan.runId,
step.stepKey,
step.stepRunId,
step.taskId,
step.taskDefinitionRef,
step.taskDefinitionDigest,
canonicalJson(step.needs),
step.initialStatus,
mutation.mutationId,
mutation.event.id,
);
}
}
export class LocalSqlitePluginPackageWorkflowAdmissionRepository
implements PluginPackageWorkflowAdmissionRepository
{
readonly #authority: LocalSqliteOperationAuthority;
constructor(authority: LocalSqliteOperationAuthority | DatabaseSync) {
this.#authority =
authority instanceof LocalSqliteOperationAuthority
? authority
: new LocalSqliteOperationAuthority(authority);
}
#enqueue<T>(work: () => T | Promise<T>): Promise<T> {
return this.#authority.enqueue(
async () => {
try {
return await work();
} catch (error) {
throw mapStorageError(error);
}
},
() => new PluginPackageWorkflowAdmissionUnavailableError(),
);
}
#select(where: string, value: string): Row | undefined {
return this.#authority.client
.prepare(
`SELECT
plan_digest AS "planDigest", plan_id AS "planId",
run_id AS "runId", 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",
publication_digest AS "publicationDigest",
workflow_id AS "workflowId",
workflow_definition_digest AS "workflowDefinitionDigest",
step_count AS "stepCount", admitted_at_ms AS "admittedAtMs",
final_run_version AS "finalRunVersion",
final_run_event_sequence AS "finalRunEventSequence",
receipt_digest AS "receiptDigest", plan_json AS "planJson",
receipt_json AS "receiptJson"
FROM "QingLong3PluginPackageWorkflowAdmissions"
WHERE ${where} LIMIT 2`,
)
.get(value) as Row | undefined;
}
#parse(row: Row): Readonly<{
plan: Readonly<PluginPackageWorkflowExecutionPlan>;
receipt: Readonly<PluginPackageWorkflowAdmissionReceipt>;
bundle: Readonly<PluginPackageWorkflowAdmissionBundle>;
}> {
try {
const plan = normalizePluginPackageWorkflowExecutionPlan(
JSON.parse(text(row, 'planJson')) as PluginPackageWorkflowExecutionPlan,
);
const receipt = normalizePluginPackageWorkflowAdmissionReceipt(
JSON.parse(
text(row, 'receiptJson'),
) as PluginPackageWorkflowAdmissionReceipt,
);
const bundle = createPluginPackageWorkflowAdmissionBundle(plan);
if (
plan.planDigest !== text(row, 'planDigest') ||
plan.planId !== text(row, 'planId') ||
plan.runId !== text(row, 'runId') ||
plan.target.projectId !== text(row, 'projectId') ||
plan.target.packageName !== text(row, 'packageName') ||
plan.target.installationId !== text(row, 'installationId') ||
plan.target.lockDigest !== text(row, 'lockDigest') ||
plan.target.generation !== integer(row, 'generation') ||
plan.target.generationDigest !== text(row, 'generationDigest') ||
plan.target.materializedRevisionDigest !==
text(row, 'materializedRevisionDigest') ||
plan.target.publicationDigest !== text(row, 'publicationDigest') ||
plan.target.workflowId !== text(row, 'workflowId') ||
plan.target.workflowDefinitionDigest !==
text(row, 'workflowDefinitionDigest') ||
plan.steps.length !== integer(row, 'stepCount') ||
receipt.admittedAtMs !== integer(row, 'admittedAtMs') ||
receipt.finalRunVersion !== integer(row, 'finalRunVersion') ||
receipt.finalRunEventSequence !==
integer(row, 'finalRunEventSequence') ||
receipt.receiptDigest !== text(row, 'receiptDigest') ||
canonicalJson(bundle.receipt) !== canonicalJson(receipt)
) {
throw new PluginPackageWorkflowAdmissionUnavailableError();
}
this.#assertStoredEvidence(bundle);
return Object.freeze({ plan, receipt, bundle });
} catch (error) {
if (error instanceof PluginPackageWorkflowAdmissionUnavailableError) {
throw error;
}
throw new PluginPackageWorkflowAdmissionUnavailableError();
}
}
#assertStoredEvidence(
bundle: Readonly<PluginPackageWorkflowAdmissionBundle>,
): void {
const run = bundle.run;
const storedRun = this.#authority.client
.prepare(
`SELECT project_id AS "projectId", task_id AS "taskId",
task_revision AS "taskRevision",
task_snapshot_ref AS "taskSnapshotRef",
trigger_type AS "triggerType",
execution_origin AS "executionOrigin",
execution_owner AS "executionOwner",
request_id AS "requestId", status, version,
event_sequence AS "eventSequence", priority,
idempotency_key AS "idempotencyKey",
created_at_ms AS "createdAtMs",
started_at_ms AS "startedAtMs"
FROM "Runs" WHERE id = ?`,
)
.get(run.id) as Row | undefined;
const storedRunVersion = storedRun ? integer(storedRun, 'version') : -1;
const storedEventSequence = storedRun
? integer(storedRun, 'eventSequence')
: -1;
if (
!storedRun ||
text(storedRun, 'projectId') !== run.projectId ||
text(storedRun, 'taskId') !== run.taskId ||
text(storedRun, 'taskRevision') !== run.taskRevision ||
text(storedRun, 'taskSnapshotRef') !== run.taskSnapshotRef ||
text(storedRun, 'triggerType') !== run.triggerType ||
text(storedRun, 'executionOrigin') !== run.executionOrigin ||
text(storedRun, 'executionOwner') !== run.executionOwner ||
text(storedRun, 'requestId') !== run.requestId ||
!WORKFLOW_RUN_STATUSES.has(text(storedRun, 'status')) ||
storedRunVersion < run.version ||
storedEventSequence < run.eventSequence ||
storedRunVersion !== storedEventSequence ||
integer(storedRun, 'priority') !== run.priority ||
text(storedRun, 'idempotencyKey') !== run.idempotencyKey ||
integer(storedRun, 'createdAtMs') !== run.createdAtMs ||
integer(storedRun, 'startedAtMs') !== run.startedAtMs
) {
throw new PluginPackageWorkflowAdmissionUnavailableError();
}
const events = this.#authority.client
.prepare(
`SELECT id, sequence, type, dedupe_key AS "dedupeKey",
actor_type AS "actorType", actor_id AS "actorId",
step_run_id AS "stepRunId", payload,
created_at_ms AS "createdAtMs"
FROM "RunEvents"
WHERE run_id = ? AND sequence <= ?
ORDER BY sequence`,
)
.all(run.id, bundle.receipt.finalRunEventSequence) as Row[];
const expectedEvents = [
bundle.admissionEvent,
...bundle.stepMutations.map(({ event }) => event),
];
if (
events.length !== expectedEvents.length ||
events.some((row, index) => {
const event = expectedEvents[index]!;
return (
text(row, 'id') !== event.id ||
integer(row, 'sequence') !== event.sequence ||
text(row, 'type') !== event.type ||
text(row, 'dedupeKey') !== event.dedupeKey ||
text(row, 'actorType') !== event.actorType ||
(row.actorId === null ? undefined : row.actorId) !== event.actorId ||
(row.stepRunId === null ? undefined : row.stepRunId) !==
event.stepRunId ||
text(row, 'payload') !== canonicalJson(event.payload) ||
integer(row, 'createdAtMs') !== event.createdAtMs
);
})
) {
throw new PluginPackageWorkflowAdmissionUnavailableError();
}
for (const mutation of bundle.stepMutations) {
const row = this.#authority.client
.prepare(
`SELECT
step.step_key AS "stepKey", step.step_run_id AS "stepRunId",
step.task_id AS "taskId",
step.task_definition_ref AS "taskDefinitionRef",
step.task_definition_digest AS "taskDefinitionDigest",
step.needs_json AS "needsJson",
step.initial_status AS "initialStatus",
step.mutation_id AS "mutationId", step.event_id AS "eventId",
runtime.step_key AS "currentStepKey",
runtime.kind AS "currentKind",
runtime.definition_ref AS "currentDefinitionRef",
runtime.definition_digest AS "currentDefinitionDigest",
runtime.required AS "currentRequired",
runtime.status AS "currentStatus",
runtime.version AS "currentVersion",
runtime.last_mutation_id AS "currentLastMutationId",
runtime.step_run_digest AS "currentStepRunDigest",
runtime.step_run_json AS "currentStepRunJson",
mutation.mutation_digest AS "mutationDigest",
mutation.event_sequence AS "eventSequence",
mutation.run_version AS "runVersion",
mutation.step_run_digest AS "initialStepRunDigest",
mutation.step_run_json AS "initialStepRunJson"
FROM "QingLong3PluginPackageWorkflowAdmissionSteps" AS step
JOIN "StepRuns" AS runtime
ON runtime.run_id = step.run_id
AND runtime.id = step.step_run_id
JOIN "StepRunMutations" AS mutation
ON mutation.mutation_id = step.mutation_id
WHERE step.plan_digest = ? AND step.step_key = ?`,
)
.get(bundle.plan.planDigest, mutation.stepRun.stepKey) as
| Row
| undefined;
const planStep = bundle.plan.steps.find(
({ stepKey }) => stepKey === mutation.stepRun.stepKey,
);
let currentStepRun: Readonly<StepRunRecord> | null = null;
if (row) {
try {
currentStepRun = normalizeStepRunRecord(
JSON.parse(text(row, 'currentStepRunJson')) as StepRunRecord,
);
} catch {
throw new PluginPackageWorkflowAdmissionUnavailableError();
}
}
if (
!row ||
!planStep ||
!currentStepRun ||
text(row, 'stepRunId') !== mutation.stepRun.id ||
text(row, 'taskId') !== planStep.taskId ||
text(row, 'taskDefinitionRef') !== planStep.taskDefinitionRef ||
text(row, 'taskDefinitionDigest') !== planStep.taskDefinitionDigest ||
text(row, 'needsJson') !== canonicalJson(planStep.needs) ||
text(row, 'initialStatus') !== planStep.initialStatus ||
text(row, 'mutationId') !== mutation.mutationId ||
text(row, 'eventId') !== mutation.event.id ||
text(row, 'mutationDigest') !== mutation.mutationDigest ||
integer(row, 'eventSequence') !== mutation.event.sequence ||
integer(row, 'runVersion') !== mutation.expectedRunVersion + 1 ||
text(row, 'initialStepRunDigest') !== mutation.stepRun.stepRunDigest ||
text(row, 'initialStepRunJson') !== canonicalJson(mutation.stepRun) ||
currentStepRun.id !== mutation.stepRun.id ||
currentStepRun.runId !== mutation.runId ||
currentStepRun.stepKey !== planStep.stepKey ||
currentStepRun.kind !== 'task' ||
currentStepRun.definitionRef !== planStep.taskDefinitionRef ||
currentStepRun.definitionDigest !== planStep.taskDefinitionDigest ||
currentStepRun.required !== planStep.required ||
currentStepRun.version < mutation.stepRun.version ||
text(row, 'currentStepKey') !== currentStepRun.stepKey ||
text(row, 'currentKind') !== currentStepRun.kind ||
text(row, 'currentDefinitionRef') !== currentStepRun.definitionRef ||
text(row, 'currentDefinitionDigest') !==
currentStepRun.definitionDigest ||
integer(row, 'currentRequired') !== (currentStepRun.required ? 1 : 0) ||
text(row, 'currentStatus') !== currentStepRun.status ||
integer(row, 'currentVersion') !== currentStepRun.version ||
text(row, 'currentLastMutationId') !== currentStepRun.lastMutationId ||
text(row, 'currentStepRunDigest') !== currentStepRun.stepRunDigest
) {
throw new PluginPackageWorkflowAdmissionUnavailableError();
}
}
}
#find(
where: string,
value: string,
): Readonly<{
plan: Readonly<PluginPackageWorkflowExecutionPlan>;
receipt: Readonly<PluginPackageWorkflowAdmissionReceipt>;
bundle: Readonly<PluginPackageWorkflowAdmissionBundle>;
}> | null {
const row = this.#select(where, value);
return row ? this.#parse(row) : null;
}
#assertCurrentTarget(
plan: Readonly<PluginPackageWorkflowExecutionPlan>,
): void {
const guard = this.#authority.client
.prepare(
`SELECT publication.publication_json AS "publicationJson"
FROM "QingLong3PluginPackageAutomationPublicationHeads" AS head
JOIN "QingLong3PluginPackageAutomationPublications" AS publication
ON publication.publication_digest = head.publication_digest
JOIN "QingLong3PluginPackageInstallHeads" AS install_head
ON install_head.project_id = publication.project_id
AND install_head.package_name = publication.package_name
AND install_head.installation_id = publication.installation_id
JOIN "QingLong3PluginPackageInstalls" AS install
ON install.installation_id = install_head.installation_id
AND install.lock_digest = publication.lock_digest
LEFT JOIN "QingLong3PluginPackageLifecycleHeads" AS lifecycle
ON lifecycle.project_id = publication.project_id
AND lifecycle.package_name = publication.package_name
WHERE head.project_id = ?
AND head.package_name = ?
AND head.publication_digest = ?
AND publication.state = 'active'
AND install.state = 'active'
AND install.active_lock_digest = publication.lock_digest
AND (
lifecycle.event_digest IS NULL OR
lifecycle.disposition = 'active'
)
AND NOT EXISTS (
SELECT 1
FROM "QingLong3PluginPackageQuarantineEvents" AS quarantine
WHERE quarantine.project_id = publication.project_id
AND quarantine.package_name = publication.package_name
AND quarantine.installation_id = publication.installation_id
AND quarantine.lock_digest = publication.lock_digest
)`,
)
.get(
plan.target.projectId,
plan.target.packageName,
plan.target.publicationDigest,
) as Row | undefined;
if (!guard) throw new PluginPackageWorkflowAdmissionNotAllowedError();
let publication: Readonly<PluginPackageAutomationPublication>;
try {
publication = normalizePluginPackageAutomationPublication(
JSON.parse(
text(guard, 'publicationJson'),
) as PluginPackageAutomationPublication,
);
} catch {
throw new PluginPackageWorkflowAdmissionUnavailableError();
}
const target = plan.target;
const workflow = publication.definitions.workflows.find(
({ id }) => id === target.workflowId,
);
if (
publication.publicationDigest !== target.publicationDigest ||
publication.target.projectId !== target.projectId ||
publication.target.packageName !== target.packageName ||
publication.target.installationId !== target.installationId ||
publication.target.lockDigest !== target.lockDigest ||
publication.target.generation !== target.generation ||
publication.target.generationDigest !== target.generationDigest ||
publication.target.materializedRevisionDigest !==
target.materializedRevisionDigest ||
publication.state !== 'active' ||
!workflow ||
!workflow.enabled ||
pluginPackageWorkflowDefinitionDigest(workflow) !==
target.workflowDefinitionDigest ||
workflow.steps.length !== plan.steps.length
) {
throw new PluginPackageWorkflowAdmissionConflictError(
'the exact Workflow publication drifted',
);
}
const revision = this.#authority.client
.prepare(
`SELECT revision_json AS "revisionJson"
FROM "QingLong3PluginPackageMaterializedRevisions"
WHERE generation_digest = ?
AND project_id = ?
AND package_name = ?
AND generation = ?
AND lock_digest = ?
AND revision_digest = ?`,
)
.get(
target.generationDigest,
target.projectId,
target.packageName,
target.generation,
target.lockDigest,
target.materializedRevisionDigest,
) as Row | undefined;
if (!revision) {
throw new PluginPackageWorkflowAdmissionConflictError(
'the exact materialized revision is absent',
);
}
let resources: unknown;
try {
const parsed = JSON.parse(text(revision, 'revisionJson')) as {
resources?: unknown;
};
resources = parsed.resources;
} catch {
throw new PluginPackageWorkflowAdmissionUnavailableError();
}
if (!Array.isArray(resources)) {
throw new PluginPackageWorkflowAdmissionUnavailableError();
}
for (const step of plan.steps) {
const workflowStep = workflow.steps.find(({ id }) => id === step.stepKey);
const matches = resources.filter((resource) => {
if (!resource || typeof resource !== 'object') return false;
const candidate = resource as {
kind?: unknown;
sourceDigest?: unknown;
value?: {
id?: unknown;
enabled?: unknown;
};
};
return (
candidate.kind === 'task' &&
candidate.sourceDigest === step.taskDefinitionDigest &&
candidate.value?.id === step.taskId &&
candidate.value.enabled === true
);
});
if (
!workflowStep ||
workflowStep.task !== step.taskId ||
!exactArray(workflowStep.needs, step.needs) ||
step.initialStatus !==
(workflowStep.needs.length === 0 ? 'ready' : 'pending') ||
step.taskDefinitionRef !==
`plugin-package:${target.materializedRevisionDigest}:task:${step.taskId}` ||
matches.length !== 1
) {
throw new PluginPackageWorkflowAdmissionConflictError(
'the exact Workflow step or Task evidence drifted',
);
}
}
}
findByPlanId(
planIdValue: string,
): Promise<Readonly<PluginPackageWorkflowAdmissionReceipt> | null> {
const planId = identity(planIdValue, 'planId');
return this.#enqueue(
() => this.#find('plan_id = ?', planId)?.receipt ?? null,
);
}
findByRunId(
runIdValue: string,
): Promise<Readonly<PluginPackageWorkflowAdmissionReceipt> | null> {
const runId = identity(runIdValue, 'runId');
return this.#enqueue(
() => this.#find('run_id = ?', runId)?.receipt ?? null,
);
}
findPlanByPlanId(
planIdValue: string,
): Promise<Readonly<PluginPackageWorkflowExecutionPlan> | null> {
const planId = identity(planIdValue, 'planId');
return this.#enqueue(() => this.#find('plan_id = ?', planId)?.plan ?? null);
}
admit(
planValue: Readonly<PluginPackageWorkflowExecutionPlan>,
transactionGuard?: LocalSqlitePluginPackageWorkflowAdmissionTransactionGuard,
): Promise<
Readonly<{
status: 'created' | 'existing';
receipt: Readonly<PluginPackageWorkflowAdmissionReceipt>;
}>
> {
if (
transactionGuard !== undefined &&
typeof transactionGuard !== 'function'
) {
return Promise.reject(
new InvalidPluginPackageWorkflowExecutionPlanError(
'transaction guard is invalid',
),
);
}
const plan = normalizePluginPackageWorkflowExecutionPlan(planValue);
const bundle = createPluginPackageWorkflowAdmissionBundle(plan);
return this.#enqueue(() => {
let began = false;
try {
this.#authority.client.exec('BEGIN IMMEDIATE');
began = true;
const existing = this.#find('plan_id = ?', plan.planId);
if (existing) {
if (
existing.plan.planDigest !== plan.planDigest ||
canonicalJson(existing.plan) !== canonicalJson(plan)
) {
throw new PluginPackageWorkflowAdmissionConflictError(
'planId is already bound to another plan',
);
}
transactionGuard?.(
Object.freeze({
replay: true,
plan: existing.plan,
receipt: existing.receipt,
}),
);
this.#authority.client.exec('COMMIT');
began = false;
return Object.freeze({
status: 'existing' as const,
receipt: existing.receipt,
});
}
transactionGuard?.(
Object.freeze({
replay: false,
plan,
receipt: bundle.receipt,
}),
);
this.#assertCurrentTarget(plan);
insertRun(this.#authority.client, bundle);
insertAdmissionEvent(this.#authority.client, bundle);
insertStepEvidence(this.#authority.client, bundle);
insertAdmission(this.#authority.client, bundle);
this.#authority.client.exec('COMMIT');
began = false;
return Object.freeze({
status: 'created' as const,
receipt: bundle.receipt,
});
} finally {
if (began) {
try {
this.#authority.client.exec('ROLLBACK');
} catch {
// Preserve the original fail-closed error.
}
}
}
});
}
}
@@ -0,0 +1,488 @@
import type { DatabaseSync } from 'node:sqlite';
import type {
RunAttemptRecord,
RunEventRecord,
RunRecord,
} from '@qinglong/runtime-core';
import {
ClusterRunCancellationConvergenceUnavailableError,
normalizeClusterRunCancellationConvergencePageCommand,
normalizeClusterRunCancellationConvergencePageResult,
type ClusterRunCancellationConvergencePageCommand,
type ClusterRunCancellationConvergencePageResult,
type ClusterRunCancellationConvergenceRepository,
} from '@qinglong/runtime-core/cluster-run-cancellation-convergence';
import {
resolvePluginPackageWorkflowCancellation,
type PluginPackageWorkflowCancellationActiveAttempt,
} from '@qinglong/runtime-core/plugin-package-workflow-cancellation-convergence';
import type {
PluginPackageWorkflowTaskAttemptAdmissionReceipt,
} from '@qinglong/runtime-core/plugin-package-workflow-task-attempt-admission';
import {
normalizeStepRunRecord,
type StepRunMutation,
type StepRunRecord,
} from '@qinglong/runtime-core/step-run';
import { LocalSqliteOperationAuthority } from '../../authority/operationAuthority';
type Row = Record<string, unknown>;
function unavailable(
cause?: unknown,
): ClusterRunCancellationConvergenceUnavailableError {
return new ClusterRunCancellationConvergenceUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function text(row: Row, key: string): string {
const value = row[key];
if (typeof value !== 'string' || value.length < 1) throw unavailable();
return value;
}
function integer(row: Row, key: string): number {
const value = row[key];
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw unavailable();
}
return value as number;
}
function optionalInteger(row: Row, key: string): number | undefined {
return row[key] === null || row[key] === undefined
? undefined
: integer(row, key);
}
function json(row: Row, key: string): Readonly<Record<string, unknown>> {
try {
const value = JSON.parse(text(row, key)) as unknown;
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw unavailable();
}
return Object.freeze({ ...(value as Record<string, unknown>) });
} catch (error) {
if (error instanceof ClusterRunCancellationConvergenceUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function runFromRow(row: Row): Readonly<RunRecord> {
return Object.freeze({
id: text(row, 'id'),
projectId: text(row, 'projectId'),
taskId: text(row, 'taskId'),
taskRevision: text(row, 'taskRevision'),
triggerType: text(row, 'triggerType'),
executionOrigin: text(
row,
'executionOrigin',
) as RunRecord['executionOrigin'],
executionOwner: text(
row,
'executionOwner',
) as RunRecord['executionOwner'],
requestId: text(row, 'requestId'),
status: text(row, 'status') as RunRecord['status'],
version: integer(row, 'version'),
eventSequence: integer(row, 'eventSequence'),
priority: integer(row, 'priority'),
idempotencyKey: text(row, 'idempotencyKey'),
createdAtMs: integer(row, 'createdAtMs'),
...(optionalInteger(row, 'startedAtMs') === undefined
? {}
: { startedAtMs: optionalInteger(row, 'startedAtMs')! }),
cancelRequestedAtMs: integer(row, 'cancelRequestedAtMs'),
cancelReason: text(
row,
'cancelReason',
) as NonNullable<RunRecord['cancelReason']>,
});
}
function attemptFromRow(row: Row): Readonly<RunAttemptRecord> {
return Object.freeze({
id: text(row, 'attemptId'),
runId: text(row, 'attemptRunId'),
stepRunId: text(row, 'attemptStepRunId'),
attempt: integer(row, 'attemptNumber'),
status: text(row, 'attemptStatus') as RunAttemptRecord['status'],
executorType: text(row, 'executorType'),
callbackSequence: integer(row, 'callbackSequence'),
createdAtMs: integer(row, 'attemptCreatedAtMs'),
...(optionalInteger(row, 'attemptStartedAtMs') === undefined
? {}
: {
startedAtMs: optionalInteger(row, 'attemptStartedAtMs')!,
}),
});
}
function insertEvent(client: DatabaseSync, event: Readonly<RunEventRecord>): void {
client
.prepare(
`INSERT INTO "RunEvents" (
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
attempt_id, step_run_id, payload, created_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
event.id,
event.runId,
event.sequence,
event.type,
event.dedupeKey ?? null,
event.actorType,
event.actorId ?? null,
event.attemptId ?? null,
event.stepRunId ?? null,
JSON.stringify(event.payload),
event.createdAtMs,
);
}
function updateStepRun(
client: DatabaseSync,
mutation: Readonly<StepRunMutation>,
): void {
const stepRun = mutation.stepRun;
const updated = client
.prepare(
`UPDATE "StepRuns"
SET status = ?, version = ?, attempt_count = ?, output_ref = ?,
approval_request_id = ?, ready_at_ms = ?, started_at_ms = ?,
finished_at_ms = ?, result_code = ?, error_summary = ?,
updated_at_ms = ?, last_mutation_id = ?, step_run_digest = ?,
step_run_json = ?
WHERE id = ? AND run_id = ? AND version = ?
AND step_run_digest = ? AND status = ?`,
)
.run(
stepRun.status,
stepRun.version,
stepRun.attemptCount,
stepRun.outputRef,
stepRun.approvalRequestId,
stepRun.readyAtMs,
stepRun.startedAtMs,
stepRun.finishedAtMs,
stepRun.resultCode,
stepRun.errorSummary,
stepRun.updatedAtMs,
stepRun.lastMutationId,
stepRun.stepRunDigest,
JSON.stringify(stepRun),
stepRun.id,
stepRun.runId,
mutation.expectedStepRunVersion,
mutation.expectedStepRunDigest,
mutation.previousStatus,
);
if (updated.changes !== 1) throw unavailable();
}
function insertStepMutation(
client: DatabaseSync,
mutation: Readonly<StepRunMutation>,
committedAtMs: number,
): void {
insertEvent(client, mutation.event);
client
.prepare(
`INSERT INTO "StepRunMutations" (
mutation_id, mutation_digest, run_id, step_run_id,
step_run_digest, event_id, event_sequence, run_version,
step_run_json, committed_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
mutation.mutationId,
mutation.mutationDigest,
mutation.runId,
mutation.stepRun.id,
mutation.stepRun.stepRunDigest,
mutation.event.id,
mutation.event.sequence,
mutation.expectedRunVersion + 1,
JSON.stringify(mutation.stepRun),
committedAtMs,
);
}
/**
* Edge/standalone adapter for the shared Workflow cancellation state machine.
* It uses the existing single SQLite operation authority and one
* `BEGIN IMMEDIATE` transaction per Workflow, so page size does not lengthen a
* single write lock on low-memory router-class devices.
*/
export class LocalSqlitePluginPackageWorkflowCancellationConvergenceRepository
implements ClusterRunCancellationConvergenceRepository
{
readonly #authority: LocalSqliteOperationAuthority;
constructor(authority: LocalSqliteOperationAuthority | DatabaseSync) {
this.#authority =
authority instanceof LocalSqliteOperationAuthority
? authority
: new LocalSqliteOperationAuthority(authority);
}
convergePage(
value: Readonly<ClusterRunCancellationConvergencePageCommand>,
): Promise<Readonly<ClusterRunCancellationConvergencePageResult>> {
const command =
normalizeClusterRunCancellationConvergencePageCommand(value);
return this.#authority.enqueue(
async () => {
try {
const client = this.#authority.client;
const candidates = client
.prepare(
`SELECT id AS "runId"
FROM "Runs"
WHERE execution_owner = 'runtime'
AND trigger_type = 'plugin_package_workflow'
AND status = 'running'
AND cancel_requested_at_ms IS NOT NULL
ORDER BY cancel_requested_at_ms, id
LIMIT ?`,
)
.all(command.limit) as Row[];
let settledRuns = 0;
let settledAttempts = 0;
let blocked = 0;
for (const candidate of candidates) {
const result = this.#convergeOne(text(candidate, 'runId'));
settledRuns += result.settledRuns;
settledAttempts += result.settledAttempts;
blocked += result.blocked;
}
const continuation = client
.prepare(
`SELECT EXISTS (
SELECT 1 FROM "Runs"
WHERE execution_owner = 'runtime'
AND trigger_type = 'plugin_package_workflow'
AND status = 'running'
AND cancel_requested_at_ms IS NOT NULL
LIMIT 1
) AS "hasMore"`,
)
.get() as Row | undefined;
if (!continuation) throw unavailable();
return normalizeClusterRunCancellationConvergencePageResult({
scanned: candidates.length,
settledRuns,
settledAttempts,
blocked,
hasMore: integer(continuation, 'hasMore') === 1,
}, command.limit);
} catch (error) {
if (
error instanceof
ClusterRunCancellationConvergenceUnavailableError
) {
throw error;
}
throw unavailable(error);
}
},
() => unavailable(),
);
}
#convergeOne(runId: string): Readonly<{
settledRuns: number;
settledAttempts: number;
blocked: number;
}> {
const client = this.#authority.client;
let began = false;
try {
client.exec('BEGIN IMMEDIATE');
began = true;
const runRows = client
.prepare(
`SELECT id, project_id AS "projectId", task_id AS "taskId",
task_revision AS "taskRevision",
trigger_type AS "triggerType",
execution_origin AS "executionOrigin",
execution_owner AS "executionOwner",
request_id AS "requestId", status, version,
event_sequence AS "eventSequence", priority,
idempotency_key AS "idempotencyKey",
created_at_ms AS "createdAtMs",
started_at_ms AS "startedAtMs",
cancel_requested_at_ms AS "cancelRequestedAtMs",
cancel_reason AS "cancelReason"
FROM "Runs"
WHERE id = ? AND execution_owner = 'runtime'
AND trigger_type = 'plugin_package_workflow'
AND status = 'running'
AND cancel_requested_at_ms IS NOT NULL
LIMIT 2`,
)
.all(runId) as Row[];
if (runRows.length === 0) {
client.exec('COMMIT');
began = false;
return Object.freeze({
settledRuns: 0,
settledAttempts: 0,
blocked: 0,
});
}
if (runRows.length !== 1) throw unavailable();
const run = runFromRow(runRows[0]!);
const stepRuns = (
client
.prepare(
`SELECT step_run_json AS "stepRunJson"
FROM "StepRuns"
WHERE run_id = ?
ORDER BY step_key, id`,
)
.all(runId) as Row[]
).map((row) => {
try {
return normalizeStepRunRecord(
json(row, 'stepRunJson') as unknown as StepRunRecord,
);
} catch {
throw unavailable();
}
});
const activeRows = client
.prepare(
`SELECT admission.receipt_json AS "admissionJson",
attempt.id AS "attemptId",
attempt.run_id AS "attemptRunId",
attempt.step_run_id AS "attemptStepRunId",
attempt.attempt AS "attemptNumber",
attempt.status AS "attemptStatus",
attempt.executor_type AS "executorType",
attempt.callback_sequence AS "callbackSequence",
attempt.created_at_ms AS "attemptCreatedAtMs",
attempt.started_at_ms AS "attemptStartedAtMs"
FROM
"QingLong3PluginPackageWorkflowTaskAttemptAdmissions"
AS admission
JOIN "RunAttempts" AS attempt
ON attempt.id = admission.attempt_id
AND attempt.run_id = admission.run_id
WHERE admission.run_id = ?
AND attempt.status IN ('claimed', 'starting', 'running')
ORDER BY attempt.id`,
)
.all(runId) as Row[];
const activeTaskAttempts =
activeRows.map(
(row): Readonly<
PluginPackageWorkflowCancellationActiveAttempt
> =>
Object.freeze({
admission:
json(row, 'admissionJson') as unknown as
Readonly<PluginPackageWorkflowTaskAttemptAdmissionReceipt>,
attempt: attemptFromRow(row),
leaseStatus: null,
}),
);
const clock = client
.prepare(
`SELECT CAST(unixepoch('subsec') * 1000 AS INTEGER)
AS "observedAtMs"`,
)
.get() as Row | undefined;
if (!clock) throw unavailable();
const resolution = resolvePluginPackageWorkflowCancellation({
run,
stepRuns,
activeTaskAttempts,
observedAtMs: integer(clock, 'observedAtMs'),
});
for (const transition of resolution.attemptTransitions) {
const updated = client
.prepare(
`UPDATE "RunAttempts"
SET status = ?, finished_at_ms = ?,
error_code = ?, error_summary = ?
WHERE id = ? AND run_id = ?
AND status = ? AND callback_sequence = ?`,
)
.run(
transition.attempt.status,
transition.attempt.finishedAtMs ?? null,
transition.attempt.errorCode ?? null,
transition.attempt.errorSummary ?? null,
transition.attempt.id,
transition.attempt.runId,
transition.previousStatus,
transition.attempt.callbackSequence,
);
if (updated.changes !== 1) throw unavailable();
insertEvent(client, transition.event);
}
for (const mutation of resolution.stepMutations) {
updateStepRun(client, mutation);
insertStepMutation(client, mutation, resolution.observedAtMs);
}
if (resolution.terminalTransition) {
insertEvent(client, resolution.terminalTransition.event);
}
if (
resolution.run.version !== run.version ||
resolution.run.eventSequence !== run.eventSequence ||
resolution.run.status !== run.status
) {
const updated = client
.prepare(
`UPDATE "Runs"
SET status = ?, finished_at_ms = ?,
error_code = ?, error_summary = ?,
version = ?, event_sequence = ?
WHERE id = ? AND status = 'running'
AND execution_owner = 'runtime'
AND trigger_type = 'plugin_package_workflow'
AND cancel_requested_at_ms = ? AND cancel_reason = ?
AND version = ? AND event_sequence = ?`,
)
.run(
resolution.run.status,
resolution.run.finishedAtMs ?? null,
resolution.run.errorCode ?? null,
resolution.run.errorSummary ?? null,
resolution.run.version,
resolution.run.eventSequence,
run.id,
run.cancelRequestedAtMs!,
run.cancelReason!,
run.version,
run.eventSequence,
);
if (updated.changes !== 1) throw unavailable();
}
client.exec('COMMIT');
began = false;
return Object.freeze({
settledRuns: resolution.terminalTransition === null ? 0 : 1,
settledAttempts: resolution.attemptTransitions.length,
blocked: resolution.blockedStepRunIds.length === 0 ? 0 : 1,
});
} finally {
if (began && client.isTransaction) {
try {
client.exec('ROLLBACK');
} catch {
// Preserve the fail-closed convergence error.
}
}
}
}
}
@@ -0,0 +1,759 @@
import type { DatabaseSync } from 'node:sqlite';
import {
InvalidPluginPackageWorkflowFrontierError,
MAX_PLUGIN_PACKAGE_WORKFLOW_FRONTIER_PAGE_SIZE,
PluginPackageWorkflowFrontierConflictError,
PluginPackageWorkflowFrontierUnavailableError,
resolvePluginPackageWorkflowFrontier,
type PluginPackageWorkflowFrontierAdvanceResult,
type PluginPackageWorkflowFrontierCandidate,
type PluginPackageWorkflowFrontierCursor,
type PluginPackageWorkflowFrontierPage,
type PluginPackageWorkflowFrontierRepository,
type PluginPackageWorkflowTerminalStatus,
} from '@qinglong/runtime-core/plugin-package-workflow-frontier';
import {
normalizePluginPackageWorkflowExecutionPlan,
type PluginPackageWorkflowExecutionPlan,
} from '@qinglong/runtime-core/plugin-package-workflow-execution-plan';
import type { RunRecord } from '@qinglong/runtime-core';
import {
normalizeStepRunRecord,
type StepRunMutation,
type StepRunRecord,
} from '@qinglong/runtime-core/step-run';
import { LocalSqliteOperationAuthority } from '../../authority/operationAuthority';
type Row = Record<string, unknown>;
const IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const DIGEST = /^[0-9a-f]{64}$/;
const TERMINAL_RUN_STATUSES = new Set<PluginPackageWorkflowTerminalStatus>([
'succeeded',
'failed',
'cancelled',
'timed_out',
]);
const RUN_SELECT = `
id, project_id AS "projectId", task_id AS "taskId",
task_revision AS "taskRevision", task_name AS "taskName",
task_snapshot_ref AS "taskSnapshotRef", legacy_cron_id AS "legacyCronId",
parent_run_id AS "parentRunId", retry_of_run_id AS "retryOfRunId",
trigger_id AS "triggerId", trigger_type AS "triggerType",
execution_origin AS "executionOrigin",
execution_owner AS "executionOwner", triggered_by AS "triggeredBy",
request_id AS "requestId", scheduled_for_ms AS "scheduledForMs",
status, version, event_sequence AS "eventSequence", priority,
idempotency_key AS "idempotencyKey", input_ref AS "inputRef",
output_ref AS "outputRef", created_at_ms AS "createdAtMs",
queued_at_ms AS "queuedAtMs", started_at_ms AS "startedAtMs",
finished_at_ms AS "finishedAtMs",
cancel_requested_at_ms AS "cancelRequestedAtMs",
cancel_reason AS "cancelReason", error_code AS "errorCode",
error_summary AS "errorSummary"
`.trim();
const STEP_RUN_SELECT = `
id, run_id AS "runId", parent_step_run_id AS "parentStepRunId",
step_key AS "stepKey", kind, definition_ref AS "definitionRef",
definition_digest AS "definitionDigest", required, status, version,
attempt_count AS "attemptCount", input_ref AS "inputRef",
output_ref AS "outputRef", approval_request_id AS "approvalRequestId",
ready_at_ms AS "readyAtMs", started_at_ms AS "startedAtMs",
finished_at_ms AS "finishedAtMs", result_code AS "resultCode",
error_summary AS "errorSummary", created_at_ms AS "createdAtMs",
updated_at_ms AS "updatedAtMs", last_mutation_id AS "lastMutationId",
step_run_digest AS "stepRunDigest", step_run_json AS "stepRunJson"
`.trim();
function unavailable(cause?: unknown): PluginPackageWorkflowFrontierUnavailableError {
return new PluginPackageWorkflowFrontierUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function text(row: Row, key: string): string {
const value = row[key];
if (typeof value !== 'string') throw unavailable();
return value;
}
function integer(row: Row, key: string): number {
const value = row[key];
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw unavailable();
}
return value as number;
}
function optionalText(row: Row, key: string): string | undefined {
const value = row[key];
if (value === null || value === undefined) return undefined;
if (typeof value !== 'string') throw unavailable();
return value;
}
function optionalInteger(row: Row, key: string): number | undefined {
if (row[key] === null || row[key] === undefined) return undefined;
return integer(row, key);
}
function identity(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTITY.test(value)) {
throw new InvalidPluginPackageWorkflowFrontierError(
`${label} is invalid`,
);
}
return value;
}
function digest(value: unknown): string {
if (typeof value !== 'string' || !DIGEST.test(value)) throw unavailable();
return value;
}
function sqliteConstraint(error: unknown): boolean {
if (!error || typeof error !== 'object') return false;
const code = (error as { code?: unknown }).code;
const errcode = (error as { errcode?: unknown }).errcode;
return (
(typeof code === 'string' &&
(code === 'ERR_SQLITE_CONSTRAINT' ||
code.startsWith('SQLITE_CONSTRAINT') ||
code.startsWith('ERR_SQLITE_CONSTRAINT'))) ||
(typeof errcode === 'number' && (errcode & 0xff) === 19)
);
}
function mapStorageError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageWorkflowFrontierError ||
error instanceof PluginPackageWorkflowFrontierConflictError ||
error instanceof PluginPackageWorkflowFrontierUnavailableError
) {
return error;
}
return sqliteConstraint(error)
? new PluginPackageWorkflowFrontierConflictError()
: unavailable(error);
}
function pageLimit(value: unknown): number {
if (
!Number.isInteger(value) ||
(value as number) < 1 ||
(value as number) > MAX_PLUGIN_PACKAGE_WORKFLOW_FRONTIER_PAGE_SIZE
) {
throw new InvalidPluginPackageWorkflowFrontierError(
`page limit must be between 1 and ${MAX_PLUGIN_PACKAGE_WORKFLOW_FRONTIER_PAGE_SIZE}`,
);
}
return value as number;
}
function cursor(
value: Readonly<PluginPackageWorkflowFrontierCursor> | undefined,
): Readonly<PluginPackageWorkflowFrontierCursor> | undefined {
if (value === undefined) return undefined;
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Reflect.ownKeys(value).length !== 2 ||
!Reflect.has(value, 'admittedAtMs') ||
!Reflect.has(value, 'planDigest') ||
!Number.isSafeInteger(value.admittedAtMs) ||
value.admittedAtMs < 0 ||
!DIGEST.test(value.planDigest)
) {
throw new InvalidPluginPackageWorkflowFrontierError(
'frontier cursor is invalid',
);
}
return Object.freeze({
admittedAtMs: value.admittedAtMs,
planDigest: value.planDigest,
});
}
function runFromRow(row: Row): Readonly<RunRecord> {
return Object.freeze({
id: text(row, 'id'),
projectId: text(row, 'projectId'),
taskId: text(row, 'taskId'),
taskRevision: text(row, 'taskRevision'),
...(optionalText(row, 'taskName') === undefined
? {}
: { taskName: optionalText(row, 'taskName')! }),
...(optionalText(row, 'taskSnapshotRef') === undefined
? {}
: { taskSnapshotRef: optionalText(row, 'taskSnapshotRef')! }),
...(optionalInteger(row, 'legacyCronId') === undefined
? {}
: { legacyCronId: optionalInteger(row, 'legacyCronId')! }),
...(optionalText(row, 'parentRunId') === undefined
? {}
: { parentRunId: optionalText(row, 'parentRunId')! }),
...(optionalText(row, 'retryOfRunId') === undefined
? {}
: { retryOfRunId: optionalText(row, 'retryOfRunId')! }),
...(optionalText(row, 'triggerId') === undefined
? {}
: { triggerId: optionalText(row, 'triggerId')! }),
triggerType: text(row, 'triggerType'),
executionOrigin: text(
row,
'executionOrigin',
) as RunRecord['executionOrigin'],
executionOwner: text(row, 'executionOwner') as RunRecord['executionOwner'],
...(optionalText(row, 'triggeredBy') === undefined
? {}
: { triggeredBy: optionalText(row, 'triggeredBy')! }),
...(optionalText(row, 'requestId') === undefined
? {}
: { requestId: optionalText(row, 'requestId')! }),
...(optionalInteger(row, 'scheduledForMs') === undefined
? {}
: { scheduledForMs: optionalInteger(row, 'scheduledForMs')! }),
status: text(row, 'status') as RunRecord['status'],
version: integer(row, 'version'),
eventSequence: integer(row, 'eventSequence'),
priority: integer(row, 'priority'),
...(optionalText(row, 'idempotencyKey') === undefined
? {}
: { idempotencyKey: optionalText(row, 'idempotencyKey')! }),
...(optionalText(row, 'inputRef') === undefined
? {}
: { inputRef: optionalText(row, 'inputRef')! }),
...(optionalText(row, 'outputRef') === undefined
? {}
: { outputRef: optionalText(row, 'outputRef')! }),
createdAtMs: integer(row, 'createdAtMs'),
...(optionalInteger(row, 'queuedAtMs') === undefined
? {}
: { queuedAtMs: optionalInteger(row, 'queuedAtMs')! }),
...(optionalInteger(row, 'startedAtMs') === undefined
? {}
: { startedAtMs: optionalInteger(row, 'startedAtMs')! }),
...(optionalInteger(row, 'finishedAtMs') === undefined
? {}
: { finishedAtMs: optionalInteger(row, 'finishedAtMs')! }),
...(optionalInteger(row, 'cancelRequestedAtMs') === undefined
? {}
: {
cancelRequestedAtMs: optionalInteger(row, 'cancelRequestedAtMs')!,
}),
...(optionalText(row, 'cancelReason') === undefined
? {}
: {
cancelReason: optionalText(
row,
'cancelReason',
)! as NonNullable<RunRecord['cancelReason']>,
}),
...(optionalText(row, 'errorCode') === undefined
? {}
: { errorCode: optionalText(row, 'errorCode')! }),
...(optionalText(row, 'errorSummary') === undefined
? {}
: { errorSummary: optionalText(row, 'errorSummary')! }),
});
}
function stepRunFromRow(row: Row): Readonly<StepRunRecord> {
let stepRun: Readonly<StepRunRecord>;
try {
stepRun = normalizeStepRunRecord(
JSON.parse(text(row, 'stepRunJson')) as StepRunRecord,
);
} catch {
throw unavailable();
}
if (
text(row, 'id') !== stepRun.id ||
text(row, 'runId') !== stepRun.runId ||
optionalText(row, 'parentStepRunId') !==
(stepRun.parentStepRunId ?? undefined) ||
text(row, 'stepKey') !== stepRun.stepKey ||
text(row, 'kind') !== stepRun.kind ||
text(row, 'definitionRef') !== stepRun.definitionRef ||
text(row, 'definitionDigest') !== stepRun.definitionDigest ||
integer(row, 'required') !== (stepRun.required ? 1 : 0) ||
text(row, 'status') !== stepRun.status ||
integer(row, 'version') !== stepRun.version ||
integer(row, 'attemptCount') !== stepRun.attemptCount ||
optionalText(row, 'inputRef') !== (stepRun.inputRef ?? undefined) ||
optionalText(row, 'outputRef') !== (stepRun.outputRef ?? undefined) ||
optionalText(row, 'approvalRequestId') !==
(stepRun.approvalRequestId ?? undefined) ||
optionalInteger(row, 'readyAtMs') !==
(stepRun.readyAtMs ?? undefined) ||
optionalInteger(row, 'startedAtMs') !==
(stepRun.startedAtMs ?? undefined) ||
optionalInteger(row, 'finishedAtMs') !==
(stepRun.finishedAtMs ?? undefined) ||
optionalText(row, 'resultCode') !==
(stepRun.resultCode ?? undefined) ||
optionalText(row, 'errorSummary') !==
(stepRun.errorSummary ?? undefined) ||
integer(row, 'createdAtMs') !== stepRun.createdAtMs ||
integer(row, 'updatedAtMs') !== stepRun.updatedAtMs ||
text(row, 'lastMutationId') !== stepRun.lastMutationId ||
text(row, 'stepRunDigest') !== stepRun.stepRunDigest
) {
throw unavailable();
}
return stepRun;
}
function assertRunIdentity(
run: Readonly<RunRecord>,
plan: Readonly<PluginPackageWorkflowExecutionPlan>,
): void {
if (
run.id !== plan.runId ||
run.projectId !== plan.target.projectId ||
run.taskId !== plan.target.workflowId ||
run.taskRevision !== plan.target.publicationDigest ||
run.triggerType !== 'plugin_package_workflow' ||
run.executionOrigin !== 'system' ||
run.executionOwner !== 'runtime' ||
run.requestId !== plan.planId ||
run.idempotencyKey !== `plugin-package-workflow:${plan.planId}` ||
run.version !== run.eventSequence
) {
throw unavailable();
}
}
function updateStepRun(
client: DatabaseSync,
mutation: Readonly<StepRunMutation>,
): void {
const stepRun = mutation.stepRun;
const updated = client
.prepare(
`UPDATE "StepRuns"
SET status = ?, version = ?, attempt_count = ?, output_ref = ?,
approval_request_id = ?, ready_at_ms = ?, started_at_ms = ?,
finished_at_ms = ?, result_code = ?, error_summary = ?,
updated_at_ms = ?, last_mutation_id = ?, step_run_digest = ?,
step_run_json = ?
WHERE id = ? AND run_id = ? AND version = ?
AND step_run_digest = ? AND status = ?`,
)
.run(
stepRun.status,
stepRun.version,
stepRun.attemptCount,
stepRun.outputRef,
stepRun.approvalRequestId,
stepRun.readyAtMs,
stepRun.startedAtMs,
stepRun.finishedAtMs,
stepRun.resultCode,
stepRun.errorSummary,
stepRun.updatedAtMs,
stepRun.lastMutationId,
stepRun.stepRunDigest,
JSON.stringify(stepRun),
stepRun.id,
stepRun.runId,
mutation.expectedStepRunVersion,
mutation.expectedStepRunDigest,
mutation.previousStatus,
);
if (updated.changes !== 1) {
throw new PluginPackageWorkflowFrontierConflictError();
}
}
function insertStepMutation(
client: DatabaseSync,
mutation: Readonly<StepRunMutation>,
committedAtMs: number,
): void {
const event = mutation.event;
client
.prepare(
`INSERT INTO "RunEvents" (
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
attempt_id, step_run_id, payload, created_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)`,
)
.run(
event.id,
event.runId,
event.sequence,
event.type,
event.dedupeKey ?? null,
event.actorType,
event.actorId ?? null,
mutation.stepRun.id,
JSON.stringify(event.payload),
event.createdAtMs,
);
client
.prepare(
`INSERT INTO "StepRunMutations" (
mutation_id, mutation_digest, run_id, step_run_id,
step_run_digest, event_id, event_sequence, run_version,
step_run_json, committed_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
mutation.mutationId,
mutation.mutationDigest,
mutation.runId,
mutation.stepRun.id,
mutation.stepRun.stepRunDigest,
event.id,
event.sequence,
mutation.expectedRunVersion + 1,
JSON.stringify(mutation.stepRun),
committedAtMs,
);
}
export class LocalSqlitePluginPackageWorkflowFrontierRepository
implements PluginPackageWorkflowFrontierRepository
{
readonly #authority: LocalSqliteOperationAuthority;
constructor(authority: LocalSqliteOperationAuthority | DatabaseSync) {
this.#authority =
authority instanceof LocalSqliteOperationAuthority
? authority
: new LocalSqliteOperationAuthority(authority);
}
#enqueue<T>(work: () => T): Promise<T> {
return this.#authority.enqueue(
async () => {
try {
return work();
} catch (error) {
throw mapStorageError(error);
}
},
() => unavailable(),
);
}
listCandidates(queryValue: Readonly<{
limit: number;
after?: Readonly<PluginPackageWorkflowFrontierCursor>;
}>): Promise<Readonly<PluginPackageWorkflowFrontierPage>> {
if (
!queryValue ||
typeof queryValue !== 'object' ||
Array.isArray(queryValue) ||
!Reflect.has(queryValue, 'limit') ||
Reflect.ownKeys(queryValue).some(
(key) => key !== 'limit' && key !== 'after',
)
) {
throw new InvalidPluginPackageWorkflowFrontierError(
'page query is invalid',
);
}
const limit = pageLimit(queryValue.limit);
const after = cursor(queryValue.after);
return this.#enqueue(() => {
const rows = this.#authority.client
.prepare(
`SELECT admission.run_id AS "runId",
admission.plan_digest AS "planDigest",
admission.admitted_at_ms AS "admittedAtMs"
FROM "QingLong3PluginPackageWorkflowAdmissions" AS admission
JOIN "Runs" AS run ON run.id = admission.run_id
WHERE run.status = 'running'
AND run.cancel_requested_at_ms IS NULL
AND (
EXISTS (
SELECT 1
FROM "QingLong3PluginPackageWorkflowAdmissionSteps" AS step
JOIN "StepRuns" AS current
ON current.run_id = step.run_id
AND current.id = step.step_run_id
WHERE step.plan_digest = admission.plan_digest
AND current.status = 'pending'
AND (
NOT EXISTS (
SELECT 1
FROM json_each(step.needs_json) AS need
LEFT JOIN "QingLong3PluginPackageWorkflowAdmissionSteps"
AS dependency_step
ON dependency_step.plan_digest = step.plan_digest
AND dependency_step.step_key = need.value
LEFT JOIN "StepRuns" AS dependency
ON dependency.run_id = dependency_step.run_id
AND dependency.id = dependency_step.step_run_id
WHERE dependency.id IS NULL
OR dependency.status <> 'succeeded'
)
OR EXISTS (
SELECT 1
FROM json_each(step.needs_json) AS need
JOIN "QingLong3PluginPackageWorkflowAdmissionSteps"
AS dependency_step
ON dependency_step.plan_digest = step.plan_digest
AND dependency_step.step_key = need.value
JOIN "StepRuns" AS dependency
ON dependency.run_id = dependency_step.run_id
AND dependency.id = dependency_step.step_run_id
WHERE dependency.status IN (
'failed', 'skipped', 'cancelled', 'timed_out'
)
)
)
)
OR NOT EXISTS (
SELECT 1
FROM "QingLong3PluginPackageWorkflowAdmissionSteps" AS step
JOIN "StepRuns" AS current
ON current.run_id = step.run_id
AND current.id = step.step_run_id
WHERE step.plan_digest = admission.plan_digest
AND current.status NOT IN (
'succeeded', 'failed', 'skipped', 'cancelled', 'timed_out'
)
)
)
AND (
? IS NULL OR admission.admitted_at_ms > ? OR
(admission.admitted_at_ms = ? AND admission.plan_digest > ?)
)
ORDER BY admission.admitted_at_ms, admission.plan_digest
LIMIT ?`,
)
.all(
after?.planDigest ?? null,
after?.admittedAtMs ?? 0,
after?.admittedAtMs ?? 0,
after?.planDigest ?? '',
limit + 1,
) as Row[];
const mapped = rows.map(
(row): Readonly<PluginPackageWorkflowFrontierCandidate> =>
Object.freeze({
runId: identity(text(row, 'runId'), 'candidate runId'),
planDigest: digest(row.planDigest),
admittedAtMs: integer(row, 'admittedAtMs'),
}),
);
const truncated = mapped.length > limit;
const candidates = Object.freeze(mapped.slice(0, limit));
const last = candidates.at(-1);
return Object.freeze({
candidates,
truncated,
...(truncated && last
? {
next: Object.freeze({
admittedAtMs: last.admittedAtMs,
planDigest: last.planDigest,
}),
}
: {}),
});
});
}
advance(
runIdValue: string,
): Promise<Readonly<PluginPackageWorkflowFrontierAdvanceResult>> {
const runId = identity(runIdValue, 'runId');
return this.#enqueue(() => {
let began = false;
try {
const client = this.#authority.client;
client.exec('BEGIN IMMEDIATE');
began = true;
const admission = client
.prepare(
`SELECT plan_digest AS "planDigest", plan_json AS "planJson"
FROM "QingLong3PluginPackageWorkflowAdmissions"
WHERE run_id = ? LIMIT 2`,
)
.all(runId) as Row[];
if (admission.length !== 1) {
throw new PluginPackageWorkflowFrontierConflictError();
}
let plan: Readonly<PluginPackageWorkflowExecutionPlan>;
try {
plan = normalizePluginPackageWorkflowExecutionPlan(
JSON.parse(
text(admission[0]!, 'planJson'),
) as PluginPackageWorkflowExecutionPlan,
);
} catch {
throw unavailable();
}
if (plan.planDigest !== digest(admission[0]!.planDigest)) {
throw unavailable();
}
const runRows = client
.prepare(`SELECT ${RUN_SELECT} FROM "Runs" WHERE id = ? LIMIT 2`)
.all(runId) as Row[];
if (runRows.length !== 1) {
throw new PluginPackageWorkflowFrontierConflictError();
}
const run = runFromRow(runRows[0]!);
assertRunIdentity(run, plan);
const stepRows = client
.prepare(
`SELECT ${STEP_RUN_SELECT}
FROM "StepRuns" WHERE run_id = ?
ORDER BY step_key, id`,
)
.all(runId) as Row[];
const stepRuns = stepRows.map(stepRunFromRow);
const clock = client
.prepare(
`SELECT CAST(unixepoch('subsec') * 1000 AS INTEGER)
AS "observedAtMs"`,
)
.get() as Row | undefined;
if (!clock) throw unavailable();
const observedAtMs = integer(clock, 'observedAtMs');
const currentStatus = run.status;
const resolution = resolvePluginPackageWorkflowFrontier({
plan,
run: {
...run,
...(TERMINAL_RUN_STATUSES.has(
currentStatus as PluginPackageWorkflowTerminalStatus,
)
? { status: 'running' as const }
: {}),
},
stepRuns,
observedAtMs,
});
if (
TERMINAL_RUN_STATUSES.has(
currentStatus as PluginPackageWorkflowTerminalStatus,
)
) {
if (
resolution.stepMutations.length !== 0 ||
resolution.terminalStatus !== currentStatus
) {
throw unavailable();
}
client.exec('COMMIT');
began = false;
return Object.freeze({
status: 'settled' as const,
runId,
planDigest: plan.planDigest,
stepMutationCount: 0,
readyStepRunIds: Object.freeze([]),
terminalStatus:
currentStatus as PluginPackageWorkflowTerminalStatus,
runVersion: run.version,
runEventSequence: run.eventSequence,
observedAtMs,
});
}
if (currentStatus !== 'running') throw unavailable();
const increment =
resolution.stepMutations.length +
(resolution.terminalTransition === null ? 0 : 1);
if (increment === 0) {
client.exec('COMMIT');
began = false;
return Object.freeze({
status: 'unchanged' as const,
runId,
planDigest: plan.planDigest,
stepMutationCount: 0,
readyStepRunIds: resolution.readyStepRunIds,
terminalStatus: null,
runVersion: run.version,
runEventSequence: run.eventSequence,
observedAtMs,
});
}
for (const mutation of resolution.stepMutations) {
updateStepRun(client, mutation);
}
const terminal = resolution.terminalTransition;
const updatedRun = client
.prepare(
`UPDATE "Runs"
SET status = ?, version = version + ?,
event_sequence = event_sequence + ?,
finished_at_ms = ?, error_code = ?, error_summary = NULL
WHERE id = ? AND status = 'running'
AND version = ? AND event_sequence = ?`,
)
.run(
terminal?.status ?? 'running',
increment,
increment,
terminal?.finishedAtMs ?? null,
terminal?.errorCode ?? null,
runId,
run.version,
run.eventSequence,
);
if (updatedRun.changes !== 1) {
throw new PluginPackageWorkflowFrontierConflictError();
}
for (const mutation of resolution.stepMutations) {
insertStepMutation(client, mutation, observedAtMs);
}
if (terminal) {
const event = terminal.event;
client
.prepare(
`INSERT INTO "RunEvents" (
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
attempt_id, step_run_id, payload, created_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?)`,
)
.run(
event.id,
event.runId,
event.sequence,
event.type,
event.dedupeKey ?? null,
event.actorType,
event.actorId ?? null,
JSON.stringify(event.payload),
event.createdAtMs,
);
}
client.exec('COMMIT');
began = false;
const runVersion = run.version + increment;
return Object.freeze({
status: terminal ? ('terminal' as const) : ('advanced' as const),
runId,
planDigest: plan.planDigest,
stepMutationCount: resolution.stepMutations.length,
readyStepRunIds: terminal
? Object.freeze([])
: resolution.readyStepRunIds,
terminalStatus: terminal?.status ?? null,
runVersion,
runEventSequence: run.eventSequence + increment,
observedAtMs,
});
} finally {
if (began && this.#authority.client.isTransaction) {
try {
this.#authority.client.exec('ROLLBACK');
} catch {
// Preserve the original fail-closed error.
}
}
}
});
}
}
@@ -0,0 +1,783 @@
import type { DatabaseSync } from 'node:sqlite';
import {
createPluginPackageWorkflowTaskAttemptAdmission,
InvalidPluginPackageWorkflowTaskAttemptAdmissionError,
MAX_PLUGIN_PACKAGE_WORKFLOW_TASK_ATTEMPT_PAGE_SIZE,
normalizePluginPackageWorkflowTaskAttemptAdmissionReceipt,
PluginPackageWorkflowTaskAttemptAdmissionConflictError,
PluginPackageWorkflowTaskAttemptAdmissionUnavailableError,
type PluginPackageWorkflowTaskAttemptAdmissionCandidate,
type PluginPackageWorkflowTaskAttemptAdmissionCursor,
type PluginPackageWorkflowTaskAttemptAdmissionPage,
type PluginPackageWorkflowTaskAttemptAdmissionReceipt,
type PluginPackageWorkflowTaskAttemptAdmissionRepository,
type PluginPackageWorkflowTaskAttemptAdmissionResult,
} from '@qinglong/runtime-core/plugin-package-workflow-task-attempt-admission';
import {
normalizeLocalTaskExecutionRevision,
type LocalTaskExecutionRevision,
} from '@qinglong/runtime-core/local-dispatch';
import {
normalizePluginPackageTaskReconciliationReceipt,
type PluginPackageTaskReconciliationReceipt,
} from '@qinglong/runtime-core/plugin-package-task-reconciliation';
import {
normalizePluginPackageWorkflowExecutionPlan,
type PluginPackageWorkflowExecutionPlan,
} from '@qinglong/runtime-core/plugin-package-workflow-execution-plan';
import type { RunRecord } from '@qinglong/runtime-core';
import {
normalizeStepRunRecord,
type StepRunRecord,
} from '@qinglong/runtime-core/step-run';
import { LocalSqliteOperationAuthority } from '../../authority/operationAuthority';
type Row = Record<string, unknown>;
const IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const DIGEST = /^[0-9a-f]{64}$/;
const RUN_SELECT = `
id, project_id AS "projectId", task_id AS "taskId",
task_revision AS "taskRevision", task_name AS "taskName",
task_snapshot_ref AS "taskSnapshotRef", legacy_cron_id AS "legacyCronId",
parent_run_id AS "parentRunId", retry_of_run_id AS "retryOfRunId",
trigger_id AS "triggerId", trigger_type AS "triggerType",
execution_origin AS "executionOrigin",
execution_owner AS "executionOwner", triggered_by AS "triggeredBy",
request_id AS "requestId", scheduled_for_ms AS "scheduledForMs",
status, version, event_sequence AS "eventSequence", priority,
idempotency_key AS "idempotencyKey", input_ref AS "inputRef",
output_ref AS "outputRef", created_at_ms AS "createdAtMs",
queued_at_ms AS "queuedAtMs", started_at_ms AS "startedAtMs",
finished_at_ms AS "finishedAtMs",
cancel_requested_at_ms AS "cancelRequestedAtMs",
cancel_reason AS "cancelReason", error_code AS "errorCode",
error_summary AS "errorSummary"
`.trim();
const STEP_RUN_SELECT = `
id, run_id AS "runId", parent_step_run_id AS "parentStepRunId",
step_key AS "stepKey", kind, definition_ref AS "definitionRef",
definition_digest AS "definitionDigest", required, status, version,
attempt_count AS "attemptCount", input_ref AS "inputRef",
output_ref AS "outputRef", approval_request_id AS "approvalRequestId",
ready_at_ms AS "readyAtMs", started_at_ms AS "startedAtMs",
finished_at_ms AS "finishedAtMs", result_code AS "resultCode",
error_summary AS "errorSummary", created_at_ms AS "createdAtMs",
updated_at_ms AS "updatedAtMs", last_mutation_id AS "lastMutationId",
step_run_digest AS "stepRunDigest", step_run_json AS "stepRunJson"
`.trim();
function unavailable(
cause?: unknown,
): PluginPackageWorkflowTaskAttemptAdmissionUnavailableError {
return new PluginPackageWorkflowTaskAttemptAdmissionUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function text(row: Row, key: string): string {
const value = row[key];
if (typeof value !== 'string') throw unavailable();
return value;
}
function integer(row: Row, key: string): number {
const value = row[key];
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw unavailable();
}
return value as number;
}
function optionalText(row: Row, key: string): string | undefined {
const value = row[key];
if (value === null || value === undefined) return undefined;
return text(row, key);
}
function optionalInteger(row: Row, key: string): number | undefined {
if (row[key] === null || row[key] === undefined) return undefined;
return integer(row, key);
}
function json(row: Row, key: string): unknown {
try {
return JSON.parse(text(row, key));
} catch {
throw unavailable();
}
}
function identity(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTITY.test(value)) {
throw new InvalidPluginPackageWorkflowTaskAttemptAdmissionError(
`${label} is invalid`,
);
}
return value;
}
function sqliteConstraint(error: unknown): boolean {
if (!error || typeof error !== 'object') return false;
const code = (error as { code?: unknown }).code;
const errcode = (error as { errcode?: unknown }).errcode;
return (
(typeof code === 'string' &&
(code === 'ERR_SQLITE_CONSTRAINT' ||
code.startsWith('SQLITE_CONSTRAINT') ||
code.startsWith('ERR_SQLITE_CONSTRAINT'))) ||
(typeof errcode === 'number' && (errcode & 0xff) === 19)
);
}
function mapStorageError(error: unknown): Error {
if (
error instanceof
InvalidPluginPackageWorkflowTaskAttemptAdmissionError ||
error instanceof
PluginPackageWorkflowTaskAttemptAdmissionConflictError ||
error instanceof
PluginPackageWorkflowTaskAttemptAdmissionUnavailableError
) {
return error;
}
return sqliteConstraint(error)
? new PluginPackageWorkflowTaskAttemptAdmissionConflictError()
: unavailable(error);
}
function pageLimit(value: unknown): number {
if (
!Number.isInteger(value) ||
(value as number) < 1 ||
(value as number) >
MAX_PLUGIN_PACKAGE_WORKFLOW_TASK_ATTEMPT_PAGE_SIZE
) {
throw new InvalidPluginPackageWorkflowTaskAttemptAdmissionError(
`page limit must be between 1 and ${MAX_PLUGIN_PACKAGE_WORKFLOW_TASK_ATTEMPT_PAGE_SIZE}`,
);
}
return value as number;
}
function cursor(
value:
| Readonly<PluginPackageWorkflowTaskAttemptAdmissionCursor>
| undefined,
):
| Readonly<PluginPackageWorkflowTaskAttemptAdmissionCursor>
| undefined {
if (value === undefined) return undefined;
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Reflect.ownKeys(value).length !== 2 ||
!Reflect.has(value, 'readyAtMs') ||
!Reflect.has(value, 'stepRunId') ||
!Number.isSafeInteger(value.readyAtMs) ||
value.readyAtMs < 0 ||
typeof value.stepRunId !== 'string' ||
!IDENTITY.test(value.stepRunId)
) {
throw new InvalidPluginPackageWorkflowTaskAttemptAdmissionError(
'candidate cursor is invalid',
);
}
return Object.freeze({
readyAtMs: value.readyAtMs,
stepRunId: value.stepRunId,
});
}
function runFromRow(row: Row): Readonly<RunRecord> {
return Object.freeze({
id: text(row, 'id'),
projectId: text(row, 'projectId'),
taskId: text(row, 'taskId'),
taskRevision: text(row, 'taskRevision'),
...(optionalText(row, 'taskName') === undefined
? {}
: { taskName: optionalText(row, 'taskName')! }),
...(optionalText(row, 'taskSnapshotRef') === undefined
? {}
: { taskSnapshotRef: optionalText(row, 'taskSnapshotRef')! }),
...(optionalInteger(row, 'legacyCronId') === undefined
? {}
: { legacyCronId: optionalInteger(row, 'legacyCronId')! }),
...(optionalText(row, 'parentRunId') === undefined
? {}
: { parentRunId: optionalText(row, 'parentRunId')! }),
...(optionalText(row, 'retryOfRunId') === undefined
? {}
: { retryOfRunId: optionalText(row, 'retryOfRunId')! }),
...(optionalText(row, 'triggerId') === undefined
? {}
: { triggerId: optionalText(row, 'triggerId')! }),
triggerType: text(row, 'triggerType'),
executionOrigin: text(
row,
'executionOrigin',
) as RunRecord['executionOrigin'],
executionOwner: text(row, 'executionOwner') as RunRecord['executionOwner'],
...(optionalText(row, 'triggeredBy') === undefined
? {}
: { triggeredBy: optionalText(row, 'triggeredBy')! }),
...(optionalText(row, 'requestId') === undefined
? {}
: { requestId: optionalText(row, 'requestId')! }),
...(optionalInteger(row, 'scheduledForMs') === undefined
? {}
: { scheduledForMs: optionalInteger(row, 'scheduledForMs')! }),
status: text(row, 'status') as RunRecord['status'],
version: integer(row, 'version'),
eventSequence: integer(row, 'eventSequence'),
priority: integer(row, 'priority'),
...(optionalText(row, 'idempotencyKey') === undefined
? {}
: { idempotencyKey: optionalText(row, 'idempotencyKey')! }),
...(optionalText(row, 'inputRef') === undefined
? {}
: { inputRef: optionalText(row, 'inputRef')! }),
...(optionalText(row, 'outputRef') === undefined
? {}
: { outputRef: optionalText(row, 'outputRef')! }),
createdAtMs: integer(row, 'createdAtMs'),
...(optionalInteger(row, 'queuedAtMs') === undefined
? {}
: { queuedAtMs: optionalInteger(row, 'queuedAtMs')! }),
...(optionalInteger(row, 'startedAtMs') === undefined
? {}
: { startedAtMs: optionalInteger(row, 'startedAtMs')! }),
...(optionalInteger(row, 'finishedAtMs') === undefined
? {}
: { finishedAtMs: optionalInteger(row, 'finishedAtMs')! }),
...(optionalInteger(row, 'cancelRequestedAtMs') === undefined
? {}
: {
cancelRequestedAtMs: optionalInteger(row, 'cancelRequestedAtMs')!,
}),
...(optionalText(row, 'cancelReason') === undefined
? {}
: {
cancelReason: optionalText(
row,
'cancelReason',
)! as NonNullable<RunRecord['cancelReason']>,
}),
...(optionalText(row, 'errorCode') === undefined
? {}
: { errorCode: optionalText(row, 'errorCode')! }),
...(optionalText(row, 'errorSummary') === undefined
? {}
: { errorSummary: optionalText(row, 'errorSummary')! }),
});
}
function stepRunFromRow(row: Row): Readonly<StepRunRecord> {
let stepRun: Readonly<StepRunRecord>;
try {
stepRun = normalizeStepRunRecord(
json(row, 'stepRunJson') as StepRunRecord,
);
} catch {
throw unavailable();
}
if (
text(row, 'id') !== stepRun.id ||
text(row, 'runId') !== stepRun.runId ||
optionalText(row, 'parentStepRunId') !==
(stepRun.parentStepRunId ?? undefined) ||
text(row, 'stepKey') !== stepRun.stepKey ||
text(row, 'kind') !== stepRun.kind ||
text(row, 'definitionRef') !== stepRun.definitionRef ||
text(row, 'definitionDigest') !== stepRun.definitionDigest ||
integer(row, 'required') !== (stepRun.required ? 1 : 0) ||
text(row, 'status') !== stepRun.status ||
integer(row, 'version') !== stepRun.version ||
integer(row, 'attemptCount') !== stepRun.attemptCount ||
optionalText(row, 'inputRef') !== (stepRun.inputRef ?? undefined) ||
optionalText(row, 'outputRef') !== (stepRun.outputRef ?? undefined) ||
optionalText(row, 'approvalRequestId') !==
(stepRun.approvalRequestId ?? undefined) ||
optionalInteger(row, 'readyAtMs') !==
(stepRun.readyAtMs ?? undefined) ||
optionalInteger(row, 'startedAtMs') !==
(stepRun.startedAtMs ?? undefined) ||
optionalInteger(row, 'finishedAtMs') !==
(stepRun.finishedAtMs ?? undefined) ||
optionalText(row, 'resultCode') !==
(stepRun.resultCode ?? undefined) ||
optionalText(row, 'errorSummary') !==
(stepRun.errorSummary ?? undefined) ||
integer(row, 'createdAtMs') !== stepRun.createdAtMs ||
integer(row, 'updatedAtMs') !== stepRun.updatedAtMs ||
text(row, 'lastMutationId') !== stepRun.lastMutationId ||
text(row, 'stepRunDigest') !== stepRun.stepRunDigest
) {
throw unavailable();
}
return stepRun;
}
function executionFromRow(row: Row): Readonly<LocalTaskExecutionRevision> {
try {
return normalizeLocalTaskExecutionRevision({
projectId: text(row, 'projectId'),
taskId: text(row, 'taskId'),
taskRevision: text(row, 'taskRevision'),
executorType: 'local_process',
command: json(
row,
'commandJson',
) as LocalTaskExecutionRevision['command'],
...(optionalText(row, 'workingDirectory') === undefined
? {}
: { workingDirectory: optionalText(row, 'workingDirectory')! }),
...(optionalInteger(row, 'timeoutMs') === undefined
? {}
: { timeoutMs: optionalInteger(row, 'timeoutMs')! }),
contextRef: text(row, 'contextRef'),
contentDigest: text(row, 'contentDigest'),
createdAtMs: integer(row, 'createdAtMs'),
});
} catch {
throw unavailable();
}
}
function receiptFromRow(
row: Row,
): Readonly<PluginPackageWorkflowTaskAttemptAdmissionReceipt> {
try {
const receipt =
normalizePluginPackageWorkflowTaskAttemptAdmissionReceipt(
json(
row,
'receiptJson',
) as PluginPackageWorkflowTaskAttemptAdmissionReceipt,
);
if (
row.receiptDigest !== undefined &&
text(row, 'receiptDigest') !== receipt.receiptDigest
) {
throw unavailable();
}
return receipt;
} catch (error) {
if (
error instanceof
PluginPackageWorkflowTaskAttemptAdmissionUnavailableError
) {
throw error;
}
throw unavailable();
}
}
export class LocalSqlitePluginPackageWorkflowTaskAttemptAdmissionRepository
implements PluginPackageWorkflowTaskAttemptAdmissionRepository
{
readonly #authority: LocalSqliteOperationAuthority;
constructor(authority: LocalSqliteOperationAuthority | DatabaseSync) {
this.#authority =
authority instanceof LocalSqliteOperationAuthority
? authority
: new LocalSqliteOperationAuthority(authority);
}
#enqueue<T>(work: () => T): Promise<T> {
return this.#authority.enqueue(
async () => {
try {
return work();
} catch (error) {
throw mapStorageError(error);
}
},
() => unavailable(),
);
}
listCandidates(queryValue: Readonly<{
limit: number;
after?: Readonly<PluginPackageWorkflowTaskAttemptAdmissionCursor>;
}>): Promise<
Readonly<PluginPackageWorkflowTaskAttemptAdmissionPage>
> {
if (
!queryValue ||
typeof queryValue !== 'object' ||
Array.isArray(queryValue) ||
!Reflect.has(queryValue, 'limit') ||
Reflect.ownKeys(queryValue).some(
(key) => key !== 'limit' && key !== 'after',
)
) {
throw new InvalidPluginPackageWorkflowTaskAttemptAdmissionError(
'page query is invalid',
);
}
const limit = pageLimit(queryValue.limit);
const after = cursor(queryValue.after);
return this.#enqueue(() => {
const rows = this.#authority.client
.prepare(
`SELECT current.run_id AS "runId",
current.id AS "stepRunId",
current.ready_at_ms AS "readyAtMs",
admission.plan_digest AS "planDigest"
FROM "StepRuns" AS current
JOIN "QingLong3PluginPackageWorkflowAdmissionSteps" AS source
ON source.run_id = current.run_id
AND source.step_run_id = current.id
JOIN "QingLong3PluginPackageWorkflowAdmissions" AS admission
ON admission.plan_digest = source.plan_digest
AND admission.run_id = source.run_id
JOIN "Runs" AS run ON run.id = current.run_id
WHERE run.status = 'running'
AND run.cancel_requested_at_ms IS NULL
AND current.kind = 'task'
AND current.status = 'ready'
AND current.ready_at_ms IS NOT NULL
AND current.attempt_count < 64
AND NOT EXISTS (
SELECT 1
FROM "QingLong3PluginPackageWorkflowTaskAttemptAdmissions"
AS task_attempt
WHERE task_attempt.run_id = current.run_id
AND task_attempt.step_run_id = current.id
AND task_attempt.step_run_version = current.version
)
AND (
? IS NULL OR current.ready_at_ms > ? OR
(current.ready_at_ms = ? AND current.id > ?)
)
ORDER BY current.ready_at_ms, current.id
LIMIT ?`,
)
.all(
after?.stepRunId ?? null,
after?.readyAtMs ?? 0,
after?.readyAtMs ?? 0,
after?.stepRunId ?? '',
limit + 1,
) as Row[];
const mapped = rows.map(
(row): Readonly<PluginPackageWorkflowTaskAttemptAdmissionCandidate> =>
Object.freeze({
runId: identity(text(row, 'runId'), 'candidate runId'),
stepRunId: identity(
text(row, 'stepRunId'),
'candidate stepRunId',
),
readyAtMs: integer(row, 'readyAtMs'),
planDigest: text(row, 'planDigest'),
}),
);
if (mapped.some(({ planDigest }) => !DIGEST.test(planDigest))) {
throw unavailable();
}
const truncated = mapped.length > limit;
const candidates = Object.freeze(mapped.slice(0, limit));
const last = candidates.at(-1);
return Object.freeze({
candidates,
truncated,
...(truncated && last
? {
next: Object.freeze({
readyAtMs: last.readyAtMs,
stepRunId: last.stepRunId,
}),
}
: {}),
});
});
}
admit(
runIdValue: string,
stepRunIdValue: string,
): Promise<
Readonly<PluginPackageWorkflowTaskAttemptAdmissionResult>
> {
const runId = identity(runIdValue, 'runId');
const stepRunId = identity(stepRunIdValue, 'stepRunId');
return this.#enqueue(() => {
let began = false;
try {
const client = this.#authority.client;
client.exec('BEGIN IMMEDIATE');
began = true;
const stepRows = client
.prepare(
`SELECT ${STEP_RUN_SELECT}
FROM "StepRuns"
WHERE run_id = ? AND id = ? LIMIT 2`,
)
.all(runId, stepRunId) as Row[];
if (stepRows.length !== 1) {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
const stepRun = stepRunFromRow(stepRows[0]!);
const existingRows = client
.prepare(
`SELECT receipt_digest AS "receiptDigest",
receipt_json AS "receiptJson"
FROM "QingLong3PluginPackageWorkflowTaskAttemptAdmissions"
WHERE run_id = ? AND step_run_id = ?
AND step_run_version = ?
LIMIT 2`,
)
.all(runId, stepRunId, stepRun.version) as Row[];
if (existingRows.length > 1) throw unavailable();
if (existingRows.length === 1) {
const receipt = receiptFromRow(existingRows[0]!);
if (
receipt.runId !== runId ||
receipt.stepRunId !== stepRunId ||
receipt.stepRunVersion !== stepRun.version ||
receipt.stepRunDigest !== stepRun.stepRunDigest
) {
throw unavailable();
}
client.exec('COMMIT');
began = false;
return Object.freeze({
status: 'existing' as const,
receipt,
});
}
if (stepRun.status !== 'ready') {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
const admissionRows = client
.prepare(
`SELECT plan_json AS "planJson"
FROM "QingLong3PluginPackageWorkflowAdmissions"
WHERE run_id = ? LIMIT 2`,
)
.all(runId) as Row[];
if (admissionRows.length !== 1) {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
let plan: Readonly<PluginPackageWorkflowExecutionPlan>;
try {
plan = normalizePluginPackageWorkflowExecutionPlan(
json(
admissionRows[0]!,
'planJson',
) as PluginPackageWorkflowExecutionPlan,
);
} catch {
throw unavailable();
}
const runRows = client
.prepare(`SELECT ${RUN_SELECT} FROM "Runs" WHERE id = ? LIMIT 2`)
.all(runId) as Row[];
if (runRows.length !== 1) {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
const run = runFromRow(runRows[0]!);
const reconciliationRows = client
.prepare(
`SELECT receipt_json AS "receiptJson"
FROM "QingLong3PluginPackageTaskReconciliations"
WHERE generation_digest = ? LIMIT 2`,
)
.all(plan.target.generationDigest) as Row[];
if (reconciliationRows.length !== 1) {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
let taskReconciliation:
Readonly<PluginPackageTaskReconciliationReceipt>;
try {
taskReconciliation =
normalizePluginPackageTaskReconciliationReceipt(
json(
reconciliationRows[0]!,
'receiptJson',
) as PluginPackageTaskReconciliationReceipt,
);
} catch {
throw unavailable();
}
const planStep = plan.steps.find(
(candidate) => candidate.stepRunId === stepRun.id,
);
if (!planStep) {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
const runtimeTaskId =
`pkg:${plan.target.packageName}:${planStep.taskId}`;
const item = taskReconciliation.items.find(
({ taskId }) => taskId === runtimeTaskId,
);
if (!item) {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
const taskRevision =
`qltd:v1:${item.revision}:${item.contentDigest}`;
const executionRows = client
.prepare(
`SELECT project_id AS "projectId", task_id AS "taskId",
task_revision AS "taskRevision",
command_json AS "commandJson",
working_directory AS "workingDirectory",
timeout_ms AS "timeoutMs", context_ref AS "contextRef",
content_digest AS "contentDigest",
created_at_ms AS "createdAtMs"
FROM "QingLong3LocalTaskExecutionRevisions"
WHERE project_id = ? AND task_id = ? AND task_revision = ?
LIMIT 2`,
)
.all(plan.target.projectId, runtimeTaskId, taskRevision) as Row[];
if (executionRows.length !== 1) {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
const execution = executionFromRow(executionRows[0]!);
const clock = client
.prepare(
`SELECT CAST(unixepoch('subsec') * 1000 AS INTEGER)
AS "admittedAtMs"`,
)
.get() as Row | undefined;
if (!clock) throw unavailable();
const attemptNumberRow = client
.prepare(
`SELECT COALESCE(MAX(attempt), 0) + 1 AS "attemptNumber"
FROM "RunAttempts" WHERE run_id = ?`,
)
.get(runId) as Row | undefined;
if (!attemptNumberRow) throw unavailable();
const bundle = createPluginPackageWorkflowTaskAttemptAdmission({
plan,
run,
stepRun,
taskReconciliation,
execution,
attemptNumber: integer(attemptNumberRow, 'attemptNumber'),
admittedAtMs: integer(clock, 'admittedAtMs'),
});
const updated = client
.prepare(
`UPDATE "Runs"
SET version = ?, event_sequence = ?
WHERE id = ? AND status = 'running'
AND cancel_requested_at_ms IS NULL
AND version = ? AND event_sequence = ?`,
)
.run(
bundle.run.version,
bundle.run.eventSequence,
run.id,
run.version,
run.eventSequence,
);
if (updated.changes !== 1) {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
const attempt = bundle.attempt;
client
.prepare(
`INSERT INTO "RunAttempts" (
id, run_id, step_run_id, attempt, status, executor_type,
callback_sequence, created_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
attempt.id,
attempt.runId,
attempt.stepRunId ?? null,
attempt.attempt,
attempt.status,
attempt.executorType,
attempt.callbackSequence,
attempt.createdAtMs,
);
const event = bundle.event;
client
.prepare(
`INSERT INTO "RunEvents" (
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
attempt_id, step_run_id, payload, created_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
event.id,
event.runId,
event.sequence,
event.type,
event.dedupeKey ?? null,
event.actorType,
event.actorId ?? null,
event.attemptId ?? null,
event.stepRunId ?? null,
JSON.stringify(event.payload),
event.createdAtMs,
);
const receipt = bundle.receipt;
client
.prepare(
`INSERT INTO
"QingLong3PluginPackageWorkflowTaskAttemptAdmissions" (
receipt_digest, attempt_id, plan_digest, run_id,
step_run_id, step_run_version, step_run_digest,
generation_digest, resource_task_id,
task_reconciliation_receipt_digest, project_id, task_id,
task_revision, task_definition_digest, executor_type,
execution_digest, attempt_number, event_id, run_version,
run_event_sequence, admitted_at_ms, receipt_json
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?
)`,
)
.run(
receipt.receiptDigest,
receipt.attemptId,
receipt.planDigest,
receipt.runId,
receipt.stepRunId,
receipt.stepRunVersion,
receipt.stepRunDigest,
plan.target.generationDigest,
receipt.resourceTaskId,
receipt.taskReconciliationReceiptDigest,
execution.projectId,
receipt.taskId,
receipt.taskRevision,
receipt.taskDefinitionDigest,
receipt.executorType,
receipt.executionDigest,
receipt.attemptNumber,
receipt.eventId,
receipt.runVersion,
receipt.runEventSequence,
receipt.admittedAtMs,
JSON.stringify(receipt),
);
client.exec('COMMIT');
began = false;
return Object.freeze({
status: 'created' as const,
receipt,
});
} finally {
if (began && this.#authority.client.isTransaction) {
try {
this.#authority.client.exec('ROLLBACK');
} catch {
// Preserve the original fail-closed error.
}
}
}
});
}
}