Skip to content

firestore: add experimental option "sendWriteRequestsDelayMs" to enable write request batching in fewer http requests #8895

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

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions common/api-review/firestore.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@ export interface ExperimentalLongPollingOptions {
timeoutSeconds?: number;
}

// @public
export interface ExperimentalOptions {
sendWriteRequestsDelayMs?: number;
}

// @public
export class FieldPath {
constructor(...fieldNames: string[]);
Expand Down Expand Up @@ -252,6 +257,7 @@ export type FirestoreLocalCache = MemoryLocalCache | PersistentLocalCache;
// @public
export interface FirestoreSettings {
cacheSizeBytes?: number;
experimental?: ExperimentalOptions;
experimentalAutoDetectLongPolling?: boolean;
experimentalForceLongPolling?: boolean;
experimentalLongPollingOptions?: ExperimentalLongPollingOptions;
Expand Down
2 changes: 2 additions & 0 deletions docs-devsite/_toc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ toc:
path: /docs/reference/js/firestore_.documentsnapshot.md
- title: ExperimentalLongPollingOptions
path: /docs/reference/js/firestore_.experimentallongpollingoptions.md
- title: ExperimentalOptions
path: /docs/reference/js/firestore_.experimentaloptions.md
- title: FieldPath
path: /docs/reference/js/firestore_.fieldpath.md
- title: FieldValue
Expand Down
45 changes: 45 additions & 0 deletions docs-devsite/firestore_.experimentaloptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
Project: /docs/reference/js/_project.yaml
Book: /docs/reference/_book.yaml
page_type: reference

{% comment %}
DO NOT EDIT THIS FILE!
This is generated by the JS SDK team, and any local changes will be
overwritten. Changes should be made in the source code at
https://github.com/firebase/firebase-js-sdk
{% endcomment %}

# ExperimentalOptions interface
Experimental options to configure the Firestore SDK.

Note: This interface is "experimental" and is subject to change.

See `FirestoreSettings.experimental`<!-- -->.

<b>Signature:</b>

```typescript
export declare interface ExperimentalOptions
```

## Properties

| Property | Type | Description |
| --- | --- | --- |
| [sendWriteRequestsDelayMs](./firestore_.experimentaloptions.md#experimentaloptionssendwriterequestsdelayms) | number | The maximum amount of time, in milliseconds, to wait before sending a Firestore "write" request to the backend. If <code>undefined</code> then do not delay at all.<!-- -->A delay can be useful because it enables the in-memory "write pipeline" to gather together multiple write requests and send them in a single HTTP request to the backend, rather than one HTTP request per write request, as is done when by default, or when this property is <code>undefined</code>. Note that there is a hardcoded limit to the number of write requests that are sent at once, so setting a very large value for this property will not necessarily cause \_all\_ write requests to be sent in a single HTTP request; however, it \_could\_ greatly reduce the number of distinct HTTP requests that are used.<!-- -->The value must be an integer value strictly greater than zero and less than or equal to 10000 (10 seconds). A value of <code>200</code> is a good starting point to minimize write latency yet still enable some amount of batching.<!-- -->See https://github.com/firebase/firebase-js-sdk/issues/5971 for rationale and background information that motivated this option. |

## ExperimentalOptions.sendWriteRequestsDelayMs

The maximum amount of time, in milliseconds, to wait before sending a Firestore "write" request to the backend. If `undefined` then do not delay at all.

A delay can be useful because it enables the in-memory "write pipeline" to gather together multiple write requests and send them in a single HTTP request to the backend, rather than one HTTP request per write request, as is done when by default, or when this property is `undefined`<!-- -->. Note that there is a hardcoded limit to the number of write requests that are sent at once, so setting a very large value for this property will not necessarily cause \_all\_ write requests to be sent in a single HTTP request; however, it \_could\_ greatly reduce the number of distinct HTTP requests that are used.

The value must be an integer value strictly greater than zero and less than or equal to 10000 (10 seconds). A value of `200` is a good starting point to minimize write latency yet still enable some amount of batching.

See https://github.com/firebase/firebase-js-sdk/issues/5971 for rationale and background information that motivated this option.

<b>Signature:</b>

```typescript
sendWriteRequestsDelayMs?: number;
```
11 changes: 11 additions & 0 deletions docs-devsite/firestore_.firestoresettings.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export declare interface FirestoreSettings
| Property | Type | Description |
| --- | --- | --- |
| [cacheSizeBytes](./firestore_.firestoresettings.md#firestoresettingscachesizebytes) | number | NOTE: This field will be deprecated in a future major release. Use <code>cache</code> field instead to specify cache size, and other cache configurations.<!-- -->An approximate cache size threshold for the on-disk data. If the cache grows beyond this size, Firestore will start removing data that hasn't been recently used. The size is not a guarantee that the cache will stay below that size, only that if the cache exceeds the given size, cleanup will be attempted.<!-- -->The default value is 40 MB. The threshold must be set to at least 1 MB, and can be set to <code>CACHE_SIZE_UNLIMITED</code> to disable garbage collection. |
| [experimental](./firestore_.firestoresettings.md#firestoresettingsexperimental) | [ExperimentalOptions](./firestore_.experimentaloptions.md#experimentaloptions_interface) | Options that are "experimental", meaning that their semantics are subject to change at any time without notice, up to and including complete removal. |
| [experimentalAutoDetectLongPolling](./firestore_.firestoresettings.md#firestoresettingsexperimentalautodetectlongpolling) | boolean | Configures the SDK's underlying transport (WebChannel) to automatically detect if long-polling should be used. This is very similar to <code>experimentalForceLongPolling</code>, but only uses long-polling if required.<!-- -->After having had a default value of <code>false</code> since its inception in 2019, the default value of this setting was changed in May 2023 to <code>true</code> in v9.22.0 of the Firebase JavaScript SDK. That is, auto-detection of long polling is now enabled by default. To disable it, set this setting to <code>false</code>, and please open a GitHub issue to share the problems that motivated you disabling long-polling auto-detection.<!-- -->This setting cannot be used in a Node.js environment. |
| [experimentalForceLongPolling](./firestore_.firestoresettings.md#firestoresettingsexperimentalforcelongpolling) | boolean | Forces the SDK’s underlying network transport (WebChannel) to use long-polling. Each response from the backend will be closed immediately after the backend sends data (by default responses are kept open in case the backend has more data to send). This avoids incompatibility issues with certain proxies, antivirus software, etc. that incorrectly buffer traffic indefinitely. Use of this option will cause some performance degradation though.<!-- -->This setting cannot be used with <code>experimentalAutoDetectLongPolling</code> and may be removed in a future release. If you find yourself using it to work around a specific network reliability issue, please tell us about it in https://github.com/firebase/firebase-js-sdk/issues/1674.<!-- -->This setting cannot be used in a Node.js environment. |
| [experimentalLongPollingOptions](./firestore_.firestoresettings.md#firestoresettingsexperimentallongpollingoptions) | [ExperimentalLongPollingOptions](./firestore_.experimentallongpollingoptions.md#experimentallongpollingoptions_interface) | Options that configure the SDK’s underlying network transport (WebChannel) when long-polling is used.<!-- -->These options are only used if <code>experimentalForceLongPolling</code> is true or if <code>experimentalAutoDetectLongPolling</code> is true and the auto-detection determined that long-polling was needed. Otherwise, these options have no effect. |
Expand All @@ -45,6 +46,16 @@ The default value is 40 MB. The threshold must be set to at least 1 MB, and can
cacheSizeBytes?: number;
```

## FirestoreSettings.experimental

Options that are "experimental", meaning that their semantics are subject to change at any time without notice, up to and including complete removal.

<b>Signature:</b>

```typescript
experimental?: ExperimentalOptions;
```

## FirestoreSettings.experimentalAutoDetectLongPolling

Configures the SDK's underlying transport (WebChannel) to automatically detect if long-polling should be used. This is very similar to `experimentalForceLongPolling`<!-- -->, but only uses long-polling if required.
Expand Down
1 change: 1 addition & 0 deletions docs-devsite/firestore_.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ https://github.com/firebase/firebase-js-sdk
| [DocumentChange](./firestore_.documentchange.md#documentchange_interface) | A <code>DocumentChange</code> represents a change to the documents matching a query. It contains the document affected and the type of change that occurred. |
| [DocumentData](./firestore_.documentdata.md#documentdata_interface) | Document data (for use with [setDoc()](./firestore_lite.md#setdoc_ee215ad)<!-- -->) consists of fields mapped to values. |
| [ExperimentalLongPollingOptions](./firestore_.experimentallongpollingoptions.md#experimentallongpollingoptions_interface) | Options that configure the SDK’s underlying network transport (WebChannel) when long-polling is used.<!-- -->Note: This interface is "experimental" and is subject to change.<!-- -->See <code>FirestoreSettings.experimentalAutoDetectLongPolling</code>, <code>FirestoreSettings.experimentalForceLongPolling</code>, and <code>FirestoreSettings.experimentalLongPollingOptions</code>. |
| [ExperimentalOptions](./firestore_.experimentaloptions.md#experimentaloptions_interface) | Experimental options to configure the Firestore SDK.<!-- -->Note: This interface is "experimental" and is subject to change.<!-- -->See <code>FirestoreSettings.experimental</code>. |
| [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface) | Converter used by <code>withConverter()</code> to transform user objects of type <code>AppModelType</code> into Firestore data of type <code>DbModelType</code>.<!-- -->Using the converter allows you to specify generic type arguments when storing and retrieving objects from Firestore.<!-- -->In this context, an "AppModel" is a class that is used in an application to package together related information and functionality. Such a class could, for example, have properties with complex, nested data types, properties used for memoization, properties of types not supported by Firestore (such as <code>symbol</code> and <code>bigint</code>), and helper functions that perform compound operations. Such classes are not suitable and/or possible to store into a Firestore database. Instead, instances of such classes need to be converted to "plain old JavaScript objects" (POJOs) with exclusively primitive properties, potentially nested inside other POJOs or arrays of POJOs. In this context, this type is referred to as the "DbModel" and would be an object suitable for persisting into Firestore. For convenience, applications can implement <code>FirestoreDataConverter</code> and register the converter with Firestore objects, such as <code>DocumentReference</code> or <code>Query</code>, to automatically convert <code>AppModel</code> to <code>DbModel</code> when storing into Firestore, and convert <code>DbModel</code> to <code>AppModel</code> when retrieving from Firestore. |
| [FirestoreSettings](./firestore_.firestoresettings.md#firestoresettings_interface) | Specifies custom configurations for your Cloud Firestore instance. You must set these before invoking any other methods. |
| [Index](./firestore_.index.md#index_interface) | <b><i>(Public Preview)</i></b> The SDK definition of a Firestore index. |
Expand Down
1 change: 1 addition & 0 deletions packages/firestore/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export {
export { FirestoreSettings, PersistenceSettings } from './api/settings';
export type { PrivateSettings } from './lite-api/settings';
export { ExperimentalLongPollingOptions } from './api/long_polling_options';
export { ExperimentalOptions } from './api/experimental_options';

export {
DocumentChange,
Expand Down
76 changes: 76 additions & 0 deletions packages/firestore/src/api/experimental_options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* @license
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Experimental options to configure the Firestore SDK.
*
* Note: This interface is "experimental" and is subject to change.
*
* See `FirestoreSettings.experimental`.
*/
export interface ExperimentalOptions {
/**
* The maximum amount of time, in milliseconds, to wait before sending a
* Firestore "write" request to the backend. If `undefined` then do not delay
* at all.
*
* A delay can be useful because it enables the in-memory "write pipeline" to
* gather together multiple write requests and send them in a single HTTP
* request to the backend, rather than one HTTP request per write request, as
* is done when by default, or when this property is `undefined`. Note that
* there is a hardcoded limit to the number of write requests that are sent at
* once, so setting a very large value for this property will not necessarily
* cause _all_ write requests to be sent in a single HTTP request; however, it
* _could_ greatly reduce the number of distinct HTTP requests that are used.
*
* The value must be an integer value strictly greater than zero and less than
* or equal to 10000 (10 seconds). A value of `200` is a good starting point
* to minimize write latency yet still enable some amount of batching.
*
* See https://github.com/firebase/firebase-js-sdk/issues/5971 for rationale
* and background information that motivated this option.
*/
sendWriteRequestsDelayMs?: number;
}

/**
* Compares two `ExperimentalOptions` objects for equality.
*/
export function experimentalOptionsEqual(
options1: ExperimentalOptions,
options2: ExperimentalOptions
): boolean {
return (
options1.sendWriteRequestsDelayMs === options2.sendWriteRequestsDelayMs
);
}

/**
* Creates and returns a new `ExperimentalOptions` with the same
* option values as the given instance.
*/
export function cloneExperimentalOptions(
options: ExperimentalOptions
): ExperimentalOptions {
const clone: ExperimentalOptions = {};

if (options.sendWriteRequestsDelayMs !== undefined) {
clone.sendWriteRequestsDelayMs = options.sendWriteRequestsDelayMs;
}

return clone;
}
7 changes: 7 additions & 0 deletions packages/firestore/src/api/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { FirestoreSettings as LiteSettings } from '../lite-api/settings';

import { FirestoreLocalCache } from './cache_config';
import { ExperimentalOptions } from './experimental_options';
import { ExperimentalLongPollingOptions } from './long_polling_options';

export { DEFAULT_HOST } from '../lite-api/settings';
Expand Down Expand Up @@ -114,4 +115,10 @@ export interface FirestoreSettings extends LiteSettings {
* effect.
*/
experimentalLongPollingOptions?: ExperimentalLongPollingOptions;

/**
* Options that are "experimental", meaning that their semantics are subject
* to change at any time without notice, up to and including complete removal.
*/
experimental?: ExperimentalOptions;
}
3 changes: 2 additions & 1 deletion packages/firestore/src/core/component_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,8 @@ export class OnlineComponentProvider {
onlineState,
OnlineStateSource.RemoteStore
),
newConnectivityMonitor()
newConnectivityMonitor(),
cfg.databaseInfo.sendWriteRequestsDelayMs
);
}

Expand Down
5 changes: 4 additions & 1 deletion packages/firestore/src/core/database_info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export class DatabaseInfo {
* @param longPollingOptions Options that configure long-polling.
* @param useFetchStreams Whether to use the Fetch API instead of
* XMLHTTPRequest
* @param sendWriteRequestsDelayMs The delay, in milliseconds, to use before
* sending write requests over the wire in remote store.
*/
constructor(
readonly databaseId: DatabaseId,
Expand All @@ -48,7 +50,8 @@ export class DatabaseInfo {
readonly forceLongPolling: boolean,
readonly autoDetectLongPolling: boolean,
readonly longPollingOptions: ExperimentalLongPollingOptions,
readonly useFetchStreams: boolean
readonly useFetchStreams: boolean,
readonly sendWriteRequestsDelayMs: number | null
) {}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/firestore/src/lite-api/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export function makeDatabaseInfo(
settings.experimentalForceLongPolling,
settings.experimentalAutoDetectLongPolling,
cloneLongPollingOptions(settings.experimentalLongPollingOptions),
settings.useFetchStreams
settings.useFetchStreams,
settings.experimental.sendWriteRequestsDelayMs ?? null
);
}
40 changes: 39 additions & 1 deletion packages/firestore/src/lite-api/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import { EmulatorMockTokenOptions } from '@firebase/util';

import { FirestoreLocalCache } from '../api/cache_config';
import { CredentialsSettings } from '../api/credentials';
import {
ExperimentalOptions,
cloneExperimentalOptions,
experimentalOptionsEqual
} from '../api/experimental_options';
import {
ExperimentalLongPollingOptions,
cloneLongPollingOptions,
Expand Down Expand Up @@ -50,6 +55,10 @@ const MAX_LONG_POLLING_TIMEOUT_SECONDS = 30;
// Whether long-polling auto-detected is enabled by default.
const DEFAULT_AUTO_DETECT_LONG_POLLING = true;

// Set some maximum value for `sendWriteRequestsDelayMs` to avoid it being set
// to a value so large that it appears that write requests are never being sent.
const MAX_SEND_WRITE_REQUEST_DELAY_MS = 10000;

/**
* Specifies custom configurations for your Cloud Firestore instance.
* You must set these before invoking any other methods.
Expand Down Expand Up @@ -83,6 +92,7 @@ export interface PrivateSettings extends FirestoreSettings {
experimentalLongPollingOptions?: ExperimentalLongPollingOptions;
useFetchStreams?: boolean;
emulatorOptions?: { mockUserToken?: EmulatorMockTokenOptions | string };
experimental?: ExperimentalOptions;

localCache?: FirestoreLocalCache;
}
Expand Down Expand Up @@ -111,6 +121,7 @@ export class FirestoreSettingsImpl {

readonly useFetchStreams: boolean;
readonly localCache?: FirestoreLocalCache;
readonly experimental: ExperimentalOptions;

// Can be a google-auth-library or gapi client.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -178,6 +189,9 @@ export class FirestoreSettingsImpl {
validateLongPollingOptions(this.experimentalLongPollingOptions);

this.useFetchStreams = !!settings.useFetchStreams;

this.experimental = cloneExperimentalOptions(settings.experimental ?? {});
validateExperimentalOptions(this.experimental);
}

isEqual(other: FirestoreSettingsImpl): boolean {
Expand All @@ -195,7 +209,8 @@ export class FirestoreSettingsImpl {
other.experimentalLongPollingOptions
) &&
this.ignoreUndefinedProperties === other.ignoreUndefinedProperties &&
this.useFetchStreams === other.useFetchStreams
this.useFetchStreams === other.useFetchStreams &&
experimentalOptionsEqual(this.experimental, other.experimental)
);
}
}
Expand Down Expand Up @@ -227,3 +242,26 @@ function validateLongPollingOptions(
}
}
}

function validateExperimentalOptions(options: ExperimentalOptions): void {
if (options.sendWriteRequestsDelayMs !== undefined) {
if (isNaN(options.sendWriteRequestsDelayMs)) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
`invalid sendWriteRequestsDelayMs: ` +
`${options.sendWriteRequestsDelayMs} (must not be NaN)`
);
}
if (
options.sendWriteRequestsDelayMs <= 0 ||
options.sendWriteRequestsDelayMs > MAX_SEND_WRITE_REQUEST_DELAY_MS
) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
`invalid sendWriteRequestsDelayMs: ` +
`${options.sendWriteRequestsDelayMs} (must be greater than zero ` +
`and less than or equal to ${MAX_SEND_WRITE_REQUEST_DELAY_MS})`
);
}
}
}
Loading
Loading