fix(worker): make workspace cleanup layout-aware
This commit is contained in:
@@ -41,7 +41,11 @@ import {
|
||||
stopWorkspaceContainer,
|
||||
streamExecInContainer,
|
||||
} from './runtime/docker';
|
||||
import { acquireUserBox, releaseUserBox } from './user-container';
|
||||
import {
|
||||
acquireUserBox,
|
||||
releaseUserBox,
|
||||
runningBoxUsernames,
|
||||
} from './user-container';
|
||||
import { fetchUserEnvironment, materializeUserHome } from './user-environment';
|
||||
|
||||
type Claim = {
|
||||
@@ -1287,6 +1291,29 @@ const slugify = (value: string) =>
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.replace(/-{2,}/g, '-') || 'x';
|
||||
|
||||
export const planWorkdirCleanup = (args: {
|
||||
root: string;
|
||||
rootEntries: { name: string; isDirectory: boolean }[];
|
||||
homesEntries: Record<string, { name: string; isDirectory: boolean }[]>;
|
||||
codeLeaves: Record<string, string[]>;
|
||||
activeWorkdirs: Set<string>;
|
||||
runningBoxUsernames: Set<string>;
|
||||
}): { removeDirs: string[] } => {
|
||||
const removeDirs: string[] = [];
|
||||
for (const entry of args.rootEntries) {
|
||||
if (!entry.isDirectory || entry.name.startsWith('.')) continue;
|
||||
if (entry.name === 'homes') continue;
|
||||
const abs = path.resolve(args.root, entry.name);
|
||||
if (args.activeWorkdirs.has(abs)) continue;
|
||||
removeDirs.push(abs);
|
||||
}
|
||||
for (const [username, leaves] of Object.entries(args.codeLeaves)) {
|
||||
if (args.runningBoxUsernames.has(username)) continue;
|
||||
for (const leaf of leaves) removeDirs.push(leaf);
|
||||
}
|
||||
return { removeDirs };
|
||||
};
|
||||
|
||||
const runClaim = async (claim: Claim) => {
|
||||
const jobId = claim.job._id;
|
||||
const secretValues = [
|
||||
@@ -1885,7 +1912,8 @@ export const getWorkerHealth = async () => {
|
||||
workdir: workspace.workdir,
|
||||
agentTurnActive: Boolean(workspace.agentTurnActive),
|
||||
}));
|
||||
const containerNames = await listWorkspaceContainerNames('spoon-agent-job-');
|
||||
const jobContainers = await listWorkspaceContainerNames('spoon-agent-job-');
|
||||
const boxContainers = await listWorkspaceContainerNames('spoon-box-');
|
||||
return {
|
||||
ok: true,
|
||||
buildSha: env.buildSha,
|
||||
@@ -1904,7 +1932,8 @@ export const getWorkerHealth = async () => {
|
||||
jobTimeoutMs: env.jobTimeoutMs,
|
||||
activeWorkspaceCount: active.length,
|
||||
activeWorkspaces: active,
|
||||
workspaceContainers: containerNames,
|
||||
workspaceContainers: jobContainers,
|
||||
boxContainers,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1928,14 +1957,55 @@ export const cleanupOrphanedWorkspaces = async () => {
|
||||
removedContainers.push(containerName);
|
||||
}
|
||||
|
||||
const removedWorkdirs: string[] = [];
|
||||
const root = path.resolve(env.workdir);
|
||||
const removedWorkdirs: string[] = [];
|
||||
try {
|
||||
const entries = await readdir(root, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
||||
const target = path.resolve(root, entry.name);
|
||||
if (activeWorkdirs.has(target)) continue;
|
||||
const rootDirents = await readdir(root, { withFileTypes: true });
|
||||
const rootEntries = rootDirents.map((d) => ({
|
||||
name: d.name,
|
||||
isDirectory: d.isDirectory(),
|
||||
}));
|
||||
const homesRoot = path.join(root, 'homes');
|
||||
const homesEntries: Record<
|
||||
string,
|
||||
{ name: string; isDirectory: boolean }[]
|
||||
> = {};
|
||||
const codeLeaves: Record<string, string[]> = {};
|
||||
const homesDirents = await readdir(homesRoot, {
|
||||
withFileTypes: true,
|
||||
}).catch(() => []);
|
||||
for (const userDir of homesDirents) {
|
||||
if (!userDir.isDirectory()) continue;
|
||||
const username = userDir.name;
|
||||
const codeRoot = path.join(homesRoot, username, 'Code');
|
||||
homesEntries[username] = [];
|
||||
const leaves: string[] = [];
|
||||
const spoons = await readdir(codeRoot, { withFileTypes: true }).catch(
|
||||
() => [],
|
||||
);
|
||||
for (const spoonDir of spoons) {
|
||||
if (!spoonDir.isDirectory()) continue;
|
||||
const spoonRoot = path.join(codeRoot, spoonDir.name);
|
||||
const branches = await readdir(spoonRoot, {
|
||||
withFileTypes: true,
|
||||
}).catch(() => []);
|
||||
for (const branchDir of branches) {
|
||||
if (branchDir.isDirectory()) {
|
||||
leaves.push(path.join(spoonRoot, branchDir.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
codeLeaves[username] = leaves;
|
||||
}
|
||||
const { removeDirs } = planWorkdirCleanup({
|
||||
root,
|
||||
rootEntries,
|
||||
homesEntries,
|
||||
codeLeaves,
|
||||
activeWorkdirs,
|
||||
runningBoxUsernames: runningBoxUsernames(),
|
||||
});
|
||||
for (const target of removeDirs) {
|
||||
await rm(target, { recursive: true, force: true });
|
||||
removedWorkdirs.push(target);
|
||||
}
|
||||
@@ -1946,11 +2016,8 @@ export const cleanupOrphanedWorkspaces = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
removedContainers,
|
||||
removedWorkdirs,
|
||||
};
|
||||
const boxContainers = await listWorkspaceContainerNames('spoon-box-');
|
||||
return { success: true, removedContainers, removedWorkdirs, boxContainers };
|
||||
};
|
||||
|
||||
export const startWorker = async () => {
|
||||
|
||||
Reference in New Issue
Block a user