Repository navigation
Conversation
…nt on stop (b/565073134)
There was a problem hiding this comment.
Code Review
This pull request updates the PubsubEmulator's stop method to gracefully close active subscriptions and the Pub/Sub client before stopping the emulator, and adds corresponding unit tests. The feedback highlights multiple instances where JSON.stringify(err) is used on Error objects, which results in empty objects ("{}") in the debug logs; it is recommended to safely extract the error message instead.
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
…task-clean-up-pubsub-streaming-subs
| this.logger.logLabeled("DEBUG", "pubsub", "Pubsub kill output: " + JSON.stringify(buffer)); | ||
| const closePromises = Array.from(this.subscriptionForTopic.values()).map(async (sub) => { | ||
| try { | ||
| await sub.close(); |
There was a problem hiding this comment.
sub.close() has no time limit, so shutdown could hang. In @google-cloud/pubsub v5 (^5.2.0 here), Subscriber.close() nacks in-flight messages and then waits for the ack/nack flush. The default wait is maxExtensionTime, which is 60 minutes by default. In the normal case the emulator is still up when this runs, so it should be quick. But if the emulator process has already died, Ctrl-C on emulators:start or emulators:exec could hang there. Either:
set closeOptions: { timeout: ... } (e.g., a few seconds) when the subscription is created/obtained in maybeCreateTopicAndSub (the field is a subscriber option — check that createSubscription passes it through), or
wrap the close calls in Promise.race with a short timer.
A test that hangs one subscription's close would cover this.
There was a problem hiding this comment.
Wrapped all subscription close() calls (and this._pubsub.close()) in a withTimeout helper bounded by SUBSCRIPTION_CLOSE_TIMEOUT_MS (2s) via Promise.race with proper timer cleanup. Added unit tests using fake timers (sandbox.useFakeTimers()) to verify that hanging subscription or client close calls time out and do not block shutdown.
| }); | ||
| await Promise.all(closePromises); | ||
| this.subscriptionForTopic.clear(); | ||
| this.triggersForTopic.clear(); |
There was a problem hiding this comment.
Because await this.maybeCreateTopicAndSub(topicName) is called unconditionally before checking this.subscriptionForTopic.has(topicName) or triggers.some(...), every call to addTrigger() for an existing topicName (e.g., when multiple Cloud Functions trigger on the same Pub/Sub topic, or when triggers are re-registered on reload) creates a new Subscription instance with a live sub.on("message", ...) StreamingPull listener and overwrites the previous Subscription in this.subscriptionForTopic (or drops it on early return).
Those overwritten Subscription instances are lost from this.subscriptionForTopic and therefore won't be closed by stop().
There was a problem hiding this comment.
Updated addTrigger to check if (!this.subscriptionForTopic.has(topicName)) before creating topic/sub. When multiple Cloud Functions trigger on the same topic (or triggers are registered across reloads), the existing subscription is preserved and reused. Added tests in pubsubEmulator.spec.ts verifying that maybeCreateTopicAndSub is called only once for multiple triggers on the same topic and that duplicate triggers are ignored.
| const emulator = new PubsubEmulator({ projectId: "test-project" }); | ||
|
|
||
| const closeStub = sandbox.stub().resolves(); | ||
| const emulatorWithClient = emulator as unknown as { |
There was a problem hiding this comment.
Can we remove all references of "as unknown as" where practical?
There was a problem hiding this comment.
Defined lightweight structural interfaces PubsubSubscription and PubsubClient in src/emulator/pubsubEmulator.ts so that test doubles satisfy types directly without requiring type assertions. Removed all references of as unknown as (and as any) from pubsubEmulator.spec.ts.
…up-pubsub-streaming-subs' into ai-improve-565073134-task-clean-up-pubsub-streaming-subs
Description
Resolves Buganizer b/565073134
In
src/emulator/pubsubEmulator.ts,this.subscriptionForTopicmaintains active Google Cloud Pub/SubSubscriptioninstances connected to the emulator. WhenPubsubEmulator.stop()was invoked, it only stopped the underlying downloadable emulator process, leaving active StreamingPull gRPC channels open. As a result, the orphaned gRPC client repeatedly attempted to reconnect to the closed port, loggingUNAVAILABLEerrors, leaking socket handles and timers, and keeping the Node.js event loop active.This change:
this.subscriptionForTopic.values()and closes each cleanly before shutting down the emulator process.await this._pubsub.close()and resetsthis._pubsub = undefined.this.subscriptionForTopicandthis.triggersForTopic.downloadableEmulators.stop(Emulators.PUBSUB)is always executed in afinallyblock.src/emulator/pubsubEmulator.spec.tsvalidating complete teardown and error resilience.Scenarios Tested
_pubsubclient, verifying gRPC client close and property reset.npm run buildandnpx mocha 'src/emulator/pubsubEmulator.spec.ts'.