feat(orchestrator): harden zero-impact shadow rollout
Gate and bound Portal shadow observations while preserving Native execution. Add fail-closed service boundaries, terminal retention controls, Canary readiness telemetry, ops visibility, and isolated regression coverage.
This commit is contained in:
@@ -79,6 +79,11 @@ function gatewayError(code, message, status = 409) {
|
||||
return error;
|
||||
}
|
||||
|
||||
function normalizeWorkflowRunId(value) {
|
||||
const runId = String(value ?? '').trim();
|
||||
return /^[a-zA-Z0-9_-]{1,128}$/.test(runId) ? runId : null;
|
||||
}
|
||||
|
||||
export function buildExecutorJobEvent({
|
||||
eventId = crypto.randomUUID(),
|
||||
jobId,
|
||||
@@ -366,6 +371,36 @@ export function createInMemoryExecutorJobStore() {
|
||||
const value = jobId ? byId.get(jobId) : null;
|
||||
return value ? clone(value) : null;
|
||||
},
|
||||
async deleteById(jobId) {
|
||||
const normalizedJobId = String(jobId ?? '').trim();
|
||||
const existing = byId.get(normalizedJobId);
|
||||
if (!existing) return false;
|
||||
byId.delete(normalizedJobId);
|
||||
byIdempotencyKey.delete(existing.idempotencyKey);
|
||||
eventsByJobId.delete(normalizedJobId);
|
||||
return true;
|
||||
},
|
||||
async listTerminalBefore({ before, limit = 100 } = {}) {
|
||||
const cutoff = Number(before);
|
||||
if (!Number.isFinite(cutoff)) return [];
|
||||
return [...byId.values()]
|
||||
.filter((record) => (
|
||||
TERMINAL_JOB_STATUSES.has(record.status)
|
||||
&& record.workflowRunId
|
||||
&& Number(record.completedAt ?? 0) > 0
|
||||
&& Number(record.completedAt) <= cutoff
|
||||
))
|
||||
.sort((left, right) => (
|
||||
Number(left.completedAt) - Number(right.completedAt)
|
||||
|| String(left.jobId).localeCompare(String(right.jobId))
|
||||
))
|
||||
.slice(0, clampInteger(limit, 100, 1, 500))
|
||||
.map((record) => ({
|
||||
jobId: record.jobId,
|
||||
workflowRunId: record.workflowRunId,
|
||||
completedAt: Number(record.completedAt),
|
||||
}));
|
||||
},
|
||||
async createIfAbsent(record, { initialEvent = null } = {}) {
|
||||
const existingId = byIdempotencyKey.get(record.idempotencyKey);
|
||||
if (existingId) return { created: false, record: clone(byId.get(existingId)) };
|
||||
@@ -553,6 +588,8 @@ function validateJobStore(store) {
|
||||
'getById',
|
||||
'getByIdempotencyKey',
|
||||
'createIfAbsent',
|
||||
'deleteById',
|
||||
'listTerminalBefore',
|
||||
'update',
|
||||
'listEvents',
|
||||
]) {
|
||||
@@ -674,6 +711,7 @@ export function createExecutorGateway({
|
||||
fallbackRegistered: decision.fallbackRegistered,
|
||||
fallbackAvailable: decision.fallbackAvailable,
|
||||
decision,
|
||||
workflowRunId: normalizeWorkflowRunId(request.metadata?.workflowRunId),
|
||||
request: dispatchAllowed ? request : null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
@@ -741,10 +779,28 @@ export function createExecutorGateway({
|
||||
return store.update(updated, { event: eventForCancellation(updated) });
|
||||
}
|
||||
|
||||
async function deleteJob(jobId) {
|
||||
const current = await store.getById(jobId);
|
||||
if (!current) return false;
|
||||
if (!TERMINAL_JOB_STATUSES.has(current.status)) {
|
||||
throw gatewayError(
|
||||
'EXECUTOR_JOB_NOT_TERMINAL',
|
||||
`Executor job ${current.jobId} must be terminal before deletion`,
|
||||
);
|
||||
}
|
||||
return store.deleteById(current.jobId);
|
||||
}
|
||||
|
||||
async function listRetentionCandidates(options = {}) {
|
||||
return store.listTerminalBefore(options);
|
||||
}
|
||||
|
||||
return {
|
||||
preview,
|
||||
createJob,
|
||||
cancel,
|
||||
deleteJob,
|
||||
listRetentionCandidates,
|
||||
async getJob(jobId, { includeRequest = false } = {}) {
|
||||
const record = await store.getById(jobId);
|
||||
return includeRequest ? record : projectExecutorJobForRead(record);
|
||||
@@ -793,6 +849,7 @@ export const executorGatewayInternals = {
|
||||
PHASE3_EXECUTOR_DESCRIPTORS,
|
||||
TERMINAL_JOB_STATUSES,
|
||||
fingerprint,
|
||||
normalizeWorkflowRunId,
|
||||
projectExecutorJobForRead,
|
||||
stableValue,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user