Skip to content

fix(emulator): Storage emulator hangs under concurrent requests - #10852

Open
joehan wants to merge 3 commits into
mainfrom
fix-storage-emulator-hang
Open

fix(emulator): Storage emulator hangs under concurrent requests#10852
joehan wants to merge 3 commits into
mainfrom
fix-storage-emulator-hang

Conversation

@joehan

@joehan joehan commented Jul 27, 2026

Copy link
Copy Markdown
Member

Description

Fixes the long-standing Cloud Storage emulator hang under concurrent requests (Fixes #6194, Fixes #6865; earlier duplicates: #3915, #4768, #4918).

  • Frame stdout on newlines: The data handler now buffers the stream in handleRuntimeStdout and only parses complete newline-delimited lines, retaining any incomplete trailing line across chunks — handling both a batched chunk (many responses at once) and a single response split across two chunks.
  • Remove the ~15ms per-request write delay: It was added for Receiving: {"result":{"permit":false},"id":885,"status":"ok"} and non-resolving Promise.all while fetching from emulator storage #3915 as a workaround for exactly this bug. With correct framing it's unnecessary and only slows every request.
  • Free each pending-request entry on completion: Entries were previously deleted only on the firestore cross-service (overrideId) path, leaking one entry per request otherwise.

Scenarios Tested

  • Unit tests in src/emulator/storage/rules/runtime.spec.ts.
  • Storage Emulator tests (local and unit tests).

glorat and others added 2 commits July 12, 2026 02:39
The Storage emulator delegates rule evaluation to a Java subprocess that
writes one JSON response per line to stdout. The stdout handler parsed each
raw "data" chunk with a single JSON.parse. Node stream chunks do not respect
message boundaries, so under concurrent load multiple responses arrive in one
chunk (and a single response can be split across chunks); JSON.parse then
throws on the concatenation, the error is swallowed, and every batched
response is dropped — hanging the corresponding requests forever.

Buffer the stdout stream and dispatch only complete newline-delimited lines
(handleRuntimeStdout), retaining any incomplete trailing line across chunks.
Also:
- remove the ~15ms per-request write delay (added for #3915) that was a
  workaround for this bug and actually worsened batching;
- free each pending-request entry on completion (previously leaked outside
  the firestore cross-service path).

Adds unit tests driving handleRuntimeStdout with batched and
split-across-chunk stdout; they fail against the old per-chunk parse.

Fixes #6194
Fixes #6865
### Description
Fixes the long-standing Cloud Storage emulator hang under concurrent requests (Fixes #6194, Fixes #6865; earlier duplicates: #3915, #4768, #4918).

- Frame stdout on newlines: Buffers stream stdout in handleRuntimeStdout and only parses complete newline-delimited lines, retaining incomplete trailing lines across chunks.
- Remove the ~15ms per-request write delay.
- Free each pending-request entry on completion.

### Scenarios Tested
- Unit tests in src/emulator/storage/rules/runtime.spec.ts.
- Storage Emulator tests (local and unit tests).

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request fixes a Cloud Storage emulator hang under concurrent requests by buffering and parsing the rules runtime's stdout per-line instead of per-chunk, resolving issues #6194 and #6865. It also removes an unnecessary 15ms delay and prevents a memory leak by deleting pending-request entries upon completion. The review feedback suggests resetting the _stdoutBuffer on start/stop to avoid cross-run pollution, and replacing console.log and console.warn calls with the central EmulatorLogger to comply with the repository style guide.

private _alive = false;
// Holds the incomplete trailing line of the runtime's stdout between "data"
// events. See handleRuntimeStdout().
private _stdoutBuffer = "";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent potential cross-run pollution or unexpected hangs, consider resetting _stdoutBuffer to an empty string when starting or stopping the rules runtime (e.g., in start() or stop()). If the emulator is stopped and restarted, any leftover partial data in _stdoutBuffer from the previous run could be prepended to the first response of the new run, causing JSON parsing failures.

Comment on lines +233 to 236
if (id === undefined) {
console.log(`Received no ID from server response ${line}`);
continue;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using console.log for user-facing output. Use the central EmulatorLogger instead to adhere to the repository style guide.

Suggested change
if (id === undefined) {
console.log(`Received no ID from server response ${line}`);
continue;
}
if (id === undefined) {
EmulatorLogger.forEmulator(Emulators.STORAGE).log("WARN", "Received no ID from server response " + line);
continue;
}
References
  1. Use the central logger; never use console.log() for user-facing output. (link)

Comment on lines +240 to +244
if (rap.status !== "ok" && !("action" in rap)) {
console.warn(`[RULES] ${rap.status}: ${rap.message}`);
rap.errors.forEach(console.warn.bind(console));
continue;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using console.warn for user-facing output. Use the central EmulatorLogger instead to adhere to the repository style guide.

Suggested change
if (rap.status !== "ok" && !("action" in rap)) {
console.warn(`[RULES] ${rap.status}: ${rap.message}`);
rap.errors.forEach(console.warn.bind(console));
continue;
}
if (rap.status !== "ok" && !("action" in rap)) {
EmulatorLogger.forEmulator(Emulators.STORAGE).log("WARN", "[RULES] " + rap.status + ": " + rap.message);
rap.errors.forEach((err) => EmulatorLogger.forEmulator(Emulators.STORAGE).log("WARN", err));
continue;
}
References
  1. Use the central logger; never use console.log() for user-facing output. (link)

Comment on lines +246 to +250
if (request) {
request.handler(rap);
} else {
console.log(`No handler for event ${line}`);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using console.log for user-facing output. Use the central EmulatorLogger instead to adhere to the repository style guide.

Suggested change
if (request) {
request.handler(rap);
} else {
console.log(`No handler for event ${line}`);
}
if (request) {
request.handler(rap);
} else {
EmulatorLogger.forEmulator(Emulators.STORAGE).log("DEBUG", "No handler for event " + line);
}
References
  1. Use the central logger; never use console.log() for user-facing output. (link)

@joehan
joehan requested a review from aalej July 27, 2026 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants