Files
firebase-ios-sdk/Firestore/Source/Remote/FSTOnlineStateTracker.mm
T

165 lines
6.0 KiB
Plaintext

/*
* Copyright 2018 Google
*
* 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.
*/
#import "Firestore/Source/Remote/FSTOnlineStateTracker.h"
#import "Firestore/Source/Remote/FSTRemoteStore.h"
#import "Firestore/Source/Util/FSTDispatchQueue.h"
#include "Firestore/core/src/firebase/firestore/util/hard_assert.h"
#include "Firestore/core/src/firebase/firestore/util/log.h"
NS_ASSUME_NONNULL_BEGIN
// To deal with transient failures, we allow multiple stream attempts before giving up and
// transitioning from FSTOnlineState Unknown to Offline.
static const int kMaxWatchStreamFailures = 2;
// To deal with stream attempts that don't succeed or fail in a timely manner, we have a
// timeout for FSTOnlineState to reach Online or Offline. If the timeout is reached, we transition
// to Offline rather than waiting indefinitely.
static const NSTimeInterval kOnlineStateTimeout = 10;
@interface FSTOnlineStateTracker ()
/** The current FSTOnlineState. */
@property(nonatomic, assign) FSTOnlineState state;
/**
* A count of consecutive failures to open the stream. If it reaches the maximum defined by
* kMaxWatchStreamFailures, we'll revert to FSTOnlineStateOffline.
*/
@property(nonatomic, assign) int watchStreamFailures;
/**
* A timer that elapses after kOnlineStateTimeout, at which point we transition from FSTOnlineState
* Unknown to Offline without waiting for the stream to actually fail (kMaxWatchStreamFailures
* times).
*/
@property(nonatomic, strong, nullable) FSTDelayedCallback *onlineStateTimer;
/**
* Whether the client should log a warning message if it fails to connect to the backend
* (initially YES, cleared after a successful stream, or if we've logged the message already).
*/
@property(nonatomic, assign) BOOL shouldWarnClientIsOffline;
/** The FSTDispatchQueue to use for running timers (and to call onlineStateDelegate). */
@property(nonatomic, strong, readonly) FSTDispatchQueue *queue;
@end
@implementation FSTOnlineStateTracker
- (instancetype)initWithWorkerDispatchQueue:(FSTDispatchQueue *)queue {
if (self = [super init]) {
_queue = queue;
_state = FSTOnlineStateUnknown;
_shouldWarnClientIsOffline = YES;
}
return self;
}
- (void)handleWatchStreamStart {
if (self.watchStreamFailures == 0) {
[self setAndBroadcastState:FSTOnlineStateUnknown];
HARD_ASSERT(!self.onlineStateTimer, "onlineStateTimer shouldn't be started yet");
self.onlineStateTimer = [self.queue
dispatchAfterDelay:kOnlineStateTimeout
timerID:FSTTimerIDOnlineStateTimeout
block:^{
self.onlineStateTimer = nil;
HARD_ASSERT(
self.state == FSTOnlineStateUnknown,
"Timer should be canceled if we transitioned to a different state.");
[self logClientOfflineWarningIfNecessaryWithReason:
[NSString
stringWithFormat:@"Backend didn't respond within %f seconds.",
kOnlineStateTimeout]];
[self setAndBroadcastState:FSTOnlineStateOffline];
// NOTE: handleWatchStreamFailure will continue to increment
// watchStreamFailures even though we are already marked Offline but this is
// non-harmful.
}];
}
}
- (void)handleWatchStreamFailure:(NSError *)error {
if (self.state == FSTOnlineStateOnline) {
[self setAndBroadcastState:FSTOnlineStateUnknown];
// To get to FSTOnlineStateOnline, updateState: must have been called which would have reset
// our heuristics.
HARD_ASSERT(self.watchStreamFailures == 0, "watchStreamFailures must be 0");
HARD_ASSERT(!self.onlineStateTimer, "onlineStateTimer must be nil");
} else {
self.watchStreamFailures++;
if (self.watchStreamFailures >= kMaxWatchStreamFailures) {
[self clearOnlineStateTimer];
[self logClientOfflineWarningIfNecessaryWithReason:
[NSString stringWithFormat:@"Connection failed %d times. Most recent error: %@",
kMaxWatchStreamFailures, error]];
[self setAndBroadcastState:FSTOnlineStateOffline];
}
}
}
- (void)updateState:(FSTOnlineState)newState {
[self clearOnlineStateTimer];
self.watchStreamFailures = 0;
if (newState == FSTOnlineStateOnline) {
// We've connected to watch at least once. Don't warn the developer about being offline going
// forward.
self.shouldWarnClientIsOffline = NO;
}
[self setAndBroadcastState:newState];
}
- (void)setAndBroadcastState:(FSTOnlineState)newState {
if (newState != self.state) {
self.state = newState;
[self.onlineStateDelegate applyChangedOnlineState:newState];
}
}
- (void)logClientOfflineWarningIfNecessaryWithReason:(NSString *)reason {
NSString *message = [NSString
stringWithFormat:
@"Could not reach Cloud Firestore backend. %@\n This typically indicates that your "
@"device does not have a healthy Internet connection at the moment. The client will "
@"operate in offline mode until it is able to successfully connect to the backend.",
reason];
if (self.shouldWarnClientIsOffline) {
LOG_WARN("%s", message);
self.shouldWarnClientIsOffline = NO;
} else {
LOG_DEBUG("%s", message);
}
}
- (void)clearOnlineStateTimer {
if (self.onlineStateTimer) {
[self.onlineStateTimer cancel];
self.onlineStateTimer = nil;
}
}
@end
NS_ASSUME_NONNULL_END