Skip to content

Implement reachability and upload conditions #2826

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

Merged
merged 6 commits into from
Apr 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions GoogleDataTransport/GoogleDataTransport/Classes/GDTReachability.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2019 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 <Foundation/Foundation.h>

#import <SystemConfiguration/SCNetworkReachability.h>

NS_ASSUME_NONNULL_BEGIN

/** This class helps determine upload conditions by determining connectivity. */
@interface GDTReachability : NSObject

/** The current set flags indicating network conditions */
+ (SCNetworkReachabilityFlags)currentFlags;

@end

NS_ASSUME_NONNULL_END
104 changes: 104 additions & 0 deletions GoogleDataTransport/GoogleDataTransport/Classes/GDTReachability.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright 2019 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 "GDTReachability.h"
#import "GDTReachability_Private.h"

#import <netinet/in.h>

/** Sets the _callbackFlag ivar whenever the network changes.
*
* @param reachability The reachability object calling back.
* @param flags The new flag values.
* @param info Any data that might be passed in by the callback.
*/
static void GDTReachabilityCallback(SCNetworkReachabilityRef reachability,
SCNetworkReachabilityFlags flags,
void *info);

@implementation GDTReachability {
/** The reachability object. */
SCNetworkReachabilityRef _reachabilityRef;

/** The queue on which callbacks and all work will occur. */
dispatch_queue_t _reachabilityQueue;

/** Flags specified by reachability callbacks. */
SCNetworkConnectionFlags _callbackFlags;
}

+ (void)load {
[self sharedInstance];
}

+ (instancetype)sharedInstance {
static GDTReachability *sharedInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[GDTReachability alloc] init];
});
return sharedInstance;
}

+ (SCNetworkReachabilityFlags)currentFlags {
__block SCNetworkReachabilityFlags currentFlags;
dispatch_sync([GDTReachability sharedInstance] -> _reachabilityQueue, ^{
GDTReachability *reachability = [GDTReachability sharedInstance];
currentFlags = reachability->_flags ? reachability->_flags : reachability->_callbackFlags;
});
return currentFlags;
}

- (instancetype)init {
self = [super init];
if (self) {
struct sockaddr_in zeroAddress;
bzero(&zeroAddress, sizeof(zeroAddress));
zeroAddress.sin_len = sizeof(zeroAddress);
zeroAddress.sin_family = AF_INET;

_reachabilityQueue = dispatch_queue_create("com.google.GDTReachability", DISPATCH_QUEUE_SERIAL);
_reachabilityRef = SCNetworkReachabilityCreateWithAddress(
kCFAllocatorDefault, (const struct sockaddr *)&zeroAddress);
Boolean success = SCNetworkReachabilitySetDispatchQueue(_reachabilityRef, _reachabilityQueue);
NSAssert(success, @"The reachability queue wasn't set.");
success = SCNetworkReachabilitySetCallback(_reachabilityRef, GDTReachabilityCallback, NULL);
NSAssert(success, @"The reachability callback wasn't set.");

// Get the initial set of flags.
dispatch_async(_reachabilityQueue, ^{
Boolean valid = SCNetworkReachabilityGetFlags(self->_reachabilityRef, &self->_flags);
if (!valid) {
self->_flags = 0;
}
});
}
return self;
}

- (void)setCallbackFlags:(SCNetworkReachabilityFlags)flags {
if (_callbackFlags != flags) {
self->_callbackFlags = flags;
}
}

@end

