mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): add local run log retention
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user