Skip to content

Commit 04fd3ac

Browse files
committed
Treat ABORTED writes as retryable
This is a port of firebase/firebase-js-sdk#1456.
1 parent d764afb commit 04fd3ac

File tree

4 files changed

+66
-23
lines changed

4 files changed

+66
-23
lines changed

Firestore/Example/Tests/Remote/FSTDatastoreTests.mm

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ @interface FSTDatastoreTests : XCTestCase
2424

2525
@implementation FSTDatastoreTests
2626

27-
- (void)testIsPermanentWriteError {
27+
- (NSError *)errorForCode:(FIRFirestoreErrorCode)code {
28+
return [NSError errorWithDomain:FIRFirestoreErrorDomain code:code userInfo:nil];
29+
}
30+
31+
- (void)testIsPermanentError {
2832
// From GRPCCall -cancel
2933
NSError *error = [NSError errorWithDomain:FIRFirestoreErrorDomain
3034
code:FIRFirestoreErrorCodeCancelled
3135
userInfo:@{NSLocalizedDescriptionKey : @"Canceled by app"}];
32-
XCTAssertFalse([FSTDatastore isPermanentWriteError:error]);
36+
XCTAssertFalse([FSTDatastore isPermanentError:error]);
3337

3438
// From GRPCCall -startNextRead
3539
error = [NSError errorWithDomain:FIRFirestoreErrorDomain
@@ -38,24 +42,37 @@ - (void)testIsPermanentWriteError {
3842
NSLocalizedDescriptionKey :
3943
@"Client does not have enough memory to hold the server response."
4044
}];
41-
XCTAssertFalse([FSTDatastore isPermanentWriteError:error]);
45+
XCTAssertFalse([FSTDatastore isPermanentError:error]);
4246

4347
// From GRPCCall -startWithWriteable
4448
error = [NSError errorWithDomain:FIRFirestoreErrorDomain
4549
code:FIRFirestoreErrorCodeUnavailable
4650
userInfo:@{NSLocalizedDescriptionKey : @"Connectivity lost."}];
47-
XCTAssertFalse([FSTDatastore isPermanentWriteError:error]);
51+
XCTAssertFalse([FSTDatastore isPermanentError:error]);
4852

4953
// User info doesn't matter:
50-
error = [NSError errorWithDomain:FIRFirestoreErrorDomain
51-
code:FIRFirestoreErrorCodeUnavailable
52-
userInfo:nil];
53-
XCTAssertFalse([FSTDatastore isPermanentWriteError:error]);
54+
error = [self errorForCode:FIRFirestoreErrorCodeUnavailable];
55+
XCTAssertFalse([FSTDatastore isPermanentError:error]);
5456

5557
// "unauthenticated" is considered a recoverable error due to expired token.
56-
error = [NSError errorWithDomain:FIRFirestoreErrorDomain
57-
code:FIRFirestoreErrorCodeUnauthenticated
58-
userInfo:nil];
58+
error = [self errorForCode:FIRFirestoreErrorCodeUnauthenticated];
59+
XCTAssertFalse([FSTDatastore isPermanentError:error]);
60+
61+
error = [self errorForCode:FIRFirestoreErrorCodeDataLoss];
62+
XCTAssertTrue([FSTDatastore isPermanentError:error]);
63+
64+
error = [self errorForCode:FIRFirestoreErrorCodeAborted];
65+
XCTAssertTrue([FSTDatastore isPermanentError:error]);
66+
}
67+
68+
- (void)testIsPermanentWriteError {
69+
NSError *error = [self errorForCode:FIRFirestoreErrorCodeUnauthenticated];
70+
XCTAssertFalse([FSTDatastore isPermanentWriteError:error]);
71+
72+
error = [self errorForCode:FIRFirestoreErrorCodeDataLoss];
73+
XCTAssertTrue([FSTDatastore isPermanentWriteError:error]);
74+
75+
error = [self errorForCode:FIRFirestoreErrorCodeAborted];
5976
XCTAssertFalse([FSTDatastore isPermanentWriteError:error]);
6077
}
6178

Firestore/Source/Remote/FSTDatastore.h

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,25 @@ NS_ASSUME_NONNULL_BEGIN
7676
/** Returns YES if the given error is a GRPC ABORTED error. **/
7777
+ (BOOL)isAbortedError:(NSError *)error;
7878

79-
/** Returns YES if the given error indicates the RPC associated with it may not be retried. */
79+
/**
80+
* Determines whether an error code represents a permanent error when received in response to a
81+
* non-write operation.
82+
*
83+
* See +isPermanentWriteError for classifying write errors.
84+
*/
85+
+ (BOOL)isPermanentError:(NSError *)error;
86+
87+
/**
88+
* Determines whether an error code represents a permanent error when received in response to a
89+
* write operation.
90+
*
91+
* Write operations must be handled specially because as of b/119437764, ABORTED errors on the write
92+
* stream should be retried too (even though ABORTED errors are not generally retryable).
93+
*
94+
* Note that during the initial handshake on the write stream an ABORTED error signals that we
95+
* should discard our stream token (i.e. it is permanent). This means a handshake error should be
96+
* classified with isPermanentError, above.
97+
*/
8098
+ (BOOL)isPermanentWriteError:(NSError *)error;
8199

82100
/** Looks up a list of documents in datastore. */

Firestore/Source/Remote/FSTDatastore.mm

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -144,18 +144,18 @@ + (BOOL)isAbortedError:(NSError *)error {
144144
return error.code == FIRFirestoreErrorCodeAborted;
145145
}
146146

147-
+ (BOOL)isPermanentWriteError:(NSError *)error {
147+
+ (BOOL)isPermanentError:(NSError *)error {
148148
HARD_ASSERT([error.domain isEqualToString:FIRFirestoreErrorDomain],
149-
"isPerminanteWriteError: only works with errors emitted by FSTDatastore.");
149+
"isPermanentError: only works with errors emitted by FSTDatastore.");
150150
switch (error.code) {
151151
case FIRFirestoreErrorCodeCancelled:
152152
case FIRFirestoreErrorCodeUnknown:
153153
case FIRFirestoreErrorCodeDeadlineExceeded:
154154
case FIRFirestoreErrorCodeResourceExhausted:
155155
case FIRFirestoreErrorCodeInternal:
156156
case FIRFirestoreErrorCodeUnavailable:
157-
// Unauthenticated means something went wrong with our token and we need to retry with new
158-
// credentials which will happen automatically.
157+
// Unauthenticated means something went wrong with our token and we need to retry with new
158+
// credentials which will happen automatically.
159159
case FIRFirestoreErrorCodeUnauthenticated:
160160
return NO;
161161
case FIRFirestoreErrorCodeInvalidArgument:
@@ -164,9 +164,9 @@ + (BOOL)isPermanentWriteError:(NSError *)error {
164164
case FIRFirestoreErrorCodePermissionDenied:
165165
case FIRFirestoreErrorCodeFailedPrecondition:
166166
case FIRFirestoreErrorCodeAborted:
167-
// Aborted might be retried in some scenarios, but that is dependant on
168-
// the context and should handled individually by the calling code.
169-
// See https://cloud.google.com/apis/design/errors
167+
// Aborted might be retried in some scenarios, but that is dependant on
168+
// the context and should handled individually by the calling code.
169+
// See https://cloud.google.com/apis/design/errors
170170
case FIRFirestoreErrorCodeOutOfRange:
171171
case FIRFirestoreErrorCodeUnimplemented:
172172
case FIRFirestoreErrorCodeDataLoss:
@@ -176,6 +176,10 @@ + (BOOL)isPermanentWriteError:(NSError *)error {
176176
}
177177
}
178178

179+
+ (BOOL)isPermanentWriteError:(NSError *)error {
180+
return [self isPermanentError:error] && error.code != FIRFirestoreErrorCodeAborted;
181+
}
182+
179183
- (void)commitMutations:(NSArray<FSTMutation *> *)mutations
180184
completion:(FSTVoidErrorBlock)completion {
181185
_datastore->CommitMutations(mutations, completion);

Firestore/Source/Remote/FSTRemoteStore.mm

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -573,20 +573,24 @@ - (void)writeStreamWasInterruptedWithError:(nullable NSError *)error {
573573

574574
- (void)handleHandshakeError:(NSError *)error {
575575
HARD_ASSERT(error, "Handling write error with status OK.");
576-
// Reset the token if it's a permanent error or the error code is ABORTED, signaling the write
577-
// stream is no longer valid.
578-
if ([FSTDatastore isPermanentWriteError:error] || [FSTDatastore isAbortedError:error]) {
576+
// Reset the token if it's a permanent error, signaling the write stream is
577+
// no longer valid. Note that the handshake does not count as a write: see
578+
// comments on isPermanentWriteError for details.
579+
if ([FSTDatastore isPermanentError:error]) {
579580
NSString *token = [_writeStream->GetLastStreamToken() base64EncodedStringWithOptions:0];
580581
LOG_DEBUG("FSTRemoteStore %s error before completed handshake; resetting stream token %s: %s",
581582
(__bridge void *)self, token, error);
582583
_writeStream->SetLastStreamToken(nil);
583584
[self.localStore setLastStreamToken:nil];
585+
} else {
586+
// Some other error, don't reset stream token. Our stream logic will just retry with exponential
587+
// backoff.
584588
}
585589
}
586590

587591
- (void)handleWriteError:(NSError *)error {
588592
HARD_ASSERT(error, "Handling write error with status OK.");
589-
// Only handle permanent error. If it's transient, just let the retry logic kick in.
593+
// Only handle permanent errors here. If it's transient, just let the retry logic kick in.
590594
if (![FSTDatastore isPermanentWriteError:error]) {
591595
return;
592596
}

0 commit comments

Comments
 (0)