Skip to content

Commit a2f0cf9

Browse files
author
Brian Chen
authored
Throw last failed attempt's error in failed transactions (#3287)
1 parent 69fad8d commit a2f0cf9

File tree

6 files changed

+60
-24
lines changed

6 files changed

+60
-24
lines changed

Firestore/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
- [fixed] Fixed an internal assertion that was triggered when an update
33
with a `FieldValue.serverTimestamp()` and an update with a
44
`FieldValue.increment()` were pending for the same document.
5+
- [changed] Failed transactions now return the failure from the last attempt,
6+
instead of `ABORTED.`
57

68
# 1.4.1
79
- [fixed] Fixed certificate loading for non-CocoaPods builds that may not

Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,10 +306,7 @@ - (void)testServerTimestampsFailViaTransactionUpdateOnNonexistentDocument {
306306
completion:^(id result, NSError *error) {
307307
XCTAssertNotNil(error);
308308
XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
309-
// TODO(b/35201829): This should be NotFound, but right now we retry transactions on any
310-
// error and so this turns into Aborted instead.
311-
// TODO(mikelehen): Actually it's FailedPrecondition, unlike Android. What do we want???
312-
XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition);
309+
XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound);
313310
[expectation fulfill];
314311
}];
315312
[self awaitExpectations];

Firestore/Example/Tests/Integration/FSTTransactionTests.mm

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ - (void)testGetDocuments {
4343
// We currently require every document read to also be written.
4444
// TODO(b/34879758): Fix this check once we drop that requirement.
4545
XCTAssertNotNil(error);
46+
XCTAssertEqual(error.code, FIRFirestoreErrorCodeInvalidArgument);
4647
[expectation fulfill];
4748
}];
4849
[self awaitExpectations];
@@ -115,9 +116,7 @@ - (void)testGetNonexistentDocumentThenFailPatch {
115116
XCTAssertNil(result);
116117
XCTAssertNotNil(error);
117118
XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
118-
// TODO(dimond): This is probably the wrong error code, but it's what we use today. We
119-
// should update the code once the underlying error was fixed.
120-
XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition);
119+
XCTAssertEqual(error.code, FIRFirestoreErrorCodeInvalidArgument);
121120
[expectation fulfill];
122121
}];
123122
[self awaitExpectations];
@@ -143,9 +142,7 @@ - (void)testDeleteDocumentAndPatch {
143142
XCTAssertNil(result);
144143
XCTAssertNotNil(error);
145144
XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
146-
// TODO(dimond): This is probably the wrong error code, but it's what we use today. We
147-
// should update the code once the underlying error was fixed.
148-
XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition);
145+
XCTAssertEqual(error.code, FIRFirestoreErrorCodeInvalidArgument);
149146
[expectation fulfill];
150147
}];
151148
[self awaitExpectations];
@@ -172,9 +169,8 @@ - (void)testDeleteDocumentAndSet {
172169
XCTAssertNil(result);
173170
XCTAssertNotNil(error);
174171
XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
175-
// TODO(dimond): This is probably the wrong error code, but it's what we use today. We
176-
// should update the code once the underlying error was fixed.
177-
XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition);
172+
// This is the error surfaced by the backend.
173+
XCTAssertEqual(error.code, FIRFirestoreErrorCodeInvalidArgument);
178174
[expectation fulfill];
179175
}];
180176
[self awaitExpectations];
@@ -237,6 +233,8 @@ - (void)testCannotUpdateNonExistentDocument {
237233
}
238234
completion:^(id _Nullable result, NSError *_Nullable error) {
239235
XCTAssertNotNil(error);
236+
// This is the error surfaced by the backend.
237+
XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound);
240238
[expectation fulfill];
241239
}];
242240
[self awaitExpectations];
@@ -373,6 +371,7 @@ - (void)testHandleReadingOneDocAndWritingAnother {
373371
// XCTAssertNil(error);
374372
// XCTAssertEqualObjects(@(16), snapshot[@"count"]);
375373
XCTAssertNotNil(error);
374+
XCTAssertEqual(error.code, FIRFirestoreErrorCodeInvalidArgument);
376375
[expectation fulfill];
377376
}];
378377
[self awaitExpectations];
@@ -410,13 +409,49 @@ - (void)testReadingADocTwiceWithDifferentVersions {
410409
}
411410
completion:^(id _Nullable result, NSError *_Nullable error) {
412411
[expectation fulfill];
412+
XCTAssertNotNil(error);
413+
XCTAssertEqual(error.code, FIRFirestoreErrorCodeAborted);
413414
}];
414415
[self awaitExpectations];
415416

416417
FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
417418
XCTAssertEqualObjects(@(1234.0), snapshot[@"count"]);
418419
}
419420

