Skip to content

Commit 3068361

Browse files
patch: ensure that the client pool consistently uses grpc clients after transitioning from rest (#1807)
* Adding docs for the preferRest setting. * Cleaned up the logic around the grpc transition when using preferRest, so that the client pool consistently uses grpc after the transition, rather than occasionally reverting back to REST.
1 parent a5b680d commit 3068361

File tree

4 files changed

+213
-7
lines changed

4 files changed

+213
-7
lines changed

dev/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,13 @@ export class Firestore implements firestore.Firestore {
527527
* to `true`, these properties are skipped and not written to Firestore. If
528528
* set `false` or omitted, the SDK throws an exception when it encounters
529529
* properties of type `undefined`.
530+
* @param {boolean=} settings.preferRest Whether to force the use of HTTP/1.1 REST
531+
* transport until a method that requires gRPC is called. When a method requires gRPC,
532+
* this Firestore client will load dependent gRPC libraries and then use gRPC transport
533+
* for communication from that point forward. Currently the only operation
534+
* that requires gRPC is creating a snapshot listener with the method
535+
* `DocumentReference<T>.onSnapshot()`, `CollectionReference<T>.onSnapshot()`, or
536+
* `Query<T>.onSnapshot()`.
530537
*/
531538
constructor(settings?: firestore.Settings) {
532539
const libraryHeader = {

dev/src/pool.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ export class ClientPool<T> {
9393
let selectedClient: T | null = null;
9494
let selectedClientRequestCount = -1;
9595

96+
// Transition to grpc when we see the first operation that requires grpc.
97+
this.grpcEnabled = this.grpcEnabled || requiresGrpc;
98+
99+
// Require a grpc client for this operation if we have transitioned to grpc.
100+
requiresGrpc = requiresGrpc || this.grpcEnabled;
101+
96102
for (const [client, metadata] of this.activeClients) {
97103
// Use the "most-full" client that can still accommodate the request
98104
// in order to maximize the number of idle clients as operations start to
@@ -101,7 +107,7 @@ export class ClientPool<T> {
101107
!this.failedClients.has(client) &&
102108
metadata.activeRequestCount > selectedClientRequestCount &&
103109
metadata.activeRequestCount < this.concurrentOperationLimit &&
104-
(!requiresGrpc || metadata.grpcEnabled)
110+
(metadata.grpcEnabled || !requiresGrpc)
105111
) {
106112
selectedClient = client;
107113
selectedClientRequestCount = metadata.activeRequestCount;
@@ -223,6 +229,21 @@ export class ClientPool<T> {
223229
return activeOperationCount;
224230
}
225231

232+
/**
233+
* The currently active clients.
234+
*
235+
* @return The currently active clients.
236+
* @private
237+
* @internal
238+
*/
239+
// Visible for testing.
240+
get _activeClients(): Map<
241+
T,
242+
{activeRequestCount: number; grpcEnabled: boolean}
243+
> {
244+
return this.activeClients;
245+
}
246+
226247
/**
227248
* Runs the provided operation in this pool. This function may create an
228249
* additional client if all existing clients already operate at the concurrent

dev/test/pool.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,26 @@ function deferredPromises(count: number): Array<Deferred<void>> {
3434
return deferred;
3535
}
3636

37+
function assertOpCount<T>(
38+
pool: ClientPool<T>,
39+
grpcClientOpCount: number,
40+
restClientOpCount: number
41+
): void {
42+
let actualGrpcClientOpCount = 0;
43+
let actualRestClientOpCount = 0;
44+
45+
pool._activeClients.forEach(clientConfig => {
46+
if (clientConfig.grpcEnabled) {
47+
actualGrpcClientOpCount += clientConfig.activeRequestCount;
48+
} else {
49+
actualRestClientOpCount += clientConfig.activeRequestCount;
50+
}
51+
});
52+
53+
expect(actualGrpcClientOpCount).to.equal(grpcClientOpCount);
54+
expect(actualRestClientOpCount).to.equal(restClientOpCount);
55+
}
56+
3757
describe('Client pool', () => {
3858
it('creates new instances as needed', () => {
3959
const clientPool = new ClientPool<{}>(3, 0, () => {
@@ -133,6 +153,7 @@ describe('Client pool', () => {
133153
() => operationPromises[1].promise
134154
);
135155
expect(clientPool.size).to.equal(2);
156+
assertOpCount(clientPool, 1, 1);
136157

137158
operationPromises[0].resolve();
138159
operationPromises[1].resolve();
@@ -156,9 +177,166 @@ describe('Client pool', () => {
156177
() => operationPromises[1].promise
157178
);
158179
expect(clientPool.size).to.equal(1);
180+
assertOpCount(clientPool, 2, 0);
181+
182+
operationPromises[0].resolve();
183+
operationPromises[1].resolve();
184+
});
185+
186+
it('does not re-use rest instance after beginning the transition to grpc', async () => {
187+
const clientPool = new ClientPool<{}>(10, 1, () => {
188+
return {};
189+
});
190+
191+
const operationPromises = deferredPromises(3);
192+
193+
void clientPool.run(
194+
REQUEST_TAG,
195+
USE_REST,
196+
() => operationPromises[0].promise
197+
);
198+
void clientPool.run(
199+
REQUEST_TAG,
200+
USE_GRPC,
201+
() => operationPromises[1].promise
202+
);
203+
void clientPool.run(
204+
REQUEST_TAG,
205+
USE_REST,
206+
() => operationPromises[2].promise
207+
);
208+
209+
expect(clientPool.size).to.equal(2);
210+
assertOpCount(clientPool, 2, 1);
159211

160212
operationPromises[0].resolve();
161213
operationPromises[1].resolve();
214+
operationPromises[2].resolve();
215+
});
216+
217+
it('does not re-use rest instance after beginning the transition to grpc - rest operation resolved', async () => {
218+
const clientPool = new ClientPool<{}>(10, 1, () => {
219+
return {};
220+
});
221+
222+
const operationPromises = deferredPromises(3);
223+
224+
const restOperation = clientPool.run(
225+
REQUEST_TAG,
226+
USE_REST,
227+
() => operationPromises[0].promise
228+
);
229+
void clientPool.run(
230+
REQUEST_TAG,
231+
USE_GRPC,
232+
() => operationPromises[1].promise
233+
);
234+
235+
// resolve rest operation
236+
operationPromises[0].resolve();
237+
await restOperation;
238+
expect(clientPool.opCount).to.equal(1);
239+
240+
// Run new rest operation
241+
void clientPool.run(
242+
REQUEST_TAG,
243+
USE_REST,
244+
() => operationPromises[2].promise
245+
);
246+
247+
// Assert client pool status
248+
expect(clientPool.size).to.equal(1);
249+
assertOpCount(clientPool, 2, 0);
250+
251+
operationPromises[1].resolve();
252+
operationPromises[2].resolve();
253+
});
254+
255+
it('does not re-use rest instance after beginning the transition to grpc - grpc client full', async () => {
256+
const operationLimit = 10;
257+
const clientPool = new ClientPool<{}>(operationLimit, 1, () => {
258+
return {};
259+
});
260+
261+
const restPromises = deferredPromises(operationLimit);
262+
const grpcPromises = deferredPromises(1);
263+
264+
// First operation use GRPC
265+
void clientPool.run(REQUEST_TAG, USE_GRPC, () => grpcPromises[0].promise);
266+
267+
// Next X operations can use rest, this will fill the first
268+
// client and create a new client.
269+
// The new client should use GRPC since we have transitioned.
270+
restPromises.forEach(restPromise => {
271+
void clientPool.run(REQUEST_TAG, USE_REST, () => restPromise.promise);
272+
});
273+
expect(clientPool.opCount).to.equal(11);
274+
expect(clientPool.size).to.equal(2);
275+
assertOpCount(clientPool, 11, 0);
276+
277+
grpcPromises.forEach(grpcPromise => grpcPromise.resolve());
278+
restPromises.forEach(restPromise => restPromise.resolve());
279+
});
280+
281+
it('does not re-use rest instance after beginning the transition to grpc - multiple rest clients', async () => {
282+
const operationLimit = 10;
283+
const clientPool = new ClientPool<{}>(operationLimit, 1, () => {
284+
return {};
285+
});
286+
287+
const restPromises = deferredPromises(15);
288+
const grpcPromises = deferredPromises(5);
289+
290+
// First 15 operations can use rest, this will fill the first
291+
// client and create a new client.
292+
restPromises.forEach(restPromise => {
293+
void clientPool.run(REQUEST_TAG, USE_REST, () => restPromise.promise);
294+
});
295+
expect(clientPool.opCount).to.equal(15);
296+
expect(clientPool.size).to.equal(2);
297+
assertOpCount(clientPool, 0, 15);
298+
299+
// Next 5 operations alternate between gRPC and REST, this will create a new client using gRPC
300+
let transport = USE_GRPC;
301+
grpcPromises.forEach(grpcPromise => {
302+
void clientPool.run(REQUEST_TAG, transport, () => grpcPromise.promise);
303+
transport = !transport;
304+
});
305+
expect(clientPool.opCount).to.equal(20);
306+
expect(clientPool.size).to.equal(3);
307+
assertOpCount(clientPool, 5, 15);
308+
309+
grpcPromises.forEach(grpcPromise => grpcPromise.resolve());
310+
restPromises.forEach(restPromise => restPromise.resolve());
311+
});
312+
313+
it('does not re-use rest instance after beginning the transition to grpc - grpc client RST_STREAM', async () => {
314+
const clientPool = new ClientPool<{}>(10, 1, () => {
315+
return {};
316+
});
317+
318+
const operationPromises = deferredPromises(1);
319+
320+
const grpcOperation = clientPool.run(REQUEST_TAG, USE_GRPC, () =>
321+
Promise.reject(
322+
new GoogleError('13 INTERNAL: Received RST_STREAM with code 2')
323+
)
324+
);
325+
326+
await grpcOperation.catch(e => e);
327+
328+
// Run new rest operation
329+
void clientPool.run(
330+
REQUEST_TAG,
331+
USE_REST,
332+
() => operationPromises[0].promise
333+
);
334+
335+
// Assert client pool status
336+
expect(clientPool.size).to.equal(1);
337+
assertOpCount(clientPool, 1, 0);
338+
339+
operationPromises[0].resolve();
162340
});
163341

164342
it('bin packs operations', async () => {

types/firestore.d.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -290,12 +290,12 @@ declare namespace FirebaseFirestore {
290290
ignoreUndefinedProperties?: boolean;
291291

292292
/**
293-
* Use HTTP for requests that can be served over HTTP and JSON. This reduces
294-
* the amount of networking code that is loaded to serve requests within
295-
* Firestore.
296-
*
297-
* This setting does not apply to `onSnapshot` APIs as they cannot be served
298-
* over native HTTP.
293+
* Whether to force the use of HTTP/1.1 REST transport until a method that requires gRPC
294+
* is called. When a method requires gRPC, this Firestore client will load dependent gRPC
295+
* libraries and then use gRPC transport for communication from that point forward.
296+
* Currently the only operation that requires gRPC is creating a snapshot listener with
297+
* the method `DocumentReference<T>.onSnapshot()`, `CollectionReference<T>.onSnapshot()`,
298+
* or `Query<T>.onSnapshot()`.
299299
*/
300300
preferRest?: boolean;
301301

0 commit comments

Comments
 (0)