feat(ql3): add cluster log retention authority

This commit is contained in:
whyour
2026-08-12 03:38:27 +08:00
parent 2bfa8ca279
commit 40628c843d
20 changed files with 1737 additions and 19 deletions
@@ -4474,6 +4474,162 @@ export const runAttempts = ql3Schema.table(
index('ql3_run_attempts_lease_idx')
.on(table.leaseExpiresAtMs, table.id)
.where(sql`${table.leaseExpiresAtMs} is not null`),
index('ql3_run_log_retention_candidate_idx')
.on(table.finishedAtMs, table.id)
.where(
sql`${table.executorType} = 'remote_worker' and ${table.logArtifactId} is not null and ${table.status} in ('succeeded', 'failed', 'cancelled', 'timed_out')`,
),
],
);
export const runAttemptLogRetentionControls = ql3Schema.table(
'run_attempt_log_retention_controls',
{
attemptId: varchar('attempt_id', { length: 36 }).primaryKey(),
projectId: varchar('project_id', { length: 128 }).notNull(),
runId: varchar('run_id', { length: 36 }).notNull(),
logArtifactId: varchar('log_artifact_id', { length: 36 }).notNull(),
executorType: varchar('executor_type', { length: 32 }).notNull(),
finishedAtMs: bigint('finished_at_ms', { mode: 'number' }).notNull(),
eligibleAtMs: bigint('eligible_at_ms', { mode: 'number' }).notNull(),
state: varchar('state', { length: 16 }).notNull(),
claimOwner: varchar('claim_owner', { length: 128 }),
claimToken: varchar('claim_token', { length: 64 }),
claimVersion: integer('claim_version').default(1).notNull(),
claimExpiresAtMs: bigint('claim_expires_at_ms', { mode: 'number' }),
nextClaimAtMs: bigint('next_claim_at_ms', { mode: 'number' }),
failureCount: integer('failure_count').default(0).notNull(),
lastFailureCode: varchar('last_failure_code', { length: 64 }),
createdAtMs: bigint('created_at_ms', { mode: 'number' }).notNull(),
updatedAtMs: bigint('updated_at_ms', { mode: 'number' }).notNull(),
},
(table) => [
uniqueIndex('ql3_run_log_retention_control_artifact_key').on(
table.logArtifactId,
),
check(
'ql3_run_log_retention_control_identity_check',
sql`char_length(${table.projectId}) between 1 and 128 and char_length(${table.runId}) between 1 and 36 and char_length(${table.attemptId}) between 1 and 36 and ${table.logArtifactId} ~ '^wlog-[a-f0-9]{30}$' and ${table.executorType} = 'remote_worker'`,
),
check(
'ql3_run_log_retention_control_time_check',
sql`${table.finishedAtMs} >= 0 and ${table.eligibleAtMs} >= ${table.finishedAtMs} and ${table.createdAtMs} >= 0 and ${table.updatedAtMs} >= ${table.createdAtMs}`,
),
check(
'ql3_run_log_retention_control_state_check',
sql`${table.state} in ('claimed', 'retry', 'manual')`,
),
check(
'ql3_run_log_retention_control_claim_owner_check',
sql`${table.claimOwner} is null or ${table.claimOwner} ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'`,
),
check(
'ql3_run_log_retention_control_claim_token_check',
sql`${table.claimToken} is null or ${table.claimToken} ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{15,63}$'`,
),
check(
'ql3_run_log_retention_control_claim_version_check',
sql`${table.claimVersion} between 1 and 2147483647`,
),
check(
'ql3_run_log_retention_control_claim_expiry_check',
sql`${table.claimExpiresAtMs} is null or ${table.claimExpiresAtMs} >= 0`,
),
check(
'ql3_run_log_retention_control_next_claim_check',
sql`${table.nextClaimAtMs} is null or ${table.nextClaimAtMs} >= 0`,
),
check(
'ql3_run_log_retention_control_failure_count_check',
sql`${table.failureCount} between 0 and 2147483647`,
),
check(
'ql3_run_log_retention_control_failure_code_check',
sql`${table.lastFailureCode} is null or ${table.lastFailureCode} in ('artifact_unavailable', 'artifact_integrity_mismatch', 'retirement_record_unavailable')`,
),
check(
'ql3_run_log_retention_control_state_shape_check',
sql`(${table.state} = 'claimed' and ${table.claimOwner} is not null and ${table.claimToken} is not null and ${table.claimExpiresAtMs} is not null and ${table.nextClaimAtMs} is null) or (${table.state} = 'retry' and ${table.claimOwner} is null and ${table.claimToken} is null and ${table.claimExpiresAtMs} is null and ${table.nextClaimAtMs} is not null and ${table.lastFailureCode} is not null) or (${table.state} = 'manual' and ${table.claimOwner} is null and ${table.claimToken} is null and ${table.claimExpiresAtMs} is null and ${table.nextClaimAtMs} is null and ${table.lastFailureCode} is not null)`,
),
foreignKey({
name: 'ql3_run_log_retention_control_attempt_fk',
columns: [table.attemptId],
foreignColumns: [runAttempts.id],
}).onDelete('cascade'),
foreignKey({
name: 'ql3_run_log_retention_control_run_fk',
columns: [table.runId],
foreignColumns: [runs.id],
}).onDelete('cascade'),
index('ql3_run_log_retention_retry_idx')
.on(table.nextClaimAtMs, table.finishedAtMs, table.attemptId)
.where(sql`${table.state} = 'retry'`),
index('ql3_run_log_retention_claim_expiry_idx')
.on(table.claimExpiresAtMs, table.finishedAtMs, table.attemptId)
.where(sql`${table.state} = 'claimed'`),
],
);
export const runAttemptLogArtifactTombstones = ql3Schema.table(
'run_attempt_log_artifact_tombstones',
{
logArtifactId: varchar('log_artifact_id', { length: 36 }).primaryKey(),
projectId: varchar('project_id', { length: 128 }).notNull(),
runId: varchar('run_id', { length: 36 }).notNull(),
attemptId: varchar('attempt_id', { length: 36 }).notNull(),
executorType: varchar('executor_type', { length: 32 }).notNull(),
finishedAtMs: bigint('finished_at_ms', { mode: 'number' }).notNull(),
eligibleAtMs: bigint('eligible_at_ms', { mode: 'number' }).notNull(),
retiredAtMs: bigint('retired_at_ms', { mode: 'number' }).notNull(),
disposition: varchar('disposition', { length: 16 }).notNull(),
byteLength: bigint('byte_length', { mode: 'number' }).notNull(),
truncated: varchar('truncated', { length: 16 }).notNull(),
maximumBytes: bigint('maximum_bytes', { mode: 'number' }),
truncationObservedAtMs: bigint('truncation_observed_at_ms', {
mode: 'number',
}),
recordDigest: char('record_digest', { length: 64 }).notNull(),
},
(table) => [
uniqueIndex('ql3_run_log_tombstone_attempt_key').on(table.attemptId),
check(
'ql3_run_log_tombstone_identity_check',
sql`char_length(${table.projectId}) between 1 and 128 and char_length(${table.runId}) between 1 and 36 and char_length(${table.attemptId}) between 1 and 36 and ${table.logArtifactId} ~ '^wlog-[a-f0-9]{30}$' and ${table.executorType} = 'remote_worker'`,
),
check(
'ql3_run_log_tombstone_time_check',
sql`${table.finishedAtMs} >= 0 and ${table.eligibleAtMs} >= ${table.finishedAtMs} and ${table.retiredAtMs} >= ${table.eligibleAtMs}`,
),
check(
'ql3_run_log_tombstone_disposition_check',
sql`${table.disposition} in ('deleted', 'already_absent') and (${table.disposition} <> 'already_absent' or ${table.byteLength} = 0)`,
),
check(
'ql3_run_log_tombstone_size_check',
sql`${table.byteLength} between 0 and 1073741824`,
),
check(
'ql3_run_log_tombstone_truncation_check',
sql`(${table.truncated} = 'unknown' and ${table.maximumBytes} is null and ${table.truncationObservedAtMs} is null) or (${table.truncated} in ('true', 'false') and ${table.maximumBytes} >= 1 and ${table.truncationObservedAtMs} >= 0)`,
),
check(
'ql3_run_log_tombstone_digest_check',
sql`${table.recordDigest} ~ '^[a-f0-9]{64}$'`,
),
foreignKey({
name: 'ql3_run_log_tombstone_attempt_fk',
columns: [table.attemptId],
foreignColumns: [runAttempts.id],
}).onDelete('cascade'),
foreignKey({
name: 'ql3_run_log_tombstone_run_fk',
columns: [table.runId],
foreignColumns: [runs.id],
}).onDelete('cascade'),
index('ql3_run_log_tombstone_retired_idx').on(
table.retiredAtMs,
table.attemptId,
),
],
);
@@ -5700,6 +5856,8 @@ export const ql3PostgresTables = [
toolExecutionResultRekeyHeads,
toolResultKeyRetirementReceipts,
runAttempts,
runAttemptLogRetentionControls,
runAttemptLogArtifactTombstones,
workerSessions,
runDispatchLeases,
workerCredentials,
@@ -15,12 +15,13 @@ export interface PostgresSchemaContractFunction {
export interface PostgresSchemaContract {
readonly schema: 'ql3';
readonly contractName: 'control-core';
readonly contractVersion: 53;
readonly migrationId: 'pg-0054-approval-management-boundary';
readonly contractVersion: 54;
readonly migrationId: 'pg-0055-run-attempt-log-retention';
readonly minimumServerMajor: 16;
readonly maximumServerMajor: 18;
readonly capabilities: Readonly<{
run_core: 1;
run_attempt_log_retention: 1;
run_dispatch_lease: 1;
run_retry_policy: 1;
project_policy: 1;
@@ -100,8 +101,8 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
Object.freeze({
schema: 'ql3',
contractName: 'control-core',
contractVersion: 53,
migrationId: 'pg-0054-approval-management-boundary',
contractVersion: 54,
migrationId: 'pg-0055-run-attempt-log-retention',
minimumServerMajor: 16,
maximumServerMajor: 18,
capabilities: Object.freeze({
@@ -141,6 +142,7 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
project_policy: 1,
project_tool_definition_snapshot: 1,
run_core: 1,
run_attempt_log_retention: 1,
run_dispatch_lease: 1,
run_retry_policy: 1,
security_audit: 1,
@@ -1166,6 +1168,41 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
'error_code',
'error_summary',
]),
table('run_attempt_log_retention_controls', [
'attempt_id',
'project_id',
'run_id',
'log_artifact_id',
'executor_type',
'finished_at_ms',
'eligible_at_ms',
'state',
'claim_owner',
'claim_token',
'claim_version',
'claim_expires_at_ms',
'next_claim_at_ms',
'failure_count',
'last_failure_code',
'created_at_ms',
'updated_at_ms',
]),
table('run_attempt_log_artifact_tombstones', [
'log_artifact_id',
'project_id',
'run_id',
'attempt_id',
'executor_type',
'finished_at_ms',
'eligible_at_ms',
'retired_at_ms',
'disposition',
'byte_length',
'truncated',
'maximum_bytes',
'truncation_observed_at_ms',
'record_digest',
]),
table('worker_sessions', [
'worker_id',
'session_id',
@@ -1594,6 +1631,14 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
'ql3_run_attempts_dispatch_candidates_idx',
'ql3_run_attempts_recovery_idx',
'ql3_run_attempts_lease_idx',
'run_attempt_log_retention_controls_pkey',
'ql3_run_log_retention_control_artifact_key',
'ql3_run_log_retention_retry_idx',
'ql3_run_log_retention_claim_expiry_idx',
'run_attempt_log_artifact_tombstones_pkey',
'ql3_run_log_tombstone_attempt_key',
'ql3_run_log_tombstone_retired_idx',
'ql3_run_log_retention_candidate_idx',
'worker_sessions_pkey',
'ql3_worker_sessions_available_idx',
'run_dispatch_leases_pkey',
@@ -1913,6 +1958,23 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
'ql3_run_attempts_created_at_check',
'ql3_run_attempts_started_at_check',
'ql3_run_attempts_finished_at_check',
'ql3_run_log_retention_control_identity_check',
'ql3_run_log_retention_control_time_check',
'ql3_run_log_retention_control_state_check',
'ql3_run_log_retention_control_claim_owner_check',
'ql3_run_log_retention_control_claim_token_check',
'ql3_run_log_retention_control_claim_version_check',
'ql3_run_log_retention_control_claim_expiry_check',
'ql3_run_log_retention_control_next_claim_check',
'ql3_run_log_retention_control_failure_count_check',
'ql3_run_log_retention_control_failure_code_check',
'ql3_run_log_retention_control_state_shape_check',
'ql3_run_log_tombstone_identity_check',
'ql3_run_log_tombstone_time_check',
'ql3_run_log_tombstone_disposition_check',
'ql3_run_log_tombstone_size_check',
'ql3_run_log_tombstone_truncation_check',
'ql3_run_log_tombstone_digest_check',
'ql3_worker_sessions_worker_id_check',
'ql3_worker_sessions_session_id_check',
'ql3_worker_sessions_generation_check',
@@ -2211,6 +2273,10 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
'ql3_result_retirement_catalog_fk',
'ql3_run_attempts_run_fk',
'ql3_run_attempts_step_run_fk',
'ql3_run_log_retention_control_attempt_fk',
'ql3_run_log_retention_control_run_fk',
'ql3_run_log_tombstone_attempt_fk',
'ql3_run_log_tombstone_run_fk',
'ql3_run_dispatch_leases_attempt_fk',
'ql3_run_dispatch_leases_run_fk',
'ql3_run_dispatch_leases_worker_fk',
@@ -539,6 +539,18 @@ const REQUIRED_RUNTIME_PRIVILEGES = Object.freeze({
update: true,
delete: false,
}),
run_attempt_log_retention_controls: Object.freeze({
select: true,
insert: true,
update: true,
delete: true,
}),
run_attempt_log_artifact_tombstones: Object.freeze({
select: true,
insert: true,
update: false,
delete: false,
}),
worker_sessions: Object.freeze({
select: true,
insert: true,
@@ -1034,6 +1046,18 @@ const REQUIRED_ADMIN_PRIVILEGES = Object.freeze({
update: false,
delete: false,
}),
run_attempt_log_retention_controls: Object.freeze({
select: false,
insert: false,
update: false,
delete: false,
}),
run_attempt_log_artifact_tombstones: Object.freeze({
select: false,
insert: false,
update: false,
delete: false,
}),
worker_sessions: Object.freeze({
select: false,
insert: false,