Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/js-no-execution-cap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/computer": minor
---

`WorkerJavaScriptBackend` no longer caps concurrent executions, and the `maxConcurrentExecutions` option is removed. Its default of 24 sat above the platform's own limit of 10 concurrent Dynamic Workers per request, so runs 11 through 24 were admitted and then failed with a platform error anyway. Executions now start until the platform says no, and that error is the execution's error. Remove `maxConcurrentExecutions` from backend options.
2 changes: 1 addition & 1 deletion docs/17_isolate_javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Workspace parses the graph before loading the Worker, confines every durable pat

## Execution limits and retention

The backend admits up to twenty-four executions at a time by default. A concurrent start past that ceiling fails with `EEXEC_BUSY` instead of creating an unbounded number of Dynamic Workers. Adjust `maxConcurrentExecutions` after measuring the Durable Object and Worker Loader limits for the deployment.
The backend does not cap concurrent executions itself. The platform limits how many Dynamic Workers run at once, and an execution started past that limit fails with the platform's error.

Each execution also bounds combined stdout and stderr output, active event subscribers, directory entries per read, concurrent and total capability calls, and cumulative capability request and response bytes. The corresponding `maxStdioBytes`, `maxExecutionSubscribers`, `maxDirectoryEntries`, and `max*Capability*` options may be lowered for public workloads. Directory reads apply their limit in SQLite before materializing rows. Requests are checked inside the isolate before Workers RPC and again by the host. Every capability call goes through one host bridge that enforces these limits. Values cross as real Workers RPC values, measured as UTF-8 bytes for strings and raw bytes for byte arrays, and anything that is not plain data, such as a function, an RPC stub, or a cycle, is rejected before the host acts on it.

