blob: c9e5502a5da347616a8d8f049fd892b06f3b5a62 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { FleetReport, FleetWork, FleetWorkerDescription } from './fleet.js';
import type { FleetTransport } from './fleet-store.js';
export interface FleetCellOutput {
report: Omit<FleetReport, 'artifacts'>;
artifacts: Uint8Array[];
}
/** Implemented by mock VMs for now. The callback reports cells before the group settles. */
export type FleetGroupExecution = (
work: FleetWork,
emit: (attemptId: string, output: FleetCellOutput) => void,
) => Promise<void>;
/**
* Transport-independent worker loop. Its outbox survives disconnection, not process death.
* The caller drives polling; no wall clock, timers, networking or cloud SDK is hidden here.
*/
export class FleetWorker {
#runs = new Map<string, { work: FleetWork; settled: boolean; emitted: Set<string> }>();
#outbox = new Map<string, FleetCellOutput>();
#polling: Promise<void> | undefined;
constructor(
readonly description: FleetWorkerDescription,
private readonly transport: FleetTransport,
private readonly execute: FleetGroupExecution,
) {}
poll(): Promise<void> {
this.#polling ??= this.#poll().finally(() => {
this.#polling = undefined;
});
return this.#polling;
}
get bufferedReports() {
return this.#outbox.size;
}
get activeGroups() {
return this.#runs.size;
}
async #poll() {
await this.transport.command({ kind: 'register', worker: this.description });
const heartbeat = await this.transport.command({
kind: 'heartbeat',
workerId: this.description.id,
assignmentIds: [...this.#runs.keys()],
});
for (const work of heartbeat.work ?? []) this.#adopt(work);
for (const [attemptId, output] of this.#outbox) {
const artifacts = [];
for (const bytes of output.artifacts) artifacts.push(await this.transport.putArtifact(bytes));
await this.transport.command({
kind: 'report',
workerId: this.description.id,
attemptId,
report: { ...output.report, artifacts },
});
this.#outbox.delete(attemptId);
}
for (const [assignmentId, run] of this.#runs) {
if (!run.settled || run.work.cells.some((cell) => this.#outbox.has(cell.attemptId))) continue;
await this.transport.command({ kind: 'finish', workerId: this.description.id, assignmentId });
this.#runs.delete(assignmentId);
}
while (this.#runs.size < this.description.groupSlots) {
const reply = await this.transport.command({ kind: 'claim', workerId: this.description.id });
if (!reply.assignment) break;
for (const work of reply.work ?? []) this.#adopt(work);
}
}
#adopt(work: FleetWork) {
if (this.#runs.has(work.assignment.id)) return;
const run = { work, settled: false, emitted: new Set<string>() };
this.#runs.set(work.assignment.id, run);
const emit = (attemptId: string, output: FleetCellOutput) => {
if (!work.cells.some((cell) => cell.attemptId === attemptId) || run.emitted.has(attemptId)) {
throw new Error('executor emitted an unknown or duplicate attempt');
}
run.emitted.add(attemptId);
this.#outbox.set(attemptId, structuredClone(output));
};
// A failed local operation is an execution uncertainty, not a provider classification.
void Promise.resolve()
.then(() => this.execute(work, emit))
.catch(() => undefined)
.finally(() => {
for (const cell of work.cells) {
if (run.emitted.has(cell.attemptId)) continue;
emit(cell.attemptId, {
report: {
result: {
status: 'infra_failed',
score: null,
usage: null,
costUsd: null,
durationMs: 0,
failureReason: 'worker execution ended without a cell report',
artifacts: [],
},
execution: 'unknown',
verification: 'not_run',
usage: 'missing',
cleanup: 'unknown',
environmentFailure: null,
},
artifacts: [],
});
}
run.settled = true;
});
}
}