Skip to content

Commit 57e9210

Browse files
Merge branch 'main' into node-18
2 parents c31cd4b + 42b9f48 commit 57e9210

File tree

4 files changed

+218
-42
lines changed

4 files changed

+218
-42
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@
44

55
[1]: https://www.npmjs.com/package/@google-cloud/firestore?activeTab=versions
66

7+
## [7.11.6](https://github.com/googleapis/nodejs-firestore/compare/v7.11.5...v7.11.6) (2025-09-26)
8+
9+
10+
### Bug Fixes
11+
12+
* Pool.ts: add even more logging ([c508d1b](https://github.com/googleapis/nodejs-firestore/commit/c508d1bd653f5d2b9bbe78236fac15e999c27e69))
13+
14+
## [7.11.5](https://github.com/googleapis/nodejs-firestore/compare/v7.11.4...v7.11.5) (2025-09-22)
15+
16+
17+
### Bug Fixes
18+
19+
* Pool.ts: add more detailed logging for client garbage collection ([#2420](https://github.com/googleapis/nodejs-firestore/issues/2420)) ([1bbca46](https://github.com/googleapis/nodejs-firestore/commit/1bbca46ff2a6ea98b52a83ff7dae6092e69b044d))
20+
721
## [7.11.4](https://github.com/googleapis/nodejs-firestore/compare/v7.11.3...v7.11.4) (2025-09-16)
822

923

dev/src/pool.ts

Lines changed: 202 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {GoogleError} from 'google-gax';
1818
import * as assert from 'assert';
1919

2020
import {logger} from './logger';
21-
import {Deferred, requestTag as generateClientId} from './util';
21+
import {Deferred, requestTag as generateTag} from './util';
2222

2323
export const CLIENT_TERMINATED_ERROR_MSG =
2424
'The client has already been terminated';
@@ -78,6 +78,11 @@ export class ClientPool<T extends object> {
7878
*/
7979
private readonly terminateDeferred = new Deferred<void>();
8080

81+
/**
82+
* A unique identifier for this object, for inclusion in log messages.
83+
*/
84+
private readonly instanceId = 'cpl' + generateTag();
85+
8186
/**
8287
* @param concurrentOperationLimit The number of operations that each client
8388
* can handle.
@@ -137,16 +142,16 @@ export class ClientPool<T extends object> {
137142
if (selectedClient) {
138143
const selectedClientId = this.clientIdByClient.get(selectedClient);
139144
logger(
140-
'ClientPool.acquire',
145+
`ClientPool[${this.instanceId}].acquire`,
141146
requestTag,
142147
'Re-using existing client [%s] with %s remaining operations',
143148
selectedClientId,
144149
this.concurrentOperationLimit - selectedClientRequestCount,
145150
);
146151
} else {
147-
const newClientId = 'cli' + generateClientId();
152+
const newClientId = 'cli' + generateTag();
148153
logger(
149-
'ClientPool.acquire',
154+
`ClientPool[${this.instanceId}].acquire`,
150155
requestTag,
151156
'Creating a new client [%s] (requiresGrpc: %s)',
152157
newClientId,
@@ -176,8 +181,10 @@ export class ClientPool<T extends object> {
176181
* @internal
177182
*/
178183
private async release(requestTag: string, client: T): Promise<void> {
184+
const clientId = this.clientIdByClient.get(client);
179185
const metadata = this.activeClients.get(client);
180186
assert(metadata && metadata.activeRequestCount > 0, 'No active requests');
187+
181188
this.activeClients.set(client, {
182189
grpcEnabled: metadata.grpcEnabled,
183190
activeRequestCount: metadata.activeRequestCount - 1,
@@ -186,26 +193,39 @@ export class ClientPool<T extends object> {
186193
this.terminateDeferred.resolve();
187194
}
188195

189-
if (this.shouldGarbageCollectClient(client)) {
190-
const clientId = this.clientIdByClient.get(client);
191-
logger(
192-
'ClientPool.release',
193-
requestTag,
194-
'Garbage collecting client [%s] (%s)',
195-
clientId,
196-
this.lazyLogStringForAllClientIds,
197-
);
198-
this.activeClients.delete(client);
199-
this.failedClients.delete(client);
200-
await this.clientDestructor(client);
201-
logger(
202-
'ClientPool.release',
203-
requestTag,
204-
'Garbage collected client [%s] (%s)',
205-
clientId,
206-
this.lazyLogStringForAllClientIds,
207-
);
196+
const gcDetermination = this.shouldGarbageCollectClient(client);
197+
logger(
198+
`ClientPool[${this.instanceId}].release`,
199+
requestTag,
200+
'Releasing client [%s] (gc=%s)',
201+
clientId,
202+
gcDetermination
203+
);
204+
205+
if (!gcDetermination.shouldGarbageCollectClient) {
206+
return;
208207
}
208+
209+
logger(
210+
`ClientPool[${this.instanceId}].release`,
211+
requestTag,
212+
'Garbage collecting client [%s] (%s)',
213+
clientId,
214+
this.lazyLogStringForAllClientIds
215+
);
216+
217+
const activeClientDeleted = this.activeClients.delete(client);
218+
this.failedClients.delete(client);
219+
await this.clientDestructor(client);
220+
221+
logger(
222+
`ClientPool[${this.instanceId}].release`,
223+
requestTag,
224+
'Garbage collected client [%s] activeClientDeleted=%s (%s)',
225+
clientId,
226+
activeClientDeleted,
227+
this.lazyLogStringForAllClientIds
228+
);
209229
}
210230

211231
/**
@@ -214,23 +234,36 @@ export class ClientPool<T extends object> {
214234
* @private
215235
* @internal
216236
*/
217-
private shouldGarbageCollectClient(client: T): boolean {
237+
private shouldGarbageCollectClient(
238+
client: T
239+
): ShouldGarbageCollectClientResult {
218240
const clientMetadata = this.activeClients.get(client)!;
219241

220242
if (clientMetadata.activeRequestCount !== 0) {
221243
// Don't garbage collect clients that have active requests.
222-
return false;
244+
return new ClientHasActiveRequests({
245+
shouldGarbageCollectClient: false,
246+
clientActiveRequestCount: clientMetadata.activeRequestCount,
247+
});
223248
}
224249

225250
if (this.grpcEnabled !== clientMetadata.grpcEnabled) {
226251
// We are transitioning to GRPC. Garbage collect REST clients.
227-
return true;
252+
return new PoolIsTransitioningToGrpc({
253+
shouldGarbageCollectClient: true,
254+
clientActiveRequestCount: clientMetadata.activeRequestCount,
255+
poolGrpcEnabled: this.grpcEnabled,
256+
clientGrpcEnabled: clientMetadata.grpcEnabled,
257+
});
228258
}
229259

230260
// Idle clients that have received RST_STREAM errors are always garbage
231261
// collected.
232262
if (this.failedClients.has(client)) {
233-
return true;
263+
return new ClientIsFailed({
264+
shouldGarbageCollectClient: true,
265+
clientActiveRequestCount: clientMetadata.activeRequestCount,
266+
});
234267
}
235268

236269
// Otherwise, only garbage collect if we have too much idle capacity (e.g.
@@ -240,9 +273,17 @@ export class ClientPool<T extends object> {
240273
idleCapacityCount +=
241274
this.concurrentOperationLimit - metadata.activeRequestCount;
242275
}
243-
return (
244-
idleCapacityCount > this.maxIdleClients * this.concurrentOperationLimit
245-
);
276+
277+
const maxIdleCapacityCount =
278+
this.maxIdleClients * this.concurrentOperationLimit;
279+
return new IdleCapacity({
280+
shouldGarbageCollectClient: idleCapacityCount > maxIdleCapacityCount,
281+
clientActiveRequestCount: clientMetadata.activeRequestCount,
282+
idleCapacityCount: idleCapacityCount,
283+
maxIdleCapacityCount: maxIdleCapacityCount,
284+
maxIdleClients: this.maxIdleClients,
285+
concurrentOperationLimit: this.concurrentOperationLimit,
286+
});
246287
}
247288

248289
/**
@@ -333,14 +374,20 @@ export class ClientPool<T extends object> {
333374
// Wait for all pending operations to complete before terminating.
334375
if (this.opCount > 0) {
335376
logger(
336-
'ClientPool.terminate',
377+
`ClientPool[${this.instanceId}].terminate`,
337378
/* requestTag= */ null,
338379
'Waiting for %s pending operations to complete before terminating (%s)',
339380
this.opCount,
340381
this.lazyLogStringForAllClientIds,
341382
);
342383
await this.terminateDeferred.promise;
343384
}
385+
logger(
386+
`ClientPool[${this.instanceId}].terminate`,
387+
/* requestTag= */ null,
388+
'Closing all active clients (%s)',
389+
this.lazyLogStringForAllClientIds
390+
);
344391
for (const [client] of this.activeClients) {
345392
this.activeClients.delete(client);
346393
await this.clientDestructor(client);
@@ -354,12 +401,12 @@ export class ClientPool<T extends object> {
354401
* failed clients.
355402
*/
356403
class LazyLogStringForAllClientIds<T extends object> {
357-
private readonly activeClients: Map<T, unknown>;
404+
private readonly activeClients: Map<T, {activeRequestCount: number}>;
358405
private readonly failedClients: Set<T>;
359406
private readonly clientIdByClient: WeakMap<T, string>;
360407

361408
constructor(config: {
362-
activeClients: Map<T, unknown>;
409+
activeClients: Map<T, {activeRequestCount: number}>;
363410
failedClients: Set<T>;
364411
clientIdByClient: WeakMap<T, string>;
365412
}) {
@@ -369,20 +416,135 @@ class LazyLogStringForAllClientIds<T extends object> {
369416
}
370417

371418
toString(): string {
419+
const activeClientsDescription = Array.from(this.activeClients.entries())
420+
.map(
421+
([client, metadata]) =>
422+
`${this.clientIdByClient.get(client)}=${metadata.activeRequestCount}`
423+
)
424+
.sort()
425+
.join(', ');
426+
const failedClientsDescription = Array.from(this.failedClients)
427+
.map(client => `${this.clientIdByClient.get(client)}`)
428+
.sort()
429+
.join(', ');
430+
372431
return (
373432
`${this.activeClients.size} active clients: {` +
374-
this.logStringFromClientIds(this.activeClients.keys()) +
433+
activeClientsDescription +
375434
'}, ' +
376435
`${this.failedClients.size} failed clients: {` +
377-
this.logStringFromClientIds(this.failedClients) +
436+
failedClientsDescription +
378437
'}'
379438
);
380439
}
440+
}
381441

382-
private logStringFromClientIds(clients: Iterable<T>): string {
383-
return Array.from(clients)
384-
.map(client => this.clientIdByClient.get(client) ?? '<unknown>')
385-
.sort()
386-
.join(', ');
442+
/**
443+
* Minimum data to be included in the objects returned from
444+
* ClientPool.shouldGarbageCollectClient().
445+
*/
446+
abstract class BaseShouldGarbageCollectClientResult {
447+
abstract readonly name: string;
448+
abstract readonly shouldGarbageCollectClient: boolean;
449+
abstract readonly clientActiveRequestCount: number;
450+
451+
/**
452+
* Return a terse, one-line string representation. This makes it easy to
453+
* grep through log output to find the logged values.
454+
*/
455+
toString(): string {
456+
const propertyStrings: string[] = [];
457+
for (const propertyName of Object.getOwnPropertyNames(this)) {
458+
const propertyValue = this[propertyName as keyof typeof this];
459+
propertyStrings.push(`${propertyName}=${propertyValue}`);
460+
}
461+
return '{' + propertyStrings.join(', ') + '}';
462+
}
463+
}
464+
465+
class ClientHasActiveRequests extends BaseShouldGarbageCollectClientResult {
466+
override readonly name = 'ClientHasActiveRequests' as const;
467+
override readonly shouldGarbageCollectClient: false;
468+
override readonly clientActiveRequestCount: number;
469+
470+
constructor(args: {
471+
shouldGarbageCollectClient: false;
472+
clientActiveRequestCount: number;
473+
}) {
474+
super();
475+
this.shouldGarbageCollectClient = args.shouldGarbageCollectClient;
476+
this.clientActiveRequestCount = args.clientActiveRequestCount;
387477
}
388478
}
479+
480+
class PoolIsTransitioningToGrpc extends BaseShouldGarbageCollectClientResult {
481+
override readonly name = 'PoolIsTransitioningToGrpc' as const;
482+
override readonly shouldGarbageCollectClient: true;
483+
override readonly clientActiveRequestCount: 0;
484+
readonly poolGrpcEnabled: boolean;
485+
readonly clientGrpcEnabled: boolean;
486+
487+
constructor(args: {
488+
shouldGarbageCollectClient: true;
489+
clientActiveRequestCount: 0;
490+
poolGrpcEnabled: boolean;
491+
clientGrpcEnabled: boolean;
492+
}) {
493+
super();
494+
this.shouldGarbageCollectClient = args.shouldGarbageCollectClient;
495+
this.clientActiveRequestCount = args.clientActiveRequestCount;
496+
this.poolGrpcEnabled = args.poolGrpcEnabled;
497+
this.clientGrpcEnabled = args.clientGrpcEnabled;
498+
}
499+
}
500+
501+
class ClientIsFailed extends BaseShouldGarbageCollectClientResult {
502+
override readonly name = 'ClientIsFailed' as const;
503+
override readonly shouldGarbageCollectClient: true;
504+
override readonly clientActiveRequestCount: 0;
505+
506+
constructor(args: {
507+
shouldGarbageCollectClient: true;
508+
clientActiveRequestCount: 0;
509+
}) {
510+
super();
511+
this.shouldGarbageCollectClient = args.shouldGarbageCollectClient;
512+
this.clientActiveRequestCount = args.clientActiveRequestCount;
513+
}
514+
}
515+
516+
class IdleCapacity extends BaseShouldGarbageCollectClientResult {
517+
override readonly name = 'IdleCapacity' as const;
518+
override readonly shouldGarbageCollectClient: boolean;
519+
override readonly clientActiveRequestCount: 0;
520+
readonly idleCapacityCount: number;
521+
readonly maxIdleCapacityCount: number;
522+
readonly maxIdleClients: number;
523+
readonly concurrentOperationLimit: number;
524+
525+
constructor(args: {
526+
shouldGarbageCollectClient: boolean;
527+
clientActiveRequestCount: 0;
528+
idleCapacityCount: number;
529+
maxIdleCapacityCount: number;
530+
maxIdleClients: number;
531+
concurrentOperationLimit: number;
532+
}) {
533+
super();
534+
this.shouldGarbageCollectClient = args.shouldGarbageCollectClient;
535+
this.clientActiveRequestCount = args.clientActiveRequestCount;
536+
this.idleCapacityCount = args.idleCapacityCount;
537+
this.maxIdleCapacityCount = args.maxIdleCapacityCount;
538+
this.maxIdleClients = args.maxIdleClients;
539+
this.concurrentOperationLimit = args.concurrentOperationLimit;
540+
}
541+
}
542+
543+
/**
544+
* The set of return types from ClientPool.shouldGarbageCollectClient().
545+
*/
546+
type ShouldGarbageCollectClientResult =
547+
| ClientHasActiveRequests
548+
| PoolIsTransitioningToGrpc
549+
| ClientIsFailed
550+
| IdleCapacity;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@google-cloud/firestore",
33
"description": "Firestore Client Library for Node.js",
4-
"version": "7.11.4",
4+
"version": "7.11.6",
55
"license": "Apache-2.0",
66
"author": "Google Inc.",
77
"engines": {

samples/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"test": "mocha --timeout 600000"
1212
},
1313
"dependencies": {
14-
"@google-cloud/firestore": "^7.11.4"
14+
"@google-cloud/firestore": "^7.11.6"
1515
},
1616
"devDependencies": {
1717
"chai": "^4.2.0",

0 commit comments

Comments
 (0)