Repository navigation
[SSR Agent] Issue Fix (22588): Fix silent hang in MessageBus.request when publish fails - #28816
joneba-google wants to merge 2 commits into
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This PR addresses a bug where MessageBus.request() would hang indefinitely if the underlying publish() call failed. By attaching an error handler to the promise, the system now correctly cleans up resources and rejects the request promise upon failure, improving reliability and error reporting. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
|
📊 PR Size: size/M
|
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
|
You already have 7 pull requests open. Please work on getting existing PRs merged before opening more. |
There was a problem hiding this comment.
Code Review
This pull request adds error handling to the request method in MessageBus by catching publication errors and rejecting the request promise, along with a corresponding unit test. However, the reviewer points out that MessageBus.publish() catches errors internally and resolves successfully (emitting an 'error' event), meaning the added .catch() block will not be triggered in production and the promise will still hang. Additionally, the unit test mocks publish to reject, which does not accurately reflect this production behavior. The reviewer also notes that asynchronous operations waiting for user input should rely on the provided AbortSignal for cancellation rather than a separate timeout.
| this.publish({ ...request, correlationId } as TRequest).catch( | ||
| (error: unknown) => { | ||
| cleanup(); | ||
| reject(error); | ||
| }, | ||
| ); |
There was a problem hiding this comment.
In MessageBus.publish(), all errors are caught internally and emitted via this.emit('error', error). If there is an 'error' listener registered on the MessageBus (which is standard practice in production for logging/monitoring), this.emit will execute successfully and the promise returned by this.publish() will resolve rather than reject. Because of this, the .catch() block added here in request() will never be triggered when a publication error occurs (such as a policy check failure or invalid message structure). The request() promise will still silently hang. Additionally, asynchronous operations waiting for user input via the MessageBus should rely on the provided AbortSignal for cancellation, rather than implementing a separate timeout, to maintain consistency with existing patterns.
References
- Asynchronous operations waiting for user input via the MessageBus should rely on the provided AbortSignal for cancellation, rather than implementing a separate timeout, to maintain consistency with existing patterns.
| it('should reject immediately if publish fails/rejects', async () => { | ||
| const publishError = new Error('Publish failed'); | ||
| vi.spyOn(messageBus, 'publish').mockRejectedValue(publishError); | ||
|
|
||
| const request: Omit<ToolConfirmationRequest, 'correlationId'> = { | ||
| type: MessageBusType.TOOL_CONFIRMATION_REQUEST, | ||
| toolCall: { name: 'test-tool', args: {} }, | ||
| }; | ||
|
|
||
| const requestPromise = messageBus.request< | ||
| ToolConfirmationRequest, | ||
| ToolConfirmationResponse | ||
| >(request, MessageBusType.TOOL_CONFIRMATION_RESPONSE, 2000); | ||
|
|
||
| await expect(requestPromise).rejects.toThrow('Publish failed'); | ||
| }); |
There was a problem hiding this comment.
This test mocks messageBus.publish to reject using mockRejectedValue. However, in production, publish() catches all errors internally and resolves successfully (only emitting an 'error' event) if there is an 'error' listener registered. Therefore, this test does not accurately reflect production behavior where request() will still hang. Consider updating publish() to rethrow errors so that this test scenario matches real-world behavior.
…and add AbortSignal support $fixes google-gemini#22588 ### Context & Problem In `MessageBus.request()`, calling `this.publish()` was a floating promise without failure handling. Additionally, `MessageBus.publish()` was swallowing all errors inside its internal catch block without rethrowing, causing `publish()` to resolve cleanly when errors occurred (such as policy check or invalid message errors), which caused `request()` to silently hang. Furthermore, `request()` lacked support for `AbortSignal` for standardized cancellation. ### Detailed Changes * **packages/core/src/confirmation-bus/message-bus.ts**: - Updated `publish()` to rethrow errors after emitting `'error'`, ensuring that publication failures reject the returned promise. - Updated `request()` to catch publication failures and reject immediately, avoiding silent hangs. - Added support for `AbortSignal` in `request()` via `timeoutMsOrOptions?: number | { timeoutMs?: number; signal?: AbortSignal }` for cooperative cancellation. * **packages/core/src/confirmation-bus/message-bus.test.ts**: - Updated tests to verify that `publish()` rejects on policy/validation failures. - Added unit tests for `request()` publication rejection, internal error rejection, and `AbortSignal` cancellation (both live abort and pre-aborted signals). ### Verification * Verified with full Vitest unit test suite (`21 tests passed` in `message-bus.test.ts`). * Verified full build (`npm run build`), typecheck (`npm run typecheck`), and ESLint check passed cleanly.
7b79f46 to
65070c0
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request enhances the MessageBus class by adding support for AbortSignal in the request method to allow request cancellation, and ensures that publish failures are caught and rejected. It also updates publish to rethrow caught errors. The review feedback points out a potential issue where emitting an 'error' event without active listeners in Node.js can synchronously throw an ERR_UNHANDLED_ERROR, bypassing the original error, and suggests checking listenerCount before emitting.
| } catch (error) { | ||
| this.emit('error', error); | ||
| throw error; | ||
| } |
There was a problem hiding this comment.
In Node.js, emitting an 'error' event when there are no registered listeners causes the EventEmitter to throw an ERR_UNHANDLED_ERROR synchronously. Since this happens inside the catch block, it bypasses the original error and propagates the unhandled error wrapper to the caller of publish or request.
To preserve the original error for the caller while still supporting global error logging when listeners are present, check if there are any 'error' listeners before emitting.
} catch (error) {
if (this.listenerCount('error') > 0) {
this.emit('error', error);
}
throw error;
}
adamfweidman
left a comment
There was a problem hiding this comment.
Too complicated to approve without deeper investigation
fixes #22588
Original issue URL: #22588
Context & Problem
In
MessageBus.request(), callingthis.publish()was a floating promise without any register of failure. Ifpublish()rejected, the promise would silently hang for 60 seconds (waiting for a timeout) or cause uncaught process crashes.Detailed Changes
Chained
.catch()onthis.publish()to clear timeout, unsubscribe handler viacleanup(), and reject the promise with the caught error.Added a unit test using Vitest asserting that
request()rejects immediately whenpublish()rejects.Verification
All unit tests passed successfully. The ESLint check passed on all modified files without errors.