mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): page blocked cancellations
This commit is contained in:
@@ -5,11 +5,15 @@ export {
|
||||
RunCancellationDispatchManagementConflictError,
|
||||
RunCancellationDispatchManagementNotFoundError,
|
||||
RunCancellationDispatchManagementUnavailableError,
|
||||
RUN_CANCELLATION_DISPATCH_BLOCKED_PAGE_LIMIT,
|
||||
type BlockingCancellationDispatchResult,
|
||||
type PostgresRunCancellationDispatchBlockedListCommand,
|
||||
type PostgresRunCancellationDispatchInspectCommand,
|
||||
type PostgresRunCancellationDispatchRearmCommand,
|
||||
type PostgresRunCancellationDispatchSummaryCommand,
|
||||
type RunCancellationDispatchDiagnostic,
|
||||
type RunCancellationDispatchBlockedCursor,
|
||||
type RunCancellationDispatchBlockedPage,
|
||||
type RunCancellationDispatchRearmReceipt,
|
||||
type RunCancellationDispatchSummary,
|
||||
} from '../run-management/runCancellationDispatchManagementRepository';
|
||||
|
||||
@@ -343,5 +343,10 @@ export const postgresqlMainMigrationManifest: MigrationStreamManifest =
|
||||
checksum:
|
||||
'e78e24a06dc4c4dbdd859685f28b4bc837a8cfb279eb3512e0a57dc6d27eaaaa',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0068-cancellation-dispatch-project-keyset',
|
||||
checksum:
|
||||
'2fcac38386581189db63faacff325356f11c4529a8db9cef6be1a1ca706aaf10',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
@@ -70,6 +70,7 @@ import { pg0064PluginPackageSecretBindingTransitionApprovalPlansMigration } from
|
||||
import { pg0065ApprovedActionManualRecoveryMigration } from '../approved-action/pg-0065-approved-action-manual-recovery';
|
||||
import { pg0066CancellationDispatchMigration } from '../run/migrations/pg-0066-cancellation-dispatch';
|
||||
import { pg0067CancellationDispatchManagementMigration } from '../run-management/pg-0067-cancellation-dispatch-management';
|
||||
import { pg0068CancellationDispatchProjectKeysetMigration } from '../run-management/pg-0068-cancellation-dispatch-project-keyset';
|
||||
|
||||
export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMigrationContext> =
|
||||
Object.freeze({
|
||||
@@ -145,5 +146,6 @@ export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMi
|
||||
pg0065ApprovedActionManualRecoveryMigration,
|
||||
pg0066CancellationDispatchMigration,
|
||||
pg0067CancellationDispatchManagementMigration,
|
||||
pg0068CancellationDispatchProjectKeysetMigration,
|
||||
]),
|
||||
});
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { CAPABILITIES_V66 } from './pg-0067-cancellation-dispatch-management';
|
||||
import { definePostgresSqlMigration } from '../migrations/sqlMigration';
|
||||
|
||||
export const CAPABILITIES_V67 = CAPABILITIES_V66.replace(
|
||||
'"run_cancellation_dispatch_management":1,',
|
||||
'"run_cancellation_dispatch_blocked_list":1,"run_cancellation_dispatch_management":1,',
|
||||
);
|
||||
|
||||
export const pg0068CancellationDispatchProjectKeysetMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0068-cancellation-dispatch-project-keyset',
|
||||
statements: [
|
||||
`ALTER TABLE "ql3"."run_cancellation_dispatches" ADD COLUMN project_id varchar(128)`,
|
||||
`UPDATE "ql3"."run_cancellation_dispatches" AS dispatch SET project_id = run.project_id FROM "ql3"."runs" AS run WHERE run.id = dispatch.run_id`,
|
||||
`ALTER TABLE "ql3"."run_cancellation_dispatches" ALTER COLUMN project_id SET NOT NULL`,
|
||||
`CREATE UNIQUE INDEX ql3_runs_project_id_uidx ON "ql3"."runs" (project_id, id)`,
|
||||
`ALTER TABLE "ql3"."run_cancellation_dispatches" DROP CONSTRAINT ql3_run_cancellation_dispatch_run_fk`,
|
||||
`ALTER TABLE "ql3"."run_cancellation_dispatches" ADD CONSTRAINT ql3_run_cancellation_dispatch_run_fk FOREIGN KEY (project_id, run_id) REFERENCES "ql3"."runs" (project_id, id) ON DELETE CASCADE ON UPDATE RESTRICT`,
|
||||
`CREATE INDEX ql3_run_cancellation_dispatch_project_blocked_idx ON "ql3"."run_cancellation_dispatches" (project_id, updated_at_ms, run_id) WHERE status = 'blocked'`,
|
||||
`DO $ql3$ BEGIN UPDATE "ql3"."schema_capabilities" SET contract_version = 67, migration_id = 'pg-0068-cancellation-dispatch-project-keyset', capabilities = '${CAPABILITIES_V67}'::jsonb, updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint WHERE contract_name = 'control-core' AND contract_version = 66 AND migration_id = 'pg-0067-cancellation-dispatch-management' AND capabilities = '${CAPABILITIES_V66}'::jsonb; IF NOT FOUND THEN RAISE EXCEPTION 'control-core capability is not at version 66' USING ERRCODE = 'check_violation'; END IF; END $ql3$`,
|
||||
],
|
||||
});
|
||||
+187
@@ -7,6 +7,7 @@ import {
|
||||
} from '@qinglong/runtime-core';
|
||||
import {
|
||||
CANCELLATION_DISPATCH_BLOCKING_RESULTS,
|
||||
CANCELLATION_DISPATCH_BLOCKED_PAGE_LIMIT,
|
||||
CANCELLATION_DISPATCH_RESULTS,
|
||||
CANCELLATION_DISPATCH_STATUSES,
|
||||
MAX_CANCELLATION_DISPATCH_RETRY_DELAY_MS,
|
||||
@@ -83,6 +84,27 @@ export type RunCancellationDispatchSummary = Readonly<{
|
||||
oldestBlockedAtMs?: number;
|
||||
}>;
|
||||
|
||||
export const RUN_CANCELLATION_DISPATCH_BLOCKED_PAGE_LIMIT =
|
||||
CANCELLATION_DISPATCH_BLOCKED_PAGE_LIMIT;
|
||||
|
||||
export type RunCancellationDispatchBlockedCursor = Readonly<{
|
||||
snapshotAtMs: number;
|
||||
blockedAtMs: number;
|
||||
runId: string;
|
||||
}>;
|
||||
|
||||
export type RunCancellationDispatchBlockedPage = Readonly<{
|
||||
projectId: string;
|
||||
snapshotAtMs: number;
|
||||
observedAtMs: number;
|
||||
items: readonly Readonly<{
|
||||
runId: string;
|
||||
blockedAtMs: number;
|
||||
}>[];
|
||||
truncated: boolean;
|
||||
nextCursor?: Readonly<RunCancellationDispatchBlockedCursor>;
|
||||
}>;
|
||||
|
||||
interface ProjectManagementAuthority {
|
||||
readonly projectId: string;
|
||||
readonly requestId: string;
|
||||
@@ -98,6 +120,11 @@ interface ManagementAuthority extends ProjectManagementAuthority {
|
||||
export interface PostgresRunCancellationDispatchSummaryCommand
|
||||
extends ProjectManagementAuthority {}
|
||||
|
||||
export interface PostgresRunCancellationDispatchBlockedListCommand
|
||||
extends ProjectManagementAuthority {
|
||||
readonly after?: Readonly<RunCancellationDispatchBlockedCursor>;
|
||||
}
|
||||
|
||||
export interface PostgresRunCancellationDispatchInspectCommand
|
||||
extends ManagementAuthority {}
|
||||
|
||||
@@ -347,6 +374,48 @@ function normalizeSummaryCommand(
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeBlockedCursor(
|
||||
value: unknown,
|
||||
): Readonly<RunCancellationDispatchBlockedCursor> {
|
||||
const cursor = exact(value, ['snapshotAtMs', 'blockedAtMs', 'runId']);
|
||||
const snapshotAtMs = boundedInteger(cursor.snapshotAtMs, 0);
|
||||
const blockedAtMs = boundedInteger(cursor.blockedAtMs, 0, snapshotAtMs);
|
||||
return Object.freeze({
|
||||
snapshotAtMs,
|
||||
blockedAtMs,
|
||||
runId: identifier(cursor.runId),
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeBlockedListCommand(
|
||||
value: Readonly<PostgresRunCancellationDispatchBlockedListCommand>,
|
||||
): Readonly<PostgresRunCancellationDispatchBlockedListCommand> {
|
||||
const hasAfter =
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
!Array.isArray(value) &&
|
||||
Object.hasOwn(value, 'after');
|
||||
const input = exact(value, [
|
||||
'projectId',
|
||||
'requestId',
|
||||
'auditEventId',
|
||||
'principal',
|
||||
'policyFence',
|
||||
...(hasAfter ? ['after'] : []),
|
||||
]);
|
||||
const authority = normalizeSummaryCommand({
|
||||
projectId: input.projectId as string,
|
||||
requestId: input.requestId as string,
|
||||
auditEventId: input.auditEventId as string,
|
||||
principal: input.principal as SecurityPrincipal,
|
||||
policyFence: input.policyFence as SecurityPolicyFence,
|
||||
});
|
||||
return Object.freeze({
|
||||
...authority,
|
||||
...(hasAfter ? { after: normalizeBlockedCursor(input.after) } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeRearmCommand(
|
||||
value: Readonly<PostgresRunCancellationDispatchRearmCommand>,
|
||||
): Readonly<PostgresRunCancellationDispatchRearmCommand> {
|
||||
@@ -474,6 +543,7 @@ async function recordAllowedAudit(
|
||||
command: Readonly<ProjectManagementAuthority>,
|
||||
operationId:
|
||||
| 'run.cancellation.summary'
|
||||
| 'run.cancellation.blocked.list'
|
||||
| 'run.cancellation.inspect'
|
||||
| 'run.cancellation.rearm',
|
||||
observedAtMs: number,
|
||||
@@ -603,6 +673,76 @@ function summaryProjection(
|
||||
});
|
||||
}
|
||||
|
||||
function storedIdentifier(row: Row, key: string): string {
|
||||
const value = text(row, key);
|
||||
if (!IDENTIFIER_PATTERN.test(value)) {
|
||||
throw new TypeError(
|
||||
`PostgreSQL cancellation management ${key} is invalid`,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function blockedPageProjection(
|
||||
command: Readonly<PostgresRunCancellationDispatchBlockedListCommand>,
|
||||
observedAtMs: number,
|
||||
snapshotAtMs: number,
|
||||
rows: readonly Row[],
|
||||
): Readonly<RunCancellationDispatchBlockedPage> {
|
||||
if (rows.length > RUN_CANCELLATION_DISPATCH_BLOCKED_PAGE_LIMIT + 1) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL cancellation management blocked page is invalid',
|
||||
);
|
||||
}
|
||||
const projected = rows.map((row) =>
|
||||
Object.freeze({
|
||||
runId: storedIdentifier(row, 'runId'),
|
||||
blockedAtMs: integer(row, 'blockedAtMs'),
|
||||
}),
|
||||
);
|
||||
let previous = command.after;
|
||||
for (const item of projected) {
|
||||
if (
|
||||
item.blockedAtMs > snapshotAtMs ||
|
||||
(previous !== undefined &&
|
||||
(item.blockedAtMs < previous.blockedAtMs ||
|
||||
(item.blockedAtMs === previous.blockedAtMs &&
|
||||
item.runId <= previous.runId)))
|
||||
) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL cancellation management blocked cursor order is invalid',
|
||||
);
|
||||
}
|
||||
previous = Object.freeze({
|
||||
snapshotAtMs,
|
||||
blockedAtMs: item.blockedAtMs,
|
||||
runId: item.runId,
|
||||
});
|
||||
}
|
||||
const truncated =
|
||||
projected.length > RUN_CANCELLATION_DISPATCH_BLOCKED_PAGE_LIMIT;
|
||||
const items = Object.freeze(
|
||||
projected.slice(0, RUN_CANCELLATION_DISPATCH_BLOCKED_PAGE_LIMIT),
|
||||
);
|
||||
const last = items.at(-1);
|
||||
return Object.freeze({
|
||||
projectId: command.projectId,
|
||||
snapshotAtMs,
|
||||
observedAtMs,
|
||||
items,
|
||||
truncated,
|
||||
...(truncated && last
|
||||
? {
|
||||
nextCursor: Object.freeze({
|
||||
snapshotAtMs,
|
||||
blockedAtMs: last.blockedAtMs,
|
||||
runId: last.runId,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
function runStatus(row: Row): RunStatus {
|
||||
const value = text(row, 'runStatus') as RunStatus;
|
||||
if (!RUN_STATUSES.includes(value)) {
|
||||
@@ -818,6 +958,53 @@ export class PostgresRunCancellationDispatchManagementRepository {
|
||||
});
|
||||
}
|
||||
|
||||
listBlocked(
|
||||
value: Readonly<PostgresRunCancellationDispatchBlockedListCommand>,
|
||||
): Promise<Readonly<RunCancellationDispatchBlockedPage>> {
|
||||
const command = normalizeBlockedListCommand(value);
|
||||
return this.transaction(async (client) => {
|
||||
const observedAtMs = await databaseNow(client);
|
||||
const snapshotAtMs = command.after?.snapshotAtMs ?? observedAtMs;
|
||||
if (snapshotAtMs > observedAtMs) {
|
||||
throw new InvalidRunCancellationDispatchManagementError();
|
||||
}
|
||||
const authorized = Object.freeze({
|
||||
...command,
|
||||
principal: strongPrincipal(command.principal, observedAtMs),
|
||||
});
|
||||
await confirmAuthorization(client, authorized);
|
||||
const result = await client.query<Row>(
|
||||
`SELECT run_id AS "runId", updated_at_ms AS "blockedAtMs"
|
||||
FROM "ql3"."run_cancellation_dispatches"
|
||||
WHERE project_id = $1 AND status = 'blocked'
|
||||
AND updated_at_ms <= $2
|
||||
AND ($3::bigint IS NULL OR
|
||||
(updated_at_ms, run_id) > ($3::bigint, $4::varchar))
|
||||
ORDER BY updated_at_ms ASC, run_id ASC
|
||||
LIMIT $5`,
|
||||
[
|
||||
command.projectId,
|
||||
snapshotAtMs,
|
||||
command.after?.blockedAtMs ?? null,
|
||||
command.after?.runId ?? '',
|
||||
RUN_CANCELLATION_DISPATCH_BLOCKED_PAGE_LIMIT + 1,
|
||||
],
|
||||
);
|
||||
await recordAllowedAudit(
|
||||
client,
|
||||
authorized,
|
||||
'run.cancellation.blocked.list',
|
||||
observedAtMs,
|
||||
);
|
||||
return blockedPageProjection(
|
||||
command,
|
||||
observedAtMs,
|
||||
snapshotAtMs,
|
||||
result.rows,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
inspect(
|
||||
value: Readonly<PostgresRunCancellationDispatchInspectCommand>,
|
||||
): Promise<Readonly<RunCancellationDispatchDiagnostic>> {
|
||||
|
||||
@@ -180,7 +180,7 @@ export class PostgresCancellationDispatchRepository
|
||||
return this.transaction(async (client) => {
|
||||
const nowMs = await databaseNow(client);
|
||||
const run = await client.query<Row>(
|
||||
`SELECT execution_owner AS "executionOwner", status,
|
||||
`SELECT project_id AS "projectId", execution_owner AS "executionOwner", status,
|
||||
cancel_requested_at_ms AS "cancelRequestedAtMs"
|
||||
FROM "ql3"."runs" WHERE id = $1 FOR UPDATE`,
|
||||
[command.runId],
|
||||
@@ -222,11 +222,17 @@ export class PostgresCancellationDispatchRepository
|
||||
if (dispatchResult.rows.length === 0) {
|
||||
dispatchResult = await client.query<Row>(
|
||||
`INSERT INTO "ql3"."run_cancellation_dispatches" (
|
||||
run_id, attempt_id, status, version, dispatch_count,
|
||||
project_id, run_id, attempt_id, status, version, dispatch_count,
|
||||
next_attempt_at_ms, created_at_ms, updated_at_ms
|
||||
) VALUES ($1, $2, 'pending', 0, 0, $3, $4, $4)
|
||||
) VALUES ($5, $1, $2, 'pending', 0, 0, $3, $4, $4)
|
||||
RETURNING ${DISPATCH_COLUMNS}`,
|
||||
[command.runId, command.attemptId, command.requestedAtMs, nowMs],
|
||||
[
|
||||
command.runId,
|
||||
command.attemptId,
|
||||
command.requestedAtMs,
|
||||
nowMs,
|
||||
text(runRow, 'projectId'),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (dispatchResult.rows.length !== 1) {
|
||||
|
||||
@@ -3592,6 +3592,7 @@ export const runs = ql3Schema.table(
|
||||
uniqueIndex('ql3_runs_project_idempotency_uidx')
|
||||
.on(table.projectId, table.idempotencyKey)
|
||||
.where(sql`${table.idempotencyKey} is not null`),
|
||||
uniqueIndex('ql3_runs_project_id_uidx').on(table.projectId, table.id),
|
||||
index('ql3_runs_project_created_idx').on(
|
||||
table.projectId,
|
||||
table.createdAtMs,
|
||||
@@ -4834,6 +4835,7 @@ export const runAttempts = ql3Schema.table(
|
||||
export const runCancellationDispatches = ql3Schema.table(
|
||||
'run_cancellation_dispatches',
|
||||
{
|
||||
projectId: varchar('project_id', { length: 128 }).notNull(),
|
||||
runId: varchar('run_id', { length: 36 }).primaryKey(),
|
||||
attemptId: varchar('attempt_id', { length: 36 }).notNull(),
|
||||
status: varchar('status', { length: 32 }).notNull(),
|
||||
@@ -4851,8 +4853,8 @@ export const runCancellationDispatches = ql3Schema.table(
|
||||
(table) => [
|
||||
foreignKey({
|
||||
name: 'ql3_run_cancellation_dispatch_run_fk',
|
||||
columns: [table.runId],
|
||||
foreignColumns: [runs.id],
|
||||
columns: [table.projectId, table.runId],
|
||||
foreignColumns: [runs.projectId, runs.id],
|
||||
})
|
||||
.onDelete('cascade')
|
||||
.onUpdate('restrict'),
|
||||
@@ -4897,6 +4899,9 @@ export const runCancellationDispatches = ql3Schema.table(
|
||||
index('ql3_run_cancellation_dispatch_lease_expiry_idx')
|
||||
.on(table.leaseExpiresAtMs, table.runId)
|
||||
.where(sql`${table.status} = 'leased'`),
|
||||
index('ql3_run_cancellation_dispatch_project_blocked_idx')
|
||||
.on(table.projectId, table.updatedAtMs, table.runId)
|
||||
.where(sql`${table.status} = 'blocked'`),
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -21,13 +21,14 @@ export interface PostgresSchemaContractTrigger {
|
||||
export interface PostgresSchemaContract {
|
||||
readonly schema: 'ql3';
|
||||
readonly contractName: 'control-core';
|
||||
readonly contractVersion: 66;
|
||||
readonly migrationId: 'pg-0067-cancellation-dispatch-management';
|
||||
readonly contractVersion: 67;
|
||||
readonly migrationId: 'pg-0068-cancellation-dispatch-project-keyset';
|
||||
readonly minimumServerMajor: 16;
|
||||
readonly maximumServerMajor: 18;
|
||||
readonly capabilities: Readonly<{
|
||||
run_core: 1;
|
||||
run_cancellation_dispatch: 1;
|
||||
run_cancellation_dispatch_blocked_list: 1;
|
||||
run_cancellation_dispatch_management: 1;
|
||||
run_attempt_log_retention: 1;
|
||||
run_management_boundary: 1;
|
||||
@@ -120,8 +121,8 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
Object.freeze({
|
||||
schema: 'ql3',
|
||||
contractName: 'control-core',
|
||||
contractVersion: 66,
|
||||
migrationId: 'pg-0067-cancellation-dispatch-management',
|
||||
contractVersion: 67,
|
||||
migrationId: 'pg-0068-cancellation-dispatch-project-keyset',
|
||||
minimumServerMajor: 16,
|
||||
maximumServerMajor: 18,
|
||||
capabilities: Object.freeze({
|
||||
@@ -170,6 +171,7 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
project_tool_definition_snapshot: 1,
|
||||
run_core: 1,
|
||||
run_cancellation_dispatch: 1,
|
||||
run_cancellation_dispatch_blocked_list: 1,
|
||||
run_cancellation_dispatch_management: 1,
|
||||
run_attempt_log_retention: 1,
|
||||
run_management_boundary: 1,
|
||||
@@ -1294,6 +1296,7 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
'error_summary',
|
||||
]),
|
||||
table('run_cancellation_dispatches', [
|
||||
'project_id',
|
||||
'run_id',
|
||||
'attempt_id',
|
||||
'status',
|
||||
@@ -1718,6 +1721,7 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
'ql3_api_credential_mutations_actor_idx',
|
||||
'runs_pkey',
|
||||
'ql3_runs_project_idempotency_uidx',
|
||||
'ql3_runs_project_id_uidx',
|
||||
'ql3_runs_project_created_idx',
|
||||
'ql3_runs_task_created_idx',
|
||||
'ql3_runs_dispatch_candidates_idx',
|
||||
@@ -1796,6 +1800,7 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
'run_cancellation_dispatches_pkey',
|
||||
'ql3_run_cancellation_dispatch_due_idx',
|
||||
'ql3_run_cancellation_dispatch_lease_expiry_idx',
|
||||
'ql3_run_cancellation_dispatch_project_blocked_idx',
|
||||
'run_attempt_log_retention_controls_pkey',
|
||||
'ql3_run_log_retention_control_artifact_key',
|
||||
'ql3_run_log_retention_retry_idx',
|
||||
|
||||
@@ -118,6 +118,7 @@ test('defines the immutable PostgreSQL capability and Run core stream', async ()
|
||||
'pg-0065-approved-action-manual-recovery',
|
||||
'pg-0066-cancellation-dispatch',
|
||||
'pg-0067-cancellation-dispatch-management',
|
||||
'pg-0068-cancellation-dispatch-project-keyset',
|
||||
],
|
||||
);
|
||||
for (const migration of postgresqlMainMigrationStream.migrations) {
|
||||
@@ -591,6 +592,11 @@ test('freezes every published PostgreSQL migration checksum', () => {
|
||||
checksum:
|
||||
'e78e24a06dc4c4dbdd859685f28b4bc837a8cfb279eb3512e0a57dc6d27eaaaa',
|
||||
},
|
||||
{
|
||||
id: 'pg-0068-cancellation-dispatch-project-keyset',
|
||||
checksum:
|
||||
'2fcac38386581189db63faacff325356f11c4529a8db9cef6be1a1ca706aaf10',
|
||||
},
|
||||
];
|
||||
assert.deepEqual(
|
||||
postgresqlMainMigrationStream.migrations.map(({ id, checksum }) => ({
|
||||
@@ -2373,3 +2379,45 @@ test('advances capability v66 with least-privilege cancellation diagnostics and
|
||||
assert.match(sql, /contract_version = 65/);
|
||||
assert.match(sql, /migration_id = 'pg-0066-cancellation-dispatch'/);
|
||||
});
|
||||
|
||||
test('advances capability v67 with a Project-scoped blocked keyset', async () => {
|
||||
const migration = migrationById(
|
||||
'pg-0068-cancellation-dispatch-project-keyset',
|
||||
);
|
||||
const statements = [];
|
||||
await migration.up({
|
||||
async query(statement) {
|
||||
statements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const sql = statements.join('\n');
|
||||
assert.match(
|
||||
sql,
|
||||
/ADD COLUMN project_id varchar\(128\)/,
|
||||
);
|
||||
assert.match(
|
||||
sql,
|
||||
/SET project_id = run\.project_id FROM "ql3"\."runs" AS run/,
|
||||
);
|
||||
assert.match(sql, /ALTER COLUMN project_id SET NOT NULL/);
|
||||
assert.match(
|
||||
sql,
|
||||
/CREATE UNIQUE INDEX ql3_runs_project_id_uidx ON "ql3"\."runs" \(project_id, id\)/,
|
||||
);
|
||||
assert.match(
|
||||
sql,
|
||||
/FOREIGN KEY \(project_id, run_id\) REFERENCES "ql3"\."runs" \(project_id, id\)/,
|
||||
);
|
||||
assert.match(
|
||||
sql,
|
||||
/CREATE INDEX ql3_run_cancellation_dispatch_project_blocked_idx[\s\S]+\(project_id, updated_at_ms, run_id\) WHERE status = 'blocked'/,
|
||||
);
|
||||
assert.match(sql, /contract_version = 67/);
|
||||
assert.match(sql, /"run_cancellation_dispatch_blocked_list":1/);
|
||||
assert.match(sql, /contract_version = 66/);
|
||||
assert.match(
|
||||
sql,
|
||||
/migration_id = 'pg-0067-cancellation-dispatch-management'/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -835,7 +835,7 @@ test('accepts the exact PostgreSQL control schema and least-privilege runtime ro
|
||||
serverMajor: 16,
|
||||
currentUser: 'ql3_runtime',
|
||||
contractName: 'control-core',
|
||||
contractVersion: 66,
|
||||
contractVersion: 67,
|
||||
migrationIds: [
|
||||
'pg-0001-schema-capability',
|
||||
'pg-0002-run-core',
|
||||
@@ -904,6 +904,7 @@ test('accepts the exact PostgreSQL control schema and least-privilege runtime ro
|
||||
'pg-0065-approved-action-manual-recovery',
|
||||
'pg-0066-cancellation-dispatch',
|
||||
'pg-0067-cancellation-dispatch-management',
|
||||
'pg-0068-cancellation-dispatch-project-keyset',
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -934,10 +935,10 @@ test('accepts the exact schema and isolated least-privilege admin role', async (
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_admin');
|
||||
assert.equal(report.contractVersion, 66);
|
||||
assert.equal(report.contractVersion, 67);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0067-cancellation-dispatch-management',
|
||||
'pg-0068-cancellation-dispatch-project-keyset',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -950,10 +951,10 @@ test('accepts the isolated least-privilege automation manager role', async () =>
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_automation_manager');
|
||||
assert.equal(report.contractVersion, 66);
|
||||
assert.equal(report.contractVersion, 67);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0067-cancellation-dispatch-management',
|
||||
'pg-0068-cancellation-dispatch-project-keyset',
|
||||
);
|
||||
|
||||
const widened = automationManagerPrivileges();
|
||||
@@ -982,10 +983,10 @@ test('accepts the isolated least-privilege human Approval manager role', async (
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_approval_manager');
|
||||
assert.equal(report.contractVersion, 66);
|
||||
assert.equal(report.contractVersion, 67);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0067-cancellation-dispatch-management',
|
||||
'pg-0068-cancellation-dispatch-project-keyset',
|
||||
);
|
||||
|
||||
const widened = approvalManagerPrivileges();
|
||||
@@ -1016,10 +1017,10 @@ test('accepts the isolated least-privilege Run manager role', async () => {
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_run_manager');
|
||||
assert.equal(report.contractVersion, 66);
|
||||
assert.equal(report.contractVersion, 67);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0067-cancellation-dispatch-management',
|
||||
'pg-0068-cancellation-dispatch-project-keyset',
|
||||
);
|
||||
|
||||
const widened = runManagerPrivileges();
|
||||
@@ -1180,10 +1181,10 @@ test('accepts the exact schema and isolated Worker ingress role', async () => {
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_worker_ingress');
|
||||
assert.equal(report.contractVersion, 66);
|
||||
assert.equal(report.contractVersion, 67);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0067-cancellation-dispatch-management',
|
||||
'pg-0068-cancellation-dispatch-project-keyset',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
+81
@@ -47,6 +47,16 @@ function summaryCommand(overrides = {}) {
|
||||
return { ...authority, ...overrides };
|
||||
}
|
||||
|
||||
function blockedListCommand(after) {
|
||||
return {
|
||||
...summaryCommand({
|
||||
requestId: 'request-blocked-1',
|
||||
auditEventId: '019f9600-0000-4000-8000-000000000021',
|
||||
}),
|
||||
...(after === undefined ? {} : { after }),
|
||||
};
|
||||
}
|
||||
|
||||
function runRow() {
|
||||
return {
|
||||
projectId: 'project-1',
|
||||
@@ -133,6 +143,13 @@ function fixture(options = {}) {
|
||||
rowCount: 1,
|
||||
};
|
||||
}
|
||||
if (
|
||||
text.startsWith(
|
||||
'SELECT run_id AS "runId", updated_at_ms AS "blockedAtMs"',
|
||||
)
|
||||
) {
|
||||
return { rows: options.blockedRows ?? [], rowCount: 0 };
|
||||
}
|
||||
if (
|
||||
text.startsWith('SELECT attempt_id AS "attemptId"') &&
|
||||
!text.includes('dispatchStatus') &&
|
||||
@@ -296,6 +313,70 @@ test('derives clear and converging assessments from fixed status counts', async
|
||||
assert.equal(result.operatorAction, 'wait');
|
||||
});
|
||||
|
||||
test('lists one fixed oldest-first blocked page with a snapshot cursor', async () => {
|
||||
const blockedRows = Array.from({ length: 17 }, (_, index) => ({
|
||||
runId: `run-${String(index + 1).padStart(2, '0')}`,
|
||||
blockedAtMs: String(NOW - 100 + index),
|
||||
}));
|
||||
const { calls, repository } = fixture({ blockedRows });
|
||||
const result = await repository.listBlocked(blockedListCommand());
|
||||
assert.equal(result.projectId, 'project-1');
|
||||
assert.equal(result.snapshotAtMs, NOW);
|
||||
assert.equal(result.observedAtMs, NOW);
|
||||
assert.equal(result.items.length, 16);
|
||||
assert.equal(result.items[0].runId, 'run-01');
|
||||
assert.equal(result.items[15].runId, 'run-16');
|
||||
assert.equal(result.truncated, true);
|
||||
assert.deepEqual(result.nextCursor, {
|
||||
snapshotAtMs: NOW,
|
||||
blockedAtMs: NOW - 85,
|
||||
runId: 'run-16',
|
||||
});
|
||||
const read = calls.find(({ sql }) =>
|
||||
sql.startsWith(
|
||||
'SELECT run_id AS "runId", updated_at_ms AS "blockedAtMs"',
|
||||
),
|
||||
);
|
||||
assert.deepEqual(read.params, ['project-1', NOW, null, '', 17]);
|
||||
assert.match(
|
||||
read.sql,
|
||||
/project_id = \$1 AND status = 'blocked'[\s\S]+ORDER BY updated_at_ms ASC, run_id ASC/,
|
||||
);
|
||||
const audit = calls.find(
|
||||
({ sql, params }) =>
|
||||
sql.startsWith('INSERT INTO "ql3"."security_audit_events"') &&
|
||||
params[2] === 'run.cancellation.blocked.list',
|
||||
);
|
||||
assert.equal(audit.params[0], blockedListCommand().auditEventId);
|
||||
});
|
||||
|
||||
test('continues only inside the original blocked snapshot', async () => {
|
||||
const after = {
|
||||
snapshotAtMs: NOW - 50,
|
||||
blockedAtMs: NOW - 80,
|
||||
runId: 'run-03',
|
||||
};
|
||||
const { calls, repository } = fixture({
|
||||
blockedRows: [{ runId: 'run-04', blockedAtMs: String(NOW - 79) }],
|
||||
});
|
||||
const result = await repository.listBlocked(blockedListCommand(after));
|
||||
assert.equal(result.snapshotAtMs, NOW - 50);
|
||||
assert.equal(result.truncated, false);
|
||||
assert.equal(Object.hasOwn(result, 'nextCursor'), false);
|
||||
const read = calls.find(({ sql }) =>
|
||||
sql.startsWith(
|
||||
'SELECT run_id AS "runId", updated_at_ms AS "blockedAtMs"',
|
||||
),
|
||||
);
|
||||
assert.deepEqual(read.params, [
|
||||
'project-1',
|
||||
NOW - 50,
|
||||
NOW - 80,
|
||||
'run-03',
|
||||
17,
|
||||
]);
|
||||
});
|
||||
|
||||
test('rearms an exact blocked dispatch with one event and allowed audit', async () => {
|
||||
const { calls, repository } = fixture();
|
||||
const result = await repository.rearm(rearmCommand());
|
||||
|
||||
Reference in New Issue
Block a user