-
Notifications
You must be signed in to change notification settings - Fork 148
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
feat: allow specifying how many idle GRPC channels to keep #837
Changes from 1 commit
e3d7882
5e74ffe
c34cf1b
eb13b57
a20e58a
69354ad
0e34859
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 |
---|---|---|
|
@@ -44,13 +44,16 @@ export class ClientPool<T> { | |
/** | ||
* @param concurrentOperationLimit The number of operations that each client | ||
* can handle. | ||
* @param maxIdleClients The maximum number of idle clients to keep before | ||
* garbage collecting. | ||
* @param clientFactory A factory function called as needed when new clients | ||
* are required. | ||
* @param clientDestructor A cleanup function that is called when a client is | ||
* disposed of. | ||
*/ | ||
constructor( | ||
private readonly concurrentOperationLimit: number, | ||
private readonly maxIdleClients: number, | ||
private readonly clientFactory: () => T, | ||
private readonly clientDestructor: (client: T) => Promise<void> = () => | ||
Promise.resolve() | ||
|
@@ -66,8 +69,13 @@ export class ClientPool<T> { | |
let selectedClient: T | null = null; | ||
let selectedRequestCount = 0; | ||
|
||
this.activeClients.forEach((requestCount, client) => { | ||
if (!selectedClient && requestCount < this.concurrentOperationLimit) { | ||
for (const [client, requestCount] of this.activeClients) { | ||
// Bin pack requests to reduce the maximize the number of idle clients as | ||
// operations start to complete | ||
if ( | ||
requestCount > selectedRequestCount && | ||
requestCount < this.concurrentOperationLimit | ||
) { | ||
logger( | ||
'ClientPool.acquire', | ||
requestTag, | ||
|
@@ -77,7 +85,7 @@ export class ClientPool<T> { | |
selectedClient = client; | ||
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 log line above is going to get spewed multiple times now. I think it should be extracted from the for-loop. 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. Good catch. I moved it outside the loop. |
||
selectedRequestCount = requestCount; | ||
} | ||
}); | ||
} | ||
|
||
if (!selectedClient) { | ||
logger('ClientPool.acquire', requestTag, 'Creating a new client'); | ||
|
@@ -99,23 +107,43 @@ export class ClientPool<T> { | |
* @private | ||
*/ | ||
private async release(requestTag: string, client: T): Promise<void> { | ||
let requestCount = this.activeClients.get(client) || 0; | ||
const requestCount = this.activeClients.get(client) || 0; | ||
assert(requestCount > 0, 'No active request'); | ||
this.activeClients.set(client, requestCount! - 1); | ||
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. Seems odd you need the 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. Uhm. I hope I had my reasons when I added it first, but they are not immediately apparent now. Removed. |
||
|
||
requestCount = requestCount! - 1; | ||
this.activeClients.set(client, requestCount); | ||
if (this.shouldGarbageCollectClient(client)) { | ||
this.activeClients.delete(client); | ||
await this.clientDestructor(client); | ||
logger('ClientPool.release', requestTag, 'Garbage collected 1 client'); | ||
} | ||
} | ||
|
||
if (requestCount === 0) { | ||
const deletedCount = await this.garbageCollect(); | ||
if (deletedCount) { | ||
logger( | ||
'ClientPool.release', | ||
requestTag, | ||
'Garbage collected %s clients', | ||
deletedCount | ||
); | ||
} | ||
/** | ||
* Given the current operation counts, determines if the given client should | ||
* be garbage collected. | ||
* @private | ||
*/ | ||
private shouldGarbageCollectClient(client: T): boolean { | ||
if (this.activeClients.get(client) !== 0) { | ||
return false; | ||
} | ||
|
||
// Compute the remaining capacity of the ClientPool. If the capacity exceeds | ||
// the total capacity that `maxIdleClients` could hold, garbage collect. We | ||
// look at the capacity rather than just at the current request count to | ||
// allows us to: | ||
// - Use `maxIdleClients:1` to preserve legacy behavior (there is always at | ||
// least one active client as a single client can never exceed the | ||
// concurrent operation limit by itself). | ||
// - Use `maxIdleClients:0` to shut down the client pool completely when all | ||
// clients are idle. | ||
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. FWIW I read this comment 2 or 3 times, and read the code once, and the code still makes more sense to me than the comment. So it might be worth rephrasing / paring down the comment. I'm not 100% sure what it's trying to get across. Is it justifying the existence of the maxIdleClients setting (in which maybe the bulk of this comment should go there?)? Or is there actually some nuance to this code that needs 10 lines of explanation? 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. I was trying to justify that the code doesn't just count the number of idle clients, but instead counts the number of idle "request slots". If this comment doesn't help, then I am ok with removing it. |
||
let idleCapacityCount = 0; | ||
for (const [_, count] of this.activeClients) { | ||
idleCapacityCount += this.concurrentOperationLimit - count; | ||
} | ||
return ( | ||
idleCapacityCount > this.maxIdleClients * this.concurrentOperationLimit | ||
); | ||
} | ||
|
||
/** | ||
|
@@ -177,28 +205,4 @@ export class ClientPool<T> { | |
await this.clientDestructor(client); | ||
} | ||
} | ||
|
||
/** | ||
* Deletes clients that are no longer executing operations. Keeps up to one | ||
* idle client to reduce future initialization costs. | ||
* | ||
* @return Number of clients deleted. | ||
* @private | ||
*/ | ||
private async garbageCollect(): Promise<number> { | ||
let idleClients = 0; | ||
const cleanUpTasks: Array<Promise<void>> = []; | ||
for (const [client, requestCount] of this.activeClients) { | ||
if (requestCount === 0) { | ||
++idleClients; | ||
|
||
if (idleClients > 1) { | ||
this.activeClients.delete(client); | ||
cleanUpTasks.push(this.clientDestructor(client)); | ||
} | ||
} | ||
} | ||
await Promise.all(cleanUpTasks); | ||
return idleClients - 1; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -72,6 +72,13 @@ export interface Settings { | |
/** Whether to use SSL when connecting. */ | ||
ssl?: boolean; | ||
|
||
/** | ||
* The maximum number of idle GRPC channels to keep. A smaller number of idle | ||
* channels reduces memory usage but increases request latency for clients | ||
* with fluctuating request rates. Defaults to 1. | ||
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. Elsewhere you document that setting it to 0 allows the client pool to shut down completely. Is that a behavior that should be documented here? 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. That's a good suggestion. Added (here and in the other two places...) |
||
*/ | ||
maxIdleChannels?: number; | ||
|
||
// tslint:disable-next-line:no-any | ||
[key: string]: any; // Accept other properties, such as GRPC settings. | ||
} | ||
|
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.
"reduce the maximize the ..."? Also, I don't do bin packing enough for this comment to 100% make sense right away. And I had to think more than 3 seconds to figure out what "selectedRequestCount" was.
Perhaps rename selectedRequestCount to selectedClientRequestCount (long/verbose, I know, sorry!) and the comment to something like:
Don't feel very strongly though.
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.
Your comment is not only grammatically correct, but also more helpful. Thanks!