Skip to content

Commit 2c726a1

Browse files
feat: Lazy-started transactions (#2017)
* Preliminary implementation of lazy transactions * Avoid creating new function contexts where possible * Fixes an update tests * Remove transaction options type from normal _get * Revert rollback optimisation * Do not start transaction when readTime specified * Completely revert conditional rollback * Rollback is completed asynchronously * Cleanup * Fixes Make resilient to wether transaction is included in same or different response Test transaction ID buffer length * Revert comment * Fix aggregate query stream Co-authored-by: Tom Andersen <[email protected]> * Apply suggestion for DocumentReader parameters * Fix conformance tests * Revert readTime null assertion behaviour * Fix query snapshot readTime logic * Update dev/src/reference.ts I am simply going to apply this change... * Remove un-needed null assertion Co-authored-by: Tom Andersen <[email protected]> * Apply suggested tweaks --------- Co-authored-by: Tom Andersen <[email protected]>
1 parent 5811492 commit 2c726a1

12 files changed

+918
-625
lines changed

dev/conformance/runner.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,11 @@ function queryHandler(spec: ConformanceProto) {
285285
const expectedQuery = STRUCTURED_QUERY_TYPE.fromObject(spec.query);
286286
expect(actualQuery).to.deep.equal(expectedQuery);
287287
const stream = through2.obj();
288-
setImmediate(() => stream.push(null));
288+
setImmediate(() => {
289+
// Empty query always emits a readTime
290+
stream.push({readTime: {seconds: 0, nanos: 0}});
291+
stream.push(null);
292+
});
289293
return stream;
290294
};
291295
}

dev/src/document-reader.ts

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ import {Timestamp} from './timestamp';
2525
import {DocumentData} from '@google-cloud/firestore';
2626
import api = google.firestore.v1;
2727

28+
interface BatchGetResponse<AppModelType, DbModelType extends DocumentData> {
29+
result: Array<DocumentSnapshot<AppModelType, DbModelType>>;
30+
/**
31+
* The transaction that was started as part of this request. Will only be if
32+
* `DocumentReader.transactionIdOrNewTransaction` was `api.ITransactionOptions`.
33+
*/
34+
transaction?: Uint8Array;
35+
}
36+
2837
/**
2938
* A wrapper around BatchGetDocumentsRequest that retries request upon stream
3039
* failure and returns ordered results.
@@ -33,40 +42,58 @@ import api = google.firestore.v1;
3342
* @internal
3443
*/
3544
export class DocumentReader<AppModelType, DbModelType extends DocumentData> {
36-
/** An optional field mask to apply to this read. */
37-
fieldMask?: FieldPath[];
38-
/** An optional transaction ID to use for this read. */
39-
transactionId?: Uint8Array;
40-
/** An optional readTime to use for this read. */
41-
readTime?: Timestamp;
42-
43-
private outstandingDocuments = new Set<string>();
44-
private retrievedDocuments = new Map<string, DocumentSnapshot>();
45+
private readonly outstandingDocuments = new Set<string>();
46+
private readonly retrievedDocuments = new Map<string, DocumentSnapshot>();
47+
private retrievedTransactionId?: Uint8Array;
4548

4649
/**
4750
* Creates a new DocumentReader that fetches the provided documents (via
4851
* `get()`).
4952
*
5053
* @param firestore The Firestore instance to use.
5154
* @param allDocuments The documents to get.
55+
* @param fieldMask An optional field mask to apply to this read
56+
* @param transactionOrReadTime An optional transaction ID to use for this
57+
* read or options for beginning a new transaction with this read
5258
*/
5359
constructor(
54-
private firestore: Firestore,
55-
private allDocuments: Array<DocumentReference<AppModelType, DbModelType>>
60+
private readonly firestore: Firestore,
61+
private readonly allDocuments: ReadonlyArray<
62+
DocumentReference<AppModelType, DbModelType>
63+
>,
64+
private readonly fieldMask?: FieldPath[],
65+
private readonly transactionOrReadTime?:
66+
| Uint8Array
67+
| api.ITransactionOptions
68+
| Timestamp
5669
) {
5770
for (const docRef of this.allDocuments) {
5871
this.outstandingDocuments.add(docRef.formattedName);
5972
}
6073
}
6174

6275
/**
63-
* Invokes the BatchGetDocuments RPC and returns the results.
76+
* Invokes the BatchGetDocuments RPC and returns the results as an array of
77+
* documents.
6478
*
6579
* @param requestTag A unique client-assigned identifier for this request.
6680
*/
6781
async get(
6882
requestTag: string
6983
): Promise<Array<DocumentSnapshot<AppModelType, DbModelType>>> {
84+
const {result} = await this._get(requestTag);
85+
return result;
86+
}
87+
88+
/**
89+
* Invokes the BatchGetDocuments RPC and returns the results with transaction
90+
* metadata.
91+
*
92+
* @param requestTag A unique client-assigned identifier for this request.
93+
*/
94+
async _get(
95+
requestTag: string
96+
): Promise<BatchGetResponse<AppModelType, DbModelType>> {
7097
await this.fetchDocuments(requestTag);
7198

7299
// BatchGetDocuments doesn't preserve document order. We use the request
@@ -92,7 +119,10 @@ export class DocumentReader<AppModelType, DbModelType extends DocumentData> {
92119
}
93120
}
94121

95-
return orderedDocuments;
122+
return {
123+
result: orderedDocuments,
124+
transaction: this.retrievedTransactionId,
125+
};
96126
}
97127

