Refactor connectivity monitor

pull/14529/head
Muxi Yan 7 years ago
parent 0cfd3cd554
commit 822d175ac2
  1. 22
      src/objective-c/GRPCClient/GRPCCall.m
  2. 58
      src/objective-c/GRPCClient/private/GRPCConnectivityMonitor.h
  3. 199
      src/objective-c/GRPCClient/private/GRPCConnectivityMonitor.m
  4. 25
      src/objective-c/GRPCClient/private/GRPCHost.m

@ -209,6 +209,8 @@ static NSString * const kBearerPrefix = @"Bearer ";
} else { } else {
[_responseWriteable enqueueSuccessfulCompletion]; [_responseWriteable enqueueSuccessfulCompletion];
} }
[GRPCConnectivityMonitor unregisterObserver:self];
} }
- (void)cancelCall { - (void)cancelCall {
@ -473,16 +475,8 @@ static NSString * const kBearerPrefix = @"Bearer ";
// TODO(jcanizales): Check this on init. // TODO(jcanizales): Check this on init.
[NSException raise:NSInvalidArgumentException format:@"host of %@ is nil", _host]; [NSException raise:NSInvalidArgumentException format:@"host of %@ is nil", _host];
} }
_connectivityMonitor = [GRPCConnectivityMonitor monitorWithHost:host]; [GRPCConnectivityMonitor registerObserver:self
__weak typeof(self) weakSelf = self; selector:@selector(connectivityChanged:)];
void (^handler)(void) = ^{
typeof(self) strongSelf = weakSelf;
[strongSelf finishWithError:[NSError errorWithDomain:kGRPCErrorDomain
code:GRPCErrorCodeUnavailable
userInfo:@{ NSLocalizedDescriptionKey : @"Connectivity lost." }]];
};
[_connectivityMonitor handleLossWithHandler:handler
wifiStatusChangeHandler:nil];
} }
- (void)startWithWriteable:(id<GRXWriteable>)writeable { - (void)startWithWriteable:(id<GRXWriteable>)writeable {
@ -546,4 +540,12 @@ static NSString * const kBearerPrefix = @"Bearer ";
} }
} }
- (void)connectivityChanged:(NSNotification *)note {
[self maybeFinishWithError:[NSError errorWithDomain:kGRPCErrorDomain
code:GRPCErrorCodeUnavailable
userInfo:@{ NSLocalizedDescriptionKey : @"Connectivity lost." }]];
// Cancel underlying call upon this notification
[self cancelCall];
}
@end @end

