feat(ql3): add local run log retention

This commit is contained in:
whyour
2026-08-12 02:57:43 +08:00
parent 308aa75d89
commit 2bfa8ca279
50 changed files with 2752 additions and 85 deletions
@@ -6,6 +6,7 @@ import {
type RunAttemptLogReadResult,
} from '@qinglong/runtime-core/run-attempt-log-read';
import type { RunRepositoryReader } from '@qinglong/runtime-core/run-repository';
import type { RunAttemptLogRetentionStateReader } from '@qinglong/runtime-core/run-attempt-log-retention';
import type { ClusterControlAdmissionResponse } from '../transport/httpSurface';
import type {
@@ -99,12 +100,14 @@ function projection(
export function createClusterControlRunAttemptLogReadRoute(
runs: Pick<RunRepositoryReader, 'findRunById' | 'findAttemptById'>,
reader?: RunAttemptLogRangeReader,
retention?: RunAttemptLogRetentionStateReader,
): Readonly<ClusterControlRouteDefinition> {
if (
!runs ||
typeof runs.findRunById !== 'function' ||
typeof runs.findAttemptById !== 'function' ||
(reader !== undefined && typeof reader.read !== 'function')
(reader !== undefined && typeof reader.read !== 'function') ||
(retention !== undefined && typeof retention.inspect !== 'function')
) {
throw new TypeError(
'Cluster-control Run Attempt log read dependencies are invalid',
@@ -113,12 +116,17 @@ export function createClusterControlRunAttemptLogReadRoute(
const service =
reader === undefined
? undefined
: new RunAttemptLogReadService(runs, reader, {
executorType: 'remote_worker',
artifactIdPattern: /^wlog-[a-f0-9]{30}$/,
maximumReadBytes: MAXIMUM_READ_BYTES,
activeMissingIsPending: true,
});
: new RunAttemptLogReadService(
runs,
reader,
{
executorType: 'remote_worker',
artifactIdPattern: /^wlog-[a-f0-9]{30}$/,
maximumReadBytes: MAXIMUM_READ_BYTES,
activeMissingIsPending: true,
},
retention,
);
return Object.freeze({
...CLUSTER_CONTROL_RUN_ATTEMPT_LOG_READ_ROUTE,
validateQuery,
@@ -161,6 +169,18 @@ export function createClusterControlRunAttemptLogReadRoute(
if (result.status === 'missing') {
return response(503, { code: 'artifact_unavailable' });
}
if (result.status === 'retired') {
return response(410, {
schema: 'qinglong/run-attempt-log-read-result@v1',
status: 'retired',
projectId: result.projectId,
runId: result.runId,
attemptId: result.attemptId,
retiredAtMs: result.retiredAtMs,
byteLength: result.byteLength,
truncation: result.truncation,
});
}
return response(200, projection(result));
} catch (error) {
if (error instanceof InvalidRunAttemptLogReadError) {
@@ -249,3 +249,73 @@ test('returns pending during upload and fails closed without an object reader',
body: { code: 'artifact_unavailable' },
});
});
test('maps an injected durable retention state to 410 without object access', async () => {
const {
createRunAttemptLogRetirementRecord,
} = require('@qinglong/runtime-core/run-attempt-log-retention');
let reads = 0;
const route = createClusterControlRunAttemptLogReadRoute(
{
async findRunById() {
return run({ status: 'succeeded', finishedAtMs: 10 });
},
async findAttemptById() {
return attempt({ status: 'succeeded', finishedAtMs: 10 });
},
},
{
async read() {
reads += 1;
return { status: 'missing' };
},
},
{
async inspect() {
return {
status: 'retired',
record: createRunAttemptLogRetirementRecord({
projectId: 'prj_default',
runId: 'run_123',
attemptId: 'attempt_123',
logArtifactId: `wlog-${'a'.repeat(30)}`,
executorType: 'remote_worker',
finishedAtMs: 10,
eligibleAtMs: 20,
retiredAtMs: 30,
disposition: 'deleted',
byteLength: 42,
truncation: { truncated: 'unknown' },
}),
};
},
},
);
const prepared = await createClusterControlAdmissionPipeline({
routes: createClusterControlRouteRegistry([route]),
authenticator: { authenticate: () => PRINCIPAL },
policy: {
authorize: () => ({
effect: 'allow',
reasons: ['role_grant'],
fence: { projectVersion: 1, bindingVersion: 1 },
}),
},
audit: { record() {} },
now: () => 10_000,
}).prepare(metadata());
assert.deepEqual(await prepared.handle(null), {
statusCode: 410,
body: {
schema: 'qinglong/run-attempt-log-read-result@v1',
status: 'retired',
projectId: 'prj_default',
runId: 'run_123',
attemptId: 'attempt_123',
retiredAtMs: 30,
byteLength: 42,
truncation: { truncated: 'unknown' },
},
});
assert.equal(reads, 0);
});