fix(sandbox): serialize registry mutations and lock usage

This commit is contained in:
Peter Steinberger
2026-02-18 04:55:33 +01:00
parent 28bac46c92
commit 35016a380c
3 changed files with 18 additions and 10 deletions

View File

@@ -1,12 +1,18 @@
import { mkdtempSync } from "node:fs";
import fs from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const TEST_STATE_DIR = mkdtempSync(path.join(tmpdir(), "openclaw-sandbox-registry-"));
const SANDBOX_REGISTRY_PATH = path.join(TEST_STATE_DIR, "containers.json");
const SANDBOX_BROWSER_REGISTRY_PATH = path.join(TEST_STATE_DIR, "browsers.json");
const { TEST_STATE_DIR, SANDBOX_REGISTRY_PATH, SANDBOX_BROWSER_REGISTRY_PATH } = vi.hoisted(() => {
const path = require("node:path");
const { mkdtempSync } = require("node:fs");
const { tmpdir } = require("node:os");
const baseDir = mkdtempSync(path.join(tmpdir(), "openclaw-sandbox-registry-"));
return {
TEST_STATE_DIR: baseDir,
SANDBOX_REGISTRY_PATH: path.join(baseDir, "containers.json"),
SANDBOX_BROWSER_REGISTRY_PATH: path.join(baseDir, "browsers.json"),
};
});
vi.mock("./constants.js", () => ({
SANDBOX_STATE_DIR: TEST_STATE_DIR,
@@ -183,8 +189,8 @@ describe("registry race safety", () => {
};
await Promise.all([
removeRegistryEntry("container-x"),
updateRegistry(containerEntry({ containerName: "container-x", configHash: "updated" })),
removeRegistryEntry("container-x"),
]);
const registry = await readRegistry();
@@ -224,8 +230,8 @@ describe("registry race safety", () => {
};
await Promise.all([
removeBrowserRegistryEntry("browser-x"),
updateBrowserRegistry(browserEntry({ containerName: "browser-x", configHash: "updated" })),
removeBrowserRegistryEntry("browser-x"),
]);
const registry = await readBrowserRegistry();

View File

@@ -70,7 +70,7 @@ function isRegistryFile<T extends RegistryEntry>(value: unknown): value is Regis
}
async function withRegistryLock<T>(registryPath: string, fn: () => Promise<T>): Promise<T> {
const lock = await acquireSessionWriteLock({ sessionFile: registryPath });
const lock = await acquireSessionWriteLock({ sessionFile: registryPath, allowReentrant: false });
try {
return await fn();
} finally {

View File

@@ -375,6 +375,7 @@ export async function acquireSessionWriteLock(params: {
timeoutMs?: number;
staleMs?: number;
maxHoldMs?: number;
allowReentrant?: boolean;
}): Promise<{
release: () => Promise<void>;
}> {
@@ -394,8 +395,9 @@ export async function acquireSessionWriteLock(params: {
const normalizedSessionFile = path.join(normalizedDir, path.basename(sessionFile));
const lockPath = `${normalizedSessionFile}.lock`;
const allowReentrant = params.allowReentrant ?? true;
const held = HELD_LOCKS.get(normalizedSessionFile);
if (held) {
if (allowReentrant && held) {
held.count += 1;
return {
release: async () => {