@ -19,44 +19,30 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import <SystemConfiguration/SystemConfiguration.h> #import <SystemConfiguration/SystemConfiguration.h>
@interface GRPCReachabilityFlags : NSObject typedef NS_ENUM(NSInteger, GRPCConnectivityStatus) {
GRPCConnectivityUnknown = 0,
+ (nonnull instancetype)flagsWithFlags:(SCNetworkReachabilityFlags)flags; GRPCConnectivityNoNetwork = 1,
GRPCConnectivityCellular = 2,
/** GRPCConnectivityWiFi = 3,
* One accessor method to query each of the different flags. Example: };
@property(nonatomic, readonly) BOOL isCell; extern NSString * _Nonnull kGRPCConnectivityNotification;
*/ // This interface monitors OS reachability interface for any network status
#define GRPC_XMACRO_ITEM(methodName, FlagName) \ // change. Parties interested in these events should register themselves as
@property(nonatomic, readonly) BOOL methodName; // observer.
#include "GRPCReachabilityFlagNames.xmacro.h"
#undef GRPC_XMACRO_ITEM
@property(nonatomic, readonly) BOOL isHostReachable;
@end
@interface GRPCConnectivityMonitor : NSObject @interface GRPCConnectivityMonitor : NSObject
+ (nullable instancetype)monitorWithHost:(nonnull NSString *)hostName;
- (nonnull instancetype)init NS_UNAVAILABLE; - (nonnull instancetype)init NS_UNAVAILABLE;
/** // Register an object as observer of network status change. \a observer
* Queue on which callbacks will be dispatched. Default is the main queue. Set it before calling // must have a notification method with one parameter of type
* handleLossWithHandler:. // (NSNotification *) and should pass it to parameter \a selector. The
*/ // parameter of this notification method is not used for now.
// TODO(jcanizales): Default to a serial background queue instead. + (void)registerObserver:(_Nonnull id)observer
@property(nonatomic, strong, null_resettable) dispatch_queue_t queue; selector:(_Nonnull SEL)selector;
/** // Ungegister an object from observers of network status change.
* Calls handler every time the connectivity to this instance's host is lost. If this instance is + (void)unregisterObserver:(_Nonnull id)observer;
* released before that happens, the handler won't be called.
* Only one handler is active at a time, so if this method is called again before the previous
* handler has been called, it might never be called at all (or yes, if it has already been queued).
*/
- (void)handleLossWithHandler:(nullable void (^)(void))lossHandler
wifiStatusChangeHandler:(nullable void (^)(void))wifiStatusChangeHandler;
@end @end

@ -18,175 +18,74 @@
#import "GRPCConnectivityMonitor.h" #import "GRPCConnectivityMonitor.h"
#pragma mark Flags #include <netinet/in.h>
@implementation GRPCReachabilityFlags { NSString *kGRPCConnectivityNotification = @"kGRPCConnectivityNotification";
SCNetworkReachabilityFlags _flags;
}
+ (instancetype)flagsWithFlags:(SCNetworkReachabilityFlags)flags { static SCNetworkReachabilityRef reachability;
return [[self alloc] initWithFlags:flags]; static GRPCConnectivityStatus currentStatus;
}
- (instancetype)initWithFlags:(SCNetworkReachabilityFlags)flags { // Aggregate information in flags into network status.
if ((self = [super init])) { GRPCConnectivityStatus CalculateConnectivityStatus(SCNetworkReachabilityFlags flags) {
_flags = flags; GRPCConnectivityStatus result = GRPCConnectivityUnknown;
if (((flags & kSCNetworkReachabilityFlagsReachable) == 0) ||
((flags & kSCNetworkReachabilityFlagsConnectionRequired) != 0)) {
return GRPCConnectivityNoNetwork;
} }
return self; result = GRPCConnectivityWiFi;
} #if TARGET_OS_IPHONE
if (flags & kSCNetworkReachabilityFlagsIsWWAN) {
/* return result = GRPCConnectivityCellular;
* One accessor method implementation per flag. Example:
- (BOOL)isCell { \
return !!(_flags & kSCNetworkReachabilityFlagsIsWWAN); \
}
*/
#define GRPC_XMACRO_ITEM(methodName, FlagName) \
- (BOOL)methodName { \
return !!(_flags & kSCNetworkReachabilityFlags ## FlagName); \
}
#include "GRPCReachabilityFlagNames.xmacro.h"
#undef GRPC_XMACRO_ITEM
- (BOOL)isHostReachable {
// Note: connectionOnDemand means it'll be reachable only if using the CFSocketStream API or APIs
// on top of it.
// connectionRequired means we can't tell until a connection is attempted (e.g. for VPN on
// demand).
return self.reachable && !self.interventionRequired && !self.connectionOnDemand;
}
- (NSString *)description {
NSMutableArray *activeOptions = [NSMutableArray arrayWithCapacity:9];
/*
* For each flag, add its name to the array if it's ON. Example:
if (self.isCell) {
[activeOptions addObject:@"isCell"];
} }
#endif
*/ return result;
#define GRPC_XMACRO_ITEM(methodName, FlagName) \
if (self.methodName) { \
[activeOptions addObject:@ #methodName]; \
}
#include "GRPCReachabilityFlagNames.xmacro.h"
#undef GRPC_XMACRO_ITEM
return activeOptions.count == 0 ? @"(none)" : [activeOptions componentsJoinedByString:@", "];
}
- (BOOL)isEqual:(id)object {
return [object isKindOfClass:[GRPCReachabilityFlags class]] &&
_flags == ((GRPCReachabilityFlags *)object)->_flags;
}
- (NSUInteger)hash {
return _flags;
}
@end
#pragma mark Connectivity Monitor
// Assumes the third argument is a block that accepts a GRPCReachabilityFlags object, and passes the
// received ones to it.
static void PassFlagsToContextInfoBlock(SCNetworkReachabilityRef target,
SCNetworkReachabilityFlags flags,
void *info) {
#pragma unused (target)
// This can be called many times with the same info. The info is retained by SCNetworkReachability
// while this function is being executed.
void (^handler)(GRPCReachabilityFlags *) = (__bridge void (^)(GRPCReachabilityFlags *))info;
handler([[GRPCReachabilityFlags alloc] initWithFlags:flags]);
} }
@implementation GRPCConnectivityMonitor { static void ReachabilityCallback(
SCNetworkReachabilityRef _reachabilityRef; SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) {
GRPCReachabilityFlags *_previousReachabilityFlags; GRPCConnectivityStatus newStatus = CalculateConnectivityStatus(flags);
}
- (nullable instancetype)initWithReachability:(nullable SCNetworkReachabilityRef)reachability { if (newStatus != currentStatus) {
if (!reachability) { [[NSNotificationCenter defaultCenter] postNotificationName:kGRPCConnectivityNotification
return nil; object:nil];
currentStatus = newStatus;
} }
if ((self = [super init])) {
_reachabilityRef = CFRetain(reachability);
_queue = dispatch_get_main_queue();
_previousReachabilityFlags = nil;
}
return self;
} }
+ (nullable instancetype)monitorWithHost:(nonnull NSString *)host { @implementation GRPCConnectivityMonitor
const char *hostName = host.UTF8String;
if (!hostName) {
[NSException raise:NSInvalidArgumentException
format:@"host.UTF8String returns NULL for %@", host];
}
SCNetworkReachabilityRef reachability =
SCNetworkReachabilityCreateWithName(NULL, hostName);
GRPCConnectivityMonitor *returnValue = [[self alloc] initWithReachability:reachability]; + (void)initialize {
if (reachability) { if (self == [GRPCConnectivityMonitor self]) {
CFRelease(reachability); struct sockaddr_in addr = {0};
} addr.sin_len = sizeof(addr);
return returnValue; addr.sin_family = AF_INET;
} reachability = SCNetworkReachabilityCreateWithAddress(NULL, (struct sockaddr *)&addr);
currentStatus = GRPCConnectivityUnknown;
- (void)handleLossWithHandler:(nullable void (^)(void))lossHandler SCNetworkConnectionFlags flags;
wifiStatusChangeHandler:(nullable void (^)(void))wifiStatusChangeHandler { if (SCNetworkReachabilityGetFlags(reachability, &flags)) {
__weak typeof(self) weakSelf = self; currentStatus = CalculateConnectivityStatus(flags);
[self startListeningWithHandler:^(GRPCReachabilityFlags *flags) {
typeof(self) strongSelf = weakSelf;
if (strongSelf) {
if (lossHandler && !flags.reachable) {
lossHandler();
#if TARGET_OS_IPHONE
} else if (wifiStatusChangeHandler &&
strongSelf->_previousReachabilityFlags &&
(flags.isWWAN ^
strongSelf->_previousReachabilityFlags.isWWAN)) {
wifiStatusChangeHandler();
#endif
}
strongSelf->_previousReachabilityFlags = flags;
} }
}];
}
- (void)startListeningWithHandler:(void (^)(GRPCReachabilityFlags *))handler { SCNetworkReachabilityContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
// Copy to ensure the handler block is in the heap (and so can't be deallocated when this method if (!SCNetworkReachabilitySetCallback(reachability, ReachabilityCallback, &context) ||
// returns). !SCNetworkReachabilityScheduleWithRunLoop(
void (^copiedHandler)(GRPCReachabilityFlags *) = [handler copy]; reachability, CFRunLoopGetMain(), kCFRunLoopCommonModes)) {
SCNetworkReachabilityContext context = { NSLog(@"gRPC connectivity monitor fail to set");
.version = 0, }
.info = (__bridge void *)copiedHandler, }
.retain = CFRetain,
.release = CFRelease,
};
// The following will retain context.info, and release it when the callback is set to NULL.
SCNetworkReachabilitySetCallback(_reachabilityRef, PassFlagsToContextInfoBlock, &context);
SCNetworkReachabilitySetDispatchQueue(_reachabilityRef, _queue);
}
- (void)stopListening {
// This releases the block on context.info.
SCNetworkReachabilitySetCallback(_reachabilityRef, NULL, NULL);
SCNetworkReachabilitySetDispatchQueue(_reachabilityRef, NULL);
} }
- (void)setQueue:(dispatch_queue_t)queue { + (void)registerObserver:(_Nonnull id)observer
_queue = queue ?: dispatch_get_main_queue(); selector:(SEL)selector {
[[NSNotificationCenter defaultCenter] addObserver:observer
selector:selector
name:kGRPCConnectivityNotification
object:nil];
} }
- (void)dealloc { + (void)unregisterObserver:(_Nonnull id)observer {
if (_reachabilityRef) { [[NSNotificationCenter defaultCenter] removeObserver:observer];
[self stopListening];
CFRelease(_reachabilityRef);
}
} }
@end @end

@ -37,12 +37,6 @@ NS_ASSUME_NONNULL_BEGIN
static NSMutableDictionary *kHostCache; static NSMutableDictionary *kHostCache;
// This connectivity monitor flushes the host cache when connectivity status
// changes or when connection switch between Wifi and Cellular data, so that a
// new call will use a new channel. Otherwise, a new call will still use the
// cached channel which is no longer available and will cause gRPC to hang.
static GRPCConnectivityMonitor *connectivityMonitor = nil;
@implementation GRPCHost { @implementation GRPCHost {
// TODO(mlumish): Investigate whether caching channels with strong links is a good idea. // TODO(mlumish): Investigate whether caching channels with strong links is a good idea.
GRPCChannel *_channel; GRPCChannel *_channel;
@ -90,17 +84,7 @@ static GRPCConnectivityMonitor *connectivityMonitor = nil;
kHostCache[address] = self; kHostCache[address] = self;
_compressAlgorithm = GRPC_COMPRESS_NONE; _compressAlgorithm = GRPC_COMPRESS_NONE;
} }
// Keep a single monitor to flush the cache if the connectivity status changes [GRPCConnectivityMonitor registerObserver:self selector:@selector(connectivityChange:)];
// Thread safety guarded by @synchronized(kHostCache)
if (!connectivityMonitor) {
connectivityMonitor =
[GRPCConnectivityMonitor monitorWithHost:hostURL.host];
void (^handler)(void) = ^{
[GRPCHost flushChannelCache];
};
[connectivityMonitor handleLossWithHandler:handler
wifiStatusChangeHandler:handler];
}
} }
return self; return self;
} }
@ -281,6 +265,13 @@ static GRPCConnectivityMonitor *connectivityMonitor = nil;
} }
} }
// Flushes the host cache when connectivity status changes or when connection switch between Wifi
// and Cellular data, so that a new call will use a new channel. Otherwise, a new call will still
// use the cached channel which is no longer available and will cause gRPC to hang.
- (void)connectivityChange:(NSNotification *)note {
[GRPCHost flushChannelCache];
}
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

Loading…
Cancel
Save