421+
- (void)testReadAndUpdateNonExistentDocumentWithExternalWrite {
422+
FIRFirestore *firestore = [self firestore];
423+
XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
424+
[firestore
425+
runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
426+
// Get and update a document that doesn't exist so that the transaction fails.
427+
FIRDocumentReference *doc =
428+
[[firestore collectionWithPath:@"nonexistent"] documentWithAutoID];
429+
[transaction getDocument:doc error:error];
430+
XCTAssertNil(*error);
431+
// Do a write outside of the transaction.
432+
dispatch_semaphore_t writeSemaphore = dispatch_semaphore_create(0);
433+
[doc setData:@{
434+
@"count" : @(1234)
435+
}
436+
completion:^(NSError *_Nullable error) {
437+
dispatch_semaphore_signal(writeSemaphore);
438+
}];
439+
// We can block on it, because transactions run on a background queue.
440+
dispatch_semaphore_wait(writeSemaphore, DISPATCH_TIME_FOREVER);
441+
// Now try to update the other doc from within the transaction.
442+
// This should fail, because the document didn't exist at the
443+
// start of the transaction.
444+
[transaction updateData:@{@"count" : @(16)} forDocument:doc];
445+
return nil;
446+
}
447+
completion:^(id _Nullable result, NSError *_Nullable error) {
448+
[expectation fulfill];
449+
XCTAssertNotNil(error);
450+
XCTAssertEqual(error.code, FIRFirestoreErrorCodeInvalidArgument);
451+
}];
452+
[self awaitExpectations];
453+
}
454+
420455
- (void)testCannotHaveAGetWithoutMutations {
421456
FIRFirestore *firestore = [self firestore];
422457
FIRDocumentReference *doc = [[firestore collectionWithPath:@"foo"] documentWithAutoID];
@@ -433,6 +468,7 @@ - (void)testCannotHaveAGetWithoutMutations {
433468
// We currently require every document read to also be written.
434469
// TODO(b/34879758): Fix this check once we drop that requirement.
435470
XCTAssertNotNil(error);
471+
XCTAssertEqual(error.code, FIRFirestoreErrorCodeInvalidArgument);
436472
[expectation fulfill];
437473
}];
438474
[self awaitExpectations];

Firestore/Source/Core/FSTSyncEngine.mm

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -315,13 +315,9 @@ - (void)transactionWithRetries:(int)retries
315315
resultCallback(std::move(maybe_result));
316316
return;
317317
}
318-
319318
// TODO(b/35201829): Only retry on real transaction failures.
320319
if (retries == 0) {
321-
Status wrappedError =
322-
Status(FirestoreErrorCode::FailedPrecondition, "Transaction failed all retries.")
323-
.CausedBy(std::move(status));
324-
resultCallback(std::move(wrappedError));
320+
resultCallback(std::move(status));
325321
return;
326322
}
327323
workerQueue->VerifyIsCurrentQueue();

Firestore/core/src/firebase/firestore/core/transaction.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ class Transaction {
128128
bool committed_ = false;
129129

130130
/**
131-
* An error that may have occurred as a consequence of a write. If set, needs
132-
* to be raised in the callback instead of trying to commit.
131+
* A deferred usage error that occurred previously in this transaction that
132+
* will cause the transaction to fail once it actually commits.
133133
*/
134134
util::Status last_write_error_;
135135

Firestore/core/src/firebase/firestore/core/transaction.mm

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,13 @@
7979
LookupCallback&& callback) {
8080
EnsureCommitNotCalled();
8181

82-
HARD_ASSERT(mutations_.empty(),
83-
"Transactions lookups are invalid after writes.");
82+
if (!mutations_.empty()) {
83+
Status lookup_error = Status{FirestoreErrorCode::InvalidArgument,
84+
"Firestore transactions require all reads to "
85+
"be executed before all writes"};
86+
callback({}, lookup_error);
87+
return;
88+
}
8489

8590
datastore_->LookupDocuments(
8691
keys, [this, callback](const std::vector<FSTMaybeDocument*>& documents,
@@ -124,7 +129,7 @@
124129

125130
if (version.has_value() && version.value() == SnapshotVersion::None()) {
126131
// The document to update doesn't exist, so fail the transaction.
127-
return Status{FirestoreErrorCode::Aborted,
132+
return Status{FirestoreErrorCode::InvalidArgument,
128133
"Can't update a document that doesn't exist."};
129134
} else if (version.has_value()) {
130135
// Document exists, just base precondition on document update time.
@@ -184,7 +189,7 @@
184189
// TODO(klimt): This is a temporary restriction, until "verify" is supported
185190
// on the backend.
186191
callback(
187-
Status{FirestoreErrorCode::FailedPrecondition,
192+
Status{FirestoreErrorCode::InvalidArgument,
188193
"Every document read in a transaction must also be written in "
189194
"that transaction."});
190195
} else {

0 commit comments

Comments
 (0)