static void GDTReachabilityCallback(SCNetworkReachabilityRef reachability,
SCNetworkReachabilityFlags flags,
void *info) {
[[GDTReachability sharedInstance] setCallbackFlags:flags];
}
150 changes: 91 additions & 59 deletions GoogleDataTransport/GoogleDataTransport/Classes/GDTUploadCoordinator.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#import "GDTAssert.h"
#import "GDTClock.h"
#import "GDTConsoleLogger.h"
#import "GDTReachability.h"
#import "GDTRegistrar_Private.h"
#import "GDTStorage.h"
#import "GDTUploadPackage_Private.h"
Expand Down Expand Up @@ -85,8 +86,11 @@ - (void)forceUploadForTarget:(GDTTarget)target {
if (self->_runningInBackground) {
[self->_forcedUploadQueue insertObject:forceUploadBlock atIndex:0];
[NSKeyedArchiver archiveRootObject:self toFile:[GDTUploadCoordinator archivePath]];

// Enqueue the force upload block if there's an in-flight upload for that target already.
// Enqueue the force upload block if conditions are bad or if there's an in-flight upload for
// that target already.
} else if (([self uploadConditions] & GDTUploadConditionNoNetwork) ==
GDTUploadConditionNoNetwork) {
[self->_forcedUploadQueue insertObject:forceUploadBlock atIndex:0];
} else if (self->_targetToInFlightEventSet[targetNumber]) {
[self->_forcedUploadQueue insertObject:forceUploadBlock atIndex:0];
} else {
Expand All @@ -109,39 +113,34 @@ - (GDTStorage *)storage {
// This should always be called in a thread-safe manner. When running the background, in theory,
// the uploader's background task should be calling this.
- (GDTUploaderCompletionBlock)onCompleteBlock {
__weak GDTUploadCoordinator *weakSelf = self;
static GDTUploaderCompletionBlock onCompleteBlock;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
onCompleteBlock = ^(GDTTarget target, GDTClock *nextUploadAttemptUTC, NSError *error) {
GDTUploadCoordinator *strongSelf = weakSelf;
if (strongSelf) {
dispatch_async(strongSelf.coordinationQueue, ^{
NSNumber *targetNumber = @(target);
if (error) {
GDTLogWarning(GDTMCWUploadFailed, @"Error during upload: %@", error);
[strongSelf->_targetToInFlightEventSet removeObjectForKey:targetNumber];
return;
}
strongSelf->_targetToNextUploadTimes[targetNumber] = nextUploadAttemptUTC;
NSSet<GDTStoredEvent *> *events =
[strongSelf->_targetToInFlightEventSet objectForKey:targetNumber];
GDTAssert(events, @"There should be an in-flight event set to remove.");
[strongSelf.storage removeEvents:events];
[strongSelf->_targetToInFlightEventSet removeObjectForKey:targetNumber];
if (strongSelf->_runningInBackground) {
[NSKeyedArchiver archiveRootObject:self toFile:[GDTUploadCoordinator archivePath]];
} else if (strongSelf->_forcedUploadQueue.count) {
GDTUploadCoordinatorForceUploadBlock queuedBlock =
[strongSelf->_forcedUploadQueue lastObject];
if (queuedBlock) {
dispatch_async(strongSelf->_coordinationQueue, ^{
queuedBlock();
});
}
dispatch_async(self->_coordinationQueue, ^{
NSNumber *targetNumber = @(target);
if (error) {
GDTLogWarning(GDTMCWUploadFailed, @"Error during upload: %@", error);
[self->_targetToInFlightEventSet removeObjectForKey:targetNumber];
return;
}
self->_targetToNextUploadTimes[targetNumber] = nextUploadAttemptUTC;
NSSet<GDTStoredEvent *> *events =
[self->_targetToInFlightEventSet objectForKey:targetNumber];
GDTAssert(events, @"There should be an in-flight event set to remove.");
[self->_storage removeEvents:events];
[self->_targetToInFlightEventSet removeObjectForKey:targetNumber];
if (self->_runningInBackground) {
[NSKeyedArchiver archiveRootObject:self toFile:[GDTUploadCoordinator archivePath]];
} else if (self->_forcedUploadQueue.count) {
GDTUploadCoordinatorForceUploadBlock queuedBlock = [self->_forcedUploadQueue lastObject];
if (queuedBlock) {
dispatch_async(self->_coordinationQueue, ^{
queuedBlock();
});
}
});
}
}
});
};
});
return onCompleteBlock;
Expand All @@ -153,20 +152,17 @@ - (GDTUploaderCompletionBlock)onCompleteBlock {
* check the next-upload clocks of all targets to determine if an upload attempt can be made.
*/
- (void)startTimer {
__weak GDTUploadCoordinator *weakSelf = self;
dispatch_sync(_coordinationQueue, ^{
GDTUploadCoordinator *strongSelf = weakSelf;
GDTAssert(strongSelf, @"self must be real to start a timer.");
strongSelf->_timer =
dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, strongSelf->_coordinationQueue);
dispatch_source_set_timer(strongSelf->_timer, DISPATCH_TIME_NOW, strongSelf->_timerInterval,
strongSelf->_timerLeeway);
dispatch_source_set_event_handler(strongSelf->_timer, ^{
if (!strongSelf->_runningInBackground) {
[strongSelf checkPrioritizersAndUploadEvents];
self->_timer =
dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self->_coordinationQueue);
dispatch_source_set_timer(self->_timer, DISPATCH_TIME_NOW, self->_timerInterval,
self->_timerLeeway);
dispatch_source_set_event_handler(self->_timer, ^{
if (!self->_runningInBackground) {
[self checkPrioritizersAndUploadEvents];
}
});
dispatch_resume(strongSelf->_timer);
dispatch_resume(self->_timer);
});
}

Expand All @@ -181,36 +177,72 @@ - (void)stopTimer {
* events for that target or not. If so, queries the prioritizers
*/
- (void)checkPrioritizersAndUploadEvents {
__weak GDTUploadCoordinator *weakSelf = self;
dispatch_async(_coordinationQueue, ^{
if (self->_runningInBackground) {
return;
}

GDTUploadConditions conds = [self uploadConditions];
if ((conds & GDTUploadConditionNoNetwork) == GDTUploadConditionNoNetwork) {
return;
}

static int count = 0;
count++;
GDTUploadCoordinator *strongSelf = weakSelf;
if (strongSelf) {
NSArray<NSNumber *> *targetsReadyForUpload = [self targetsReadyForUpload];
for (NSNumber *target in targetsReadyForUpload) {
id<GDTPrioritizer> prioritizer = strongSelf->_registrar.targetToPrioritizer[target];
id<GDTUploader> uploader = strongSelf->_registrar.targetToUploader[target];
GDTAssert(prioritizer && uploader, @"Target '%@' is missing an implementation", target);
GDTUploadConditions conds = [self uploadConditions];
GDTUploadPackage *package = [[prioritizer uploadPackageWithConditions:conds] copy];
package.storage = strongSelf.storage;
if (package.events && package.events.count > 0) {
strongSelf->_targetToInFlightEventSet[target] = package.events;
[uploader uploadPackage:package onComplete:self.onCompleteBlock];
}
NSArray<NSNumber *> *targetsReadyForUpload = [self targetsReadyForUpload];
for (NSNumber *target in targetsReadyForUpload) {
id<GDTPrioritizer> prioritizer = self->_registrar.targetToPrioritizer[target];
id<GDTUploader> uploader = self->_registrar.targetToUploader[target];
GDTAssert(prioritizer && uploader, @"Target '%@' is missing an implementation", target);
GDTUploadPackage *package = [[prioritizer uploadPackageWithConditions:conds] copy];
package.storage = self.storage;
if (package.events && package.events.count > 0) {
self->_targetToInFlightEventSet[target] = package.events;
[uploader uploadPackage:package onComplete:self.onCompleteBlock];
}
}
});
}

/** */
/** Returns the current upload conditions after making determinations about the network connection.
*
* @return The current upload conditions.
*/
- (GDTUploadConditions)uploadConditions {
// TODO: Compute the real upload conditions.
return GDTUploadConditionMobileData;
SCNetworkReachabilityFlags currentFlags = [GDTReachability currentFlags];

BOOL reachable =
(currentFlags & kSCNetworkReachabilityFlagsReachable) == kSCNetworkReachabilityFlagsReachable;
BOOL connectionRequired = (currentFlags & kSCNetworkReachabilityFlagsConnectionRequired) ==
kSCNetworkReachabilityFlagsConnectionRequired;
BOOL interventionRequired = (currentFlags & kSCNetworkReachabilityFlagsInterventionRequired) ==
kSCNetworkReachabilityFlagsInterventionRequired;
BOOL connectionOnDemand = (currentFlags & kSCNetworkReachabilityFlagsConnectionOnDemand) ==
kSCNetworkReachabilityFlagsConnectionOnDemand;
BOOL connectionOnTraffic = (currentFlags & kSCNetworkReachabilityFlagsConnectionOnTraffic) ==
kSCNetworkReachabilityFlagsConnectionOnTraffic;
BOOL isWWAN =
(currentFlags & kSCNetworkReachabilityFlagsIsWWAN) == kSCNetworkReachabilityFlagsIsWWAN;

if (!reachable) {
return GDTUploadConditionNoNetwork;
}

GDTUploadConditions conditions = 0;
conditions |= !connectionRequired ? GDTUploadConditionWifiData : conditions;
conditions |= isWWAN ? GDTUploadConditionMobileData : conditions;
if ((connectionOnTraffic || connectionOnDemand) && !interventionRequired) {
conditions = GDTUploadConditionWifiData;
}

BOOL wifi = (conditions & GDTUploadConditionWifiData) == GDTUploadConditionWifiData;
BOOL cell = (conditions & GDTUploadConditionMobileData) == GDTUploadConditionMobileData;

if (!(wifi || cell)) {
conditions = GDTUploadConditionUnclearConnection;
}

return conditions;
}

/** Checks the next upload time for each target and returns an array of targets that are
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2019 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 "GDTReachability.h"

@interface GDTReachability ()

/** Allows manually setting the flags for testing purposes. */
@property(nonatomic, readwrite) SCNetworkReachabilityFlags flags;

/** Creates/returns the singleton instance of this class.
*
* @return The singleton instance of this class.
*/
+ (instancetype)sharedInstance;

@end
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,20 @@ NS_ASSUME_NONNULL_BEGIN
*/
typedef NS_OPTIONS(NSInteger, GDTUploadConditions) {

/** An upload shouldn't be attempted, because there's no network. */
GDTUploadConditionNoNetwork = 1 << 0,

/** An upload would likely use mobile data. */
GDTUploadConditionMobileData,
GDTUploadConditionMobileData = 1 << 1,

/** An upload would likely use wifi data. */
GDTUploadConditionWifiData,
GDTUploadConditionWifiData = 1 << 2,

/** An upload uses some sort of network connection, but it's unclear which. */
GDTUploadConditionUnclearConnection = 1 << 3,

/** A high priority event has occurred. */
GDTUploadConditionHighPriority,
GDTUploadConditionHighPriority = 1 << 4,
};

/** This protocol defines the common interface of event prioritization. Prioritizers are
Expand Down
Loading