Expand Down
1 change: 0 additions & 1 deletion examples/rlm/worker/executor-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export class ExecutorAgent extends AIChatAgent<ModelEnv, BenchmarkAgentState> {
root: WORKSPACE_ROOT,
access: "read",
egress: { mode: "none" },
maxConcurrentExecutions: 1,
maxConcurrentCapabilityCalls: 4,
maxCapabilityCalls: 24,
maxCapabilityBytes: 4 * 1024 * 1024,
Expand Down
1 change: 0 additions & 1 deletion examples/rlm/worker/rlm-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ export class RlmAgent extends AIChatAgent<ModelEnv, BenchmarkAgentState> {
access: "read",
egress: { mode: "none" },
modules: { "ws:model": modelCapability },
maxConcurrentExecutions: 1,
maxConcurrentCapabilityCalls: 4,
// One manifest read + 24 chunk reads + one bounded ws:model batch.
maxCapabilityCalls: 26,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -486,49 +486,6 @@ describe("WorkerJavaScriptBackend", () => {
await workspace.close();
});

it("limits concurrent Dynamic Workers", async () => {
let resolveEvaluation!: (value: { result: number }) => void;
const evaluation = new Promise<{ result: number }>((resolve) => {
resolveEvaluation = resolve;
});
const workspace = new Workspace({
storage: new SQLiteTestStorage(),
backends: [
new WorkerJavaScriptBackend({
maxConcurrentExecutions: 1,
loader: {
load() {
return {
getEntrypoint() {
return {
evaluate: (
_input: unknown,
host: {
assertResult(value: unknown): Promise<void>;
attachOutput(readable: ReadableStream<Uint8Array>): Promise<void>;
},
) => evaluation.then((outcome) => evaluateResult(host, outcome.result)),
};
},
};
},
},
}),
],
});
await workspace.fs.mkdir("/workspace", { recursive: true });
const first = await workspace.runtime.exec("export default 1", { id: "first" });
await expect(
workspace.runtime.exec("export default 2", { id: "second" }),
).rejects.toMatchObject({ code: "EEXEC_BUSY" });
resolveEvaluation({ result: 1 });
await expect(first.result()).resolves.toMatchObject({ status: "completed" });
await expect(
workspace.runtime.exec("export default 2", { id: "second" }),
).resolves.toBeDefined();
await workspace.close();
});

it("waits for accepted host calls before reporting successful completion", async () => {
const db = new Database(new SQLiteTestStorage());
initializeSchema(db, () => 0);
Expand Down Expand Up @@ -1170,6 +1127,52 @@ describe("WorkerJavaScriptBackend", () => {
}
});

it("runs concurrent executions without a cap of its own", async () => {
let release!: () => void;
const released = new Promise<void>((resolve) => {
release = resolve;
});
let started = 0;
const workspace = new Workspace({
storage: new SQLiteTestStorage(),
backends: [
new WorkerJavaScriptBackend({
loader: {
load() {
return {
getEntrypoint() {
return {
evaluate: (
_input: unknown,
host: {
assertResult(value: unknown): Promise<void>;
attachOutput(readable: ReadableStream<Uint8Array>): Promise<void>;
},
) => {
started += 1;
return released.then(() => evaluateResult(host, 1));
},
};
},
};
},
},
}),
],
});
await workspace.fs.mkdir("/workspace", { recursive: true });
const handles = await Promise.all(
Array.from({ length: 30 }, (_, index) =>
workspace.runtime.exec("export default 1", { id: `run-${index}` }),
),
);
await vi.waitFor(() => expect(started).toBe(30));
release();
const results = await Promise.all(handles.map((handle) => handle.result()));
expect(results.every((result) => result.status === "completed")).toBe(true);
await workspace.close();
});

it("rejects relative imports that collide with internal Loader modules", async () => {
const load = vi.fn();
const workspace = new Workspace({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ export interface WorkerJavaScriptBackendOptions {
maxCapabilityResponseBytes?: number;
/** Maximum entries returned by one isolated directory read. Defaults to 1024. */
maxDirectoryEntries?: number;
/** Maximum graph loads and Dynamic Workers active at once. Defaults to 1. */
maxConcurrentExecutions?: number;
/** Maximum live replay subscribers per execution. Defaults to 8. */
maxExecutionSubscribers?: number;
/** Completed execution retention window. Defaults to five minutes. */
Expand Down Expand Up @@ -99,7 +97,6 @@ type ResolvedWorkerJavaScriptBackendOptions = Required<
| "maxCapabilityRequestBytes"
| "maxCapabilityResponseBytes"
| "maxDirectoryEntries"
| "maxConcurrentExecutions"
| "maxExecutionSubscribers"
| "retentionMs"
| "maxRetainedExecutions"
Expand Down Expand Up @@ -146,7 +143,6 @@ interface ExecutionRecord {
control?: ActiveControl;
bridge?: WorkspaceRuntimeBridge;
finalization?: Promise<void>;
admitted?: boolean;
persistenceFailed?: boolean;
result?: WorkspaceRuntimeValue;
hasResult?: boolean;
Expand Down Expand Up @@ -193,7 +189,6 @@ export class WorkerJavaScriptBackend implements WorkspaceModuleBackend {
"maxCapabilityResponseBytes",
);
assertPositiveInteger(options.maxDirectoryEntries ?? 1024, "maxDirectoryEntries");
assertPositiveInteger(options.maxConcurrentExecutions ?? 24, "maxConcurrentExecutions");
assertPositiveInteger(options.maxExecutionSubscribers ?? 8, "maxExecutionSubscribers");
assertPositiveFinite(options.retentionMs ?? 60 * 60_000, "retentionMs");
assertPositiveInteger(options.maxRetainedExecutions ?? 100, "maxRetainedExecutions");
Expand Down Expand Up @@ -236,7 +231,6 @@ export class WorkerJavaScriptBackend implements WorkspaceModuleBackend {
maxCapabilityRequestBytes: options.maxCapabilityRequestBytes ?? 8 * 1024 * 1024,
maxCapabilityResponseBytes: options.maxCapabilityResponseBytes ?? 8 * 1024 * 1024,
maxDirectoryEntries: options.maxDirectoryEntries ?? 1024,
maxConcurrentExecutions: options.maxConcurrentExecutions ?? 24,
maxExecutionSubscribers: options.maxExecutionSubscribers ?? 8,
retentionMs: options.retentionMs ?? 60 * 60_000,
maxRetainedExecutions: options.maxRetainedExecutions ?? 100,
Expand Down Expand Up @@ -265,7 +259,6 @@ class JavaScriptBackendHandle implements WorkspaceModuleBackendHandle {
readonly #records = new Map<string, ExecutionRecord>();
readonly #pendingIds = new Set<string>();
#closed = false;
#activeExecutions = 0;
readonly #activeStreams = new Map<string, number>();
#pendingStarts = 0;
readonly #pendingStartWaiters = new Set<() => void>();
Expand Down Expand Up @@ -367,17 +360,11 @@ class JavaScriptBackendHandle implements WorkspaceModuleBackendHandle {
if (new TextEncoder().encode(input.source).byteLength > this.#options.maxSourceBytes) {
throw new Error(`Workspace runtime source exceeds ${this.#options.maxSourceBytes} bytes.`);
}
if (this.#activeExecutions >= this.#options.maxConcurrentExecutions) {
throw runtimeError(
"EEXEC_BUSY",
`JavaScript backend already has ${this.#activeExecutions} active execution(s)`,
);
}

this.#activeExecutions += 1;
// There is no cap on concurrent executions here: the platform limits
// concurrent Dynamic Workers itself and reports that limit as the
// execution's error.
this.#pendingStarts += 1;
this.#pendingIds.add(id);
let admittedRecord: ExecutionRecord | undefined;
try {
const capability = new WorkspaceRuntimeCapability(
this.#host.fs,
Expand All @@ -403,7 +390,6 @@ class JavaScriptBackendHandle implements WorkspaceModuleBackendHandle {
events: [],
subscribers: new Set(),
status: "running",
admitted: true,
};
try {
this.#host.db.run(
Expand All @@ -422,7 +408,6 @@ class JavaScriptBackendHandle implements WorkspaceModuleBackendHandle {
if (durable) throw runtimeError("EEXEC_EXISTS", `execution ${id} already exists`);
throw error;
}
admittedRecord = record;
this.#records.set(id, record);
try {
const bridge = new WorkspaceRuntimeBridge(capability, {
Expand Down Expand Up @@ -476,7 +461,6 @@ class JavaScriptBackendHandle implements WorkspaceModuleBackendHandle {
for (const resolve of this.#pendingStartWaiters) resolve();
this.#pendingStartWaiters.clear();
}
if (!admittedRecord) this.#activeExecutions -= 1;
}
}

Expand Down Expand Up @@ -873,10 +857,6 @@ class JavaScriptBackendHandle implements WorkspaceModuleBackendHandle {
for (const subscriber of [...record.subscribers]) this.#pump(record, subscriber);
record.control = undefined;
record.bridge = undefined;
if (record.admitted) {
record.admitted = false;
this.#activeExecutions = Math.max(0, this.#activeExecutions - 1);
}
if (record.status !== "running" && !record.persistenceFailed) {
this.#records.delete(record.id);
}
Expand Down
Loading