98128
private async fetchDocuments(requestTag: string): Promise<void> {
@@ -104,10 +134,12 @@ export class DocumentReader<AppModelType, DbModelType extends DocumentData> {
104134
database: this.firestore.formattedName,
105135
documents: Array.from(this.outstandingDocuments),
106136
};
107-
if (this.transactionId) {
108-
request.transaction = this.transactionId;
109-
} else if (this.readTime) {
110-
request.readTime = this.readTime.toProto().timestampValue;
137+
if (this.transactionOrReadTime instanceof Uint8Array) {
138+
request.transaction = this.transactionOrReadTime;
139+
} else if (this.transactionOrReadTime instanceof Timestamp) {
140+
request.readTime = this.transactionOrReadTime.toProto().timestampValue;
141+
} else if (this.transactionOrReadTime) {
142+
request.newTransaction = this.transactionOrReadTime;
111143
}
112144

113145
if (this.fieldMask) {
@@ -129,8 +161,12 @@ export class DocumentReader<AppModelType, DbModelType extends DocumentData> {
129161
stream.resume();
130162

131163
for await (const response of stream) {
132-
let snapshot: DocumentSnapshot<DocumentData>;
164+
// Proto comes with zero-length buffer by default
165+
if (response.transaction?.length) {
166+
this.retrievedTransactionId = response.transaction;
167+
}
133168

169+
let snapshot: DocumentSnapshot<DocumentData> | undefined;
134170
if (response.found) {
135171
logger(
136172
'DocumentReader.fetchDocuments',
@@ -142,28 +178,31 @@ export class DocumentReader<AppModelType, DbModelType extends DocumentData> {
142178
response.found,
143179
response.readTime!
144180
);
145-
} else {
181+
} else if (response.missing) {
146182
logger(
147183
'DocumentReader.fetchDocuments',
148184
requestTag,
149185
'Document missing: %s',
150-
response.missing!
186+
response.missing
151187
);
152188
snapshot = this.firestore.snapshot_(
153-
response.missing!,
189+
response.missing,
154190
response.readTime!
155191
);
156192
}
157193

158-
const path = snapshot.ref.formattedName;
159-
this.outstandingDocuments.delete(path);
160-
this.retrievedDocuments.set(path, snapshot);
161-
++resultCount;
194+
if (snapshot) {
195+
const path = snapshot.ref.formattedName;
196+
this.outstandingDocuments.delete(path);
197+
this.retrievedDocuments.set(path, snapshot);
198+
++resultCount;
199+
}
162200
}
163201
} catch (error) {
164202
const shouldRetry =
165203
// Transactional reads are retried via the transaction runner.
166-
!this.transactionId &&
204+
!request.transaction &&
205+
!request.newTransaction &&
167206
// Only retry if we made progress.
168207
resultCount > 0 &&
169208
// Don't retry permanent errors.

dev/src/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1295,8 +1295,7 @@ export class Firestore implements firestore.Firestore {
12951295

12961296
return this.initializeIfNeeded(tag)
12971297
.then(() => {
1298-
const reader = new DocumentReader(this, documents);
1299-
reader.fieldMask = fieldMask || undefined;
1298+
const reader = new DocumentReader(this, documents, fieldMask);
13001299
return reader.get(tag);
13011300
})
13021301
.catch(err => {

0 commit comments

Comments
 (0)