Repository navigation
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces support for cancelling ongoing API calls using an AbortSignal passed via CallOptions, and includes comprehensive unit tests. The review feedback highlights a critical memory leak and MaxListenersExceededWarning risk in Node.js because the 'abort' event listener is not cleaned up when the API call completes. A code suggestion is provided to ensure the listener is removed upon call completion.
8b1a548 to
cb55669
Compare
cb55669 to
6664345
Compare
|
I've hand-reviewed this PR. |
6664345 to
2e30d7d
Compare
| signal.addEventListener('abort', abortListener, {once: true}); | ||
| const removeAbortListener = () => { | ||
| signal.removeEventListener('abort', abortListener); | ||
| }; | ||
| if (ongoingCall instanceof StreamProxy) { | ||
| ongoingCall.once('close', removeAbortListener); | ||
| ongoingCall.once('end', removeAbortListener); | ||
| ongoingCall.once('error', removeAbortListener); | ||
| } else { | ||
| const originalCallback = ongoingCall.callback; | ||
| ongoingCall.callback = (...args: Parameters<APICallback>) => { | ||
| removeAbortListener(); | ||
| originalCallback?.(...args); | ||
| }; |
There was a problem hiding this comment.
If signal.aborted is true, do we need to still go through this code? I think it can be moved into the previous else block
| if (signal.aborted) { | ||
| ongoingCall.cancel(); | ||
| } else { |
There was a problem hiding this comment.
I was curious about when .cancel() would actually be invoked, and I think there's a bug here. ongoingCall.cancel() is going to be called synchronously, so I think it's going to cause the error prematurely for both streams and promises.
I think something like
if (signal) {
const abortListener = () => {
ongoingCall.cancel();
};
}
if (signal.aborted) {
process.nextTick(() => {
ongoingCall.cancel();
});
}
might be the solution so the callback execution is deferred until the stack is finished executing.
| ongoingCall.once('close', removeAbortListener); | ||
| ongoingCall.once('end', removeAbortListener); | ||
| ongoingCall.once('error', removeAbortListener); |
There was a problem hiding this comment.
Using .once() will only register the first one that fires, and the other 2 listeners won't get unattached. I'd suggest a cleanup callback:
const cleanup = () => {
removeAbortListener();
ongoingCall.removeListener('close', cleanup);
ongoingCall.removeListener('end', cleanup);
ongoingCall.removeListener('error', cleanup);
};
ongoingCall.once('close', cleanup);
ongoingCall.once('end', cleanup);
ongoingCall.once('error', cleanup);
2e30d7d to
98dd2ff
Compare
Overview
This PR is Layer 1 of 7 in Stack #9467, which implements Publish Hedging for
@google-cloud/pubsubto mitigate high-percentile (p99/p99.9) publish tail latencies by sending backup (hedged) publish RPCs when an in-flight batch takes longer than a configurablehedgeDelaythreshold.Stack Structure
feat(gax): support AbortSignal cancellation in CallOptions and createApiCall(this PR)feat(pubsub): add HedgingOptions validation and HedgingTokenBucketfeat(pubsub): add CancellationSharer and HedgingSchedulerfeat(pubsub): add OpenTelemetry events and logging for hedged publishfeat(pubsub): integrate publish hedging into MessageQueuetest(pubsub): add unit tests for publish hedging queue integrationtest(pubsub): add publish hedging system testsSummary of Changes in This PR
To cleanly cancel losing publish attempts when either the original or a hedged publish RPC wins - even when the gapic client is invoked via callback or
util.promisify- this PR adds standard ECMAAbortSignalsupport togoogle-gax:CallOptions&CallSettings(core/packages/gax/src/gax.ts):signal?: AbortSignaltoCallOptionsandCallSettings, propagatingoptions.signalinCallSettings.prototype.merge.createApiCall(core/packages/gax/src/createApiCall.ts):thisSettings.signalis provided on an API call:signal.abortedis alreadytruewhen the call starts,ongoingCall.cancel()is invoked immediately on the next tick.{once: true}'abort'listener onthisSettings.signalthat invokesongoingCall.cancel(), and removes the listener onceongoingCall.promisesettles to prevent memory leaks.core/packages/gax/test/unit/apiCallable.ts):AbortSignalcancellation for both in-flight calls and pre-aborted signals.