fix(shutdown): keep the current volume status after shutdown cleanup (#906)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Release Workflow / request-docs-version-update (push) Has been cancelled

This commit is contained in:
Nico 2026-05-20 15:04:48 +02:00 committed by GitHub
parent 0589fd63b1
commit 273408cdb8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 50 additions and 13 deletions

View file

@ -58,7 +58,7 @@ describe("shutdown", () => {
expect(updated!.status).toBe("mounted");
});
test("keeps legacy controller-local fallback unmount on shutdown", async () => {
test("keeps mounted status while running the legacy controller-local fallback unmount on shutdown", async () => {
config.flags.enableLocalAgent = false;
const events: string[] = [];
vi.spyOn(Scheduler, "stop").mockImplementation(async () => {
@ -85,6 +85,6 @@ describe("shutdown", () => {
expect(events).toEqual(["scheduler.stop", "agents.stop"]);
expect(runVolumeCommand).not.toHaveBeenCalled();
expect(updated).toBeDefined();
expect(updated!.status).toBe("unmounted");
expect(updated!.status).toBe("mounted");
});
});

View file

@ -18,7 +18,7 @@ export const shutdown = async () => {
for (const volume of volumes) {
const { status, error } = await withContext({ organizationId: volume.organizationId }, () =>
volumeService.unmountVolume(volume.shortId),
volumeService.unmountVolume(volume.shortId, { persistStatus: false }),
).catch((error) => ({ status: "error" as const, error: toMessage(error) }));
logger.info(`Volume ${volume.name} unmount status: ${status}${error ? `, error: ${error}` : ""}`);

View file

@ -152,11 +152,38 @@ describe("volumeService.mountVolume", () => {
});
});
describe("volumeService.unmountVolume", () => {
test("persists the unmounted status for normal unmount requests", async () => {
const { organizationId, user } = await createTestSession();
const volume = await createTestVolume({ organizationId, status: "mounted", agentId: "agent-1" });
agentManagerMock.runVolumeCommand.mockResolvedValueOnce({
name: "volume.unmount",
result: { status: "unmounted" },
});
await withContext({ organizationId, userId: user.id }, async () => {
const result = await volumeService.unmountVolume(volume.shortId);
expect(result.status).toBe("unmounted");
expect(agentManagerMock.runVolumeCommand).toHaveBeenCalledWith(
volume.agentId,
expect.objectContaining({ name: "volume.unmount", volume: expect.objectContaining({ id: volume.id }) }),
);
});
const updatedVolume = await db.query.volumesTable.findFirst({ where: { id: volume.id } });
expect(updatedVolume?.status).toBe("unmounted");
});
});
describe("volumeService.ensureHealthyVolume", () => {
test("returns ready when the mounted volume passes its health check", async () => {
const { organizationId, user } = await createTestSession();
const volume = await createTestVolume({ organizationId, status: "mounted", agentId: "agent-1" });
agentManagerMock.runVolumeCommand.mockResolvedValue({ name: "volume.checkHealth", result: { status: "mounted" } });
agentManagerMock.runVolumeCommand.mockResolvedValue({
name: "volume.checkHealth",
result: { status: "mounted" },
});
await withContext({ organizationId, userId: user.id }, async () => {
const result = await volumeService.ensureHealthyVolume(volume.shortId);
@ -169,14 +196,22 @@ describe("volumeService.ensureHealthyVolume", () => {
expect(agentManagerMock.runVolumeCommand).toHaveBeenCalledOnce();
expect(agentManagerMock.runVolumeCommand).toHaveBeenCalledWith(
volume.agentId,
expect.objectContaining({ name: "volume.checkHealth", volume: expect.objectContaining({ id: volume.id }) }),
expect.objectContaining({
name: "volume.checkHealth",
volume: expect.objectContaining({ id: volume.id }),
}),
);
});
});
test("auto-remounts when the mounted volume fails its health check", async () => {
const { organizationId, user } = await createTestSession();
const volume = await createTestVolume({ organizationId, status: "mounted", autoRemount: true, agentId: "agent-1" });
const volume = await createTestVolume({
organizationId,
status: "mounted",
autoRemount: true,
agentId: "agent-1",
});
agentManagerMock.runVolumeCommand
.mockResolvedValueOnce({ name: "volume.checkHealth", result: { status: "error", error: "stale mount" } })
.mockResolvedValueOnce({ name: "volume.unmount", result: { status: "unmounted" } })

View file

@ -174,7 +174,7 @@ const mountVolume = async (shortId: ShortId) => {
return { error, status };
};
const unmountVolume = async (shortId: ShortId) => {
const unmountVolume = async (shortId: ShortId, options?: { persistStatus?: boolean }) => {
const organizationId = getOrganizationId();
const volume = await findVolume(shortId);
@ -184,13 +184,15 @@ const unmountVolume = async (shortId: ShortId) => {
const { status, error } = await runVolumeBackendCommand(volume, "volume.unmount");
await db
.update(volumesTable)
.set({ status })
.where(and(eq(volumesTable.id, volume.id), eq(volumesTable.organizationId, organizationId)));
if (options?.persistStatus !== false) {
await db
.update(volumesTable)
.set({ status })
.where(and(eq(volumesTable.id, volume.id), eq(volumesTable.organizationId, organizationId)));
if (status === "unmounted") {
serverEvents.emit("volume:unmounted", { organizationId, volumeName: volume.name });
if (status === "unmounted") {
serverEvents.emit("volume:unmounted", { organizationId, volumeName: volume.name });
}
}
return { error, status };