-
Notifications
You must be signed in to change notification settings - Fork 697
fix(firestore): eager evict idle REST clients on gRPC transition #8817
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -107,6 +107,34 @@ export class ClientPool<T extends object> { | |
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Transitions the pool to gRPC and eagerly garbage-collects idle REST clients. | ||
| * REST clients with in-flight requests will be garbage collected upon request completion. | ||
| */ | ||
| private transitionToGrpc(requestTag: string): void { | ||
| this.grpcEnabled = true; | ||
| logger( | ||
| `ClientPool[${this.instanceId}].acquire`, | ||
| requestTag, | ||
| 'Transitioning pool to gRPC (requiresGrpc: true)', | ||
| ); | ||
|
|
||
| for (const [client, metadata] of this.activeClients) { | ||
| if (!metadata.grpcEnabled && metadata.activeRequestCount === 0) { | ||
| this.activeClients.delete(client); | ||
| this.failedClients.delete(client); | ||
| void Promise.resolve(this.clientDestructor(client)).catch(err => { | ||
| logger( | ||
| `ClientPool[${this.instanceId}].transitionToGrpc`, | ||
| requestTag, | ||
| 'Failed to destroy client: %s', | ||
| err, | ||
| ); | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns an already existing client if it has less than the maximum number | ||
| * of concurrent operations or initializes and returns a new client. | ||
|
|
@@ -118,21 +146,25 @@ export class ClientPool<T extends object> { | |
| let selectedClient: T | null = null; | ||
| let selectedClientRequestCount = -1; | ||
|
|
||
| // Transition to grpc when we see the first operation that requires grpc. | ||
| this.grpcEnabled = this.grpcEnabled || requiresGrpc; | ||
| if (!this.grpcEnabled && requiresGrpc) { | ||
| this.transitionToGrpc(requestTag); | ||
| } | ||
|
|
||
| // Require a grpc client for this operation if we have transitioned to grpc. | ||
| requiresGrpc = requiresGrpc || this.grpcEnabled; | ||
|
|
||
| for (const [client, metadata] of this.activeClients) { | ||
| const isEligible = metadata.grpcEnabled || !requiresGrpc; | ||
| if (!isEligible || this.failedClients.has(client)) { | ||
| continue; | ||
| } | ||
|
|
||
| // Use the "most-full" client that can still accommodate the request | ||
| // in order to maximize the number of idle clients as operations start to | ||
| // complete. | ||
| if ( | ||
| !this.failedClients.has(client) && | ||
| metadata.activeRequestCount > selectedClientRequestCount && | ||
| metadata.activeRequestCount < this.concurrentOperationLimit && | ||
| (metadata.grpcEnabled || !requiresGrpc) | ||
| metadata.activeRequestCount < this.concurrentOperationLimit | ||
| ) { | ||
| selectedClient = client; | ||
| selectedClientRequestCount = metadata.activeRequestCount; | ||
|
|
@@ -270,8 +302,10 @@ export class ClientPool<T extends object> { | |
| // more than 100 idle capacity with default settings). | ||
| let idleCapacityCount = 0; | ||
| for (const [, metadata] of this.activeClients) { | ||
| idleCapacityCount += | ||
| this.concurrentOperationLimit - metadata.activeRequestCount; | ||
| if (metadata.grpcEnabled === this.grpcEnabled) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Active REST Clients Leak After TransitionThere is a potential connection/resource leak for REST clients that are active during the transition to gRPC. The PR description notes:
However, under the new logic in Since the REST client's As a result, the REST client will not be garbage collected and will remain in Suggested FixTo prevent this leak, we should immediately return You can add this check at the very beginning of private shouldGarbageCollectClient(client: T): boolean {
const clientMetadata = this.activeClients.get(client);
if (clientMetadata && clientMetadata.grpcEnabled !== this.grpcEnabled) {
return true;
}
// ... rest of the existing logic
}
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The if (this.grpcEnabled !== clientMetadata.grpcEnabled)which is true since I added a test to verify that the rest connection is garbage collected. |
||
| idleCapacityCount += | ||
| this.concurrentOperationLimit - metadata.activeRequestCount; | ||
| } | ||
| } | ||
|
|
||
| const maxIdleCapacityCount = | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
clientDestructorparameter is optional in theClientPoolconstructor and can beundefined(as demonstrated in some of the unit tests where only 3 arguments are passed). Callingthis.clientDestructor(client)directly without checking if it is defined will throw aTypeErrorat runtime and crash the pool transition.Additionally, wrapping the call in
Promise.resolve(this.clientDestructor(client))does not catch synchronous exceptions thrown by the destructor function itself, which would also propagate and crash the caller.To fix both issues, we should check if
this.clientDestructoris defined, and safely invoke it within a.then()block of a resolved promise chain to ensure any synchronous errors are safely caught and logged.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be a false positive. The
clientDestructoris not optional in theClientPoolconstructor.