fix: recover workspace update races

This commit is contained in:
2026-09-06 09:25:33 +00:00 Unverified
parent 053f28a367
commit f14e976bcd
6 changed files with 483 additions and 35 deletions
+53 -9
View File
@@ -1,6 +1,7 @@
import { defineCommand } from "citty";
import { Listr, type ListrTaskWrapper } from "listr2";
import { randomUUID } from "node:crypto";
import { isDeepStrictEqual } from "node:util";
import type { ComposeSpecification } from "../schema/docker.d";
import type { KuberResource } from "../types";
import {
@@ -29,6 +30,7 @@ import {
type Workspace = {
metadata: { name: string; uid: string; resourceVersion: string };
spec?: { source?: unknown; config?: unknown };
};
type ResourceIdentity = {
@@ -195,6 +197,27 @@ function workspaceInput(
};
}
function workspaceMatchesInput(
workspace: Workspace,
input: ReturnType<typeof workspaceInput>,
): boolean {
return (
isDeepStrictEqual(workspace.spec?.source, input.source) &&
isDeepStrictEqual(workspace.spec?.config, input.config)
);
}
function isWorkspaceUpdateAmbiguous(error: unknown): boolean {
if (error instanceof KuberApiError) return error.status === 409;
if (error instanceof DOMException && error.name === "AbortError")
return false;
if (error instanceof Error && error.name === "AbortError") return false;
return (
error instanceof TypeError ||
(error instanceof Error && error.name === "TimeoutError")
);
}
export async function ensureWorkspace(
project: string,
compose: ComposeSpecification,
@@ -203,21 +226,42 @@ export async function ensureWorkspace(
): Promise<Workspace> {
const path = `/workspaces/${encodeURIComponent(project)}`;
const input = workspaceInput(compose, snapshot);
const update = (workspace: Workspace) =>
request<Workspace>(path, {
method: "PUT",
headers: { "if-match": `"${workspace.metadata.resourceVersion}"` },
json: input,
});
const recover = async (originalError: unknown): Promise<Workspace> => {
try {
const fresh = await request<Workspace>(path);
if (workspaceMatchesInput(fresh, input)) return fresh;
return await update(fresh);
} catch {
throw originalError;
}
};
let current: Workspace;
try {
current = await request<Workspace>(path);
} catch (error) {
if (!(error instanceof KuberApiError) || error.status !== 404) throw error;
return request<Workspace>("/workspaces", {
method: "POST",
json: { id: project, ...input },
});
try {
return await request<Workspace>("/workspaces", {
method: "POST",
json: { id: project, ...input },
});
} catch (createError) {
if (!isWorkspaceUpdateAmbiguous(createError)) throw createError;
return recover(createError);
}
}
try {
return await update(current);
} catch (updateError) {
if (!isWorkspaceUpdateAmbiguous(updateError)) throw updateError;
return recover(updateError);
}
return request<Workspace>(path, {
method: "PUT",
headers: { "if-match": `"${current.metadata.resourceVersion}"` },
json: input,
});
}
function operationEnvironment(