diff --git a/src/objective-c/GRPCClient/GRPCCall+ChannelArg.h b/src/objective-c/GRPCClient/GRPCCall+ChannelArg.h index c05ba54c999..803f19dedfe 100644 --- a/src/objective-c/GRPCClient/GRPCCall+ChannelArg.h +++ b/src/objective-c/GRPCClient/GRPCCall+ChannelArg.h @@ -55,4 +55,19 @@ typedef NS_ENUM(NSInteger, GRPCCompressAlgorithm) { timeout:(int)timeout forHost:(nonnull NSString *)host; +/** Enable/Disable automatic retry of gRPC calls on the channel. If automatic retry is enabled, the + * retry is controlled by server's service config. If automatic retry is disabled, failed calls are + * immediately returned to the application layer. */ ++ (void)enableRetry:(BOOL)enabled forHost:(nonnull NSString *)host; + +/** Set channel connection timeout and backoff parameters. All parameters are positive integers in + * milliseconds. Set a parameter to 0 to make gRPC use default value for that parameter. + * + * Refer to gRPC's doc at https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md for the + * details of each parameter. */ ++ (void)setMinConnectTimeout:(unsigned int)timeout + initialBackoff:(unsigned int)initialBackoff + maxBackoff:(unsigned int)maxBackoff + forHost:(nonnull NSString *)host; + @end diff --git a/src/objective-c/GRPCClient/GRPCCall+ChannelArg.m b/src/objective-c/GRPCClient/GRPCCall+ChannelArg.m index 8f9c1b90ce7..0e631fb3adf 100644 --- a/src/objective-c/GRPCClient/GRPCCall+ChannelArg.m +++ b/src/objective-c/GRPCClient/GRPCCall+ChannelArg.m @@ -64,4 +64,19 @@ hostConfig.keepaliveTimeout = timeout; } ++ (void)enableRetry:(BOOL)enabled forHost:(nonnull NSString *)host { + GRPCHost *hostConfig = [GRPCHost hostWithAddress:host]; + hostConfig.retryEnabled = enabled; +} + ++ (void)setMinConnectTimeout:(unsigned int)timeout + initialBackoff:(unsigned int)initialBackoff + maxBackoff:(unsigned int)maxBackoff + forHost:(nonnull NSString *)host { + GRPCHost *hostConfig = [GRPCHost hostWithAddress:host]; + hostConfig.minConnectTimeout = timeout; + hostConfig.initialConnectBackoff = initialBackoff; + hostConfig.maxConnectBackoff = maxBackoff; +} + @end diff --git a/src/objective-c/GRPCClient/private/GRPCHost.h b/src/objective-c/GRPCClient/private/GRPCHost.h index d9916d93036..291b07df377 100644 --- a/src/objective-c/GRPCClient/private/GRPCHost.h +++ b/src/objective-c/GRPCClient/private/GRPCHost.h @@ -38,6 +38,11 @@ struct grpc_channel_credentials; @property(nonatomic) int keepaliveInterval; @property(nonatomic) int keepaliveTimeout; @property(nonatomic) id logContext; +@property(nonatomic) BOOL retryEnabled; + +@property(nonatomic) unsigned int minConnectTimeout; +@property(nonatomic) unsigned int initialConnectBackoff; +@property(nonatomic) unsigned int maxConnectBackoff; /** The following properties should only be modified for testing: */ diff --git a/src/objective-c/GRPCClient/private/GRPCHost.m b/src/objective-c/GRPCClient/private/GRPCHost.m index 348989904a3..2e9f9f243b3 100644 --- a/src/objective-c/GRPCClient/private/GRPCHost.m +++ b/src/objective-c/GRPCClient/private/GRPCHost.m @@ -85,6 +85,7 @@ static NSMutableDictionary *kHostCache; _secure = YES; kHostCache[address] = self; _compressAlgorithm = GRPC_COMPRESS_NONE; + _retryEnabled = YES; } #ifndef GRPC_CFSTREAM [GRPCConnectivityMonitor registerObserver:self selector:@selector(connectivityChange:)]; @@ -240,6 +241,20 @@ static NSMutableDictionary *kHostCache; args[@GRPC_ARG_DISABLE_CLIENT_AUTHORITY_FILTER] = [NSNumber numberWithInt:1]; } + if (_retryEnabled == NO) { + args[@GRPC_ARG_ENABLE_RETRIES] = [NSNumber numberWithInt:0]; + } + + if (_minConnectTimeout > 0) { + args[@GRPC_ARG_MIN_RECONNECT_BACKOFF_MS] = [NSNumber numberWithInt:_minConnectTimeout]; + } + if (_initialConnectBackoff > 0) { + args[@GRPC_ARG_INITIAL_RECONNECT_BACKOFF_MS] = [NSNumber numberWithInt:_initialConnectBackoff]; + } + if (_maxConnectBackoff > 0) { + args[@GRPC_ARG_MAX_RECONNECT_BACKOFF_MS] = [NSNumber numberWithInt:_maxConnectBackoff]; + } + return args; } diff --git a/src/objective-c/tests/GRPCClientTests.m b/src/objective-c/tests/GRPCClientTests.m index d9186561c35..2a169800a09 100644 --- a/src/objective-c/tests/GRPCClientTests.m +++ b/src/objective-c/tests/GRPCClientTests.m @@ -548,4 +548,47 @@ static GRPCProtoMethod *kFullDuplexCallMethod; [self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil]; } +- (void)testTimeoutBackoffWithTimeout:(double)timeout Backoff:(double)backoff { + const double maxConnectTime = timeout > backoff ? timeout : backoff; + const double kMargin = 0.1; + + __weak XCTestExpectation *completion = [self expectationWithDescription:@"Timeout in a second."]; + NSString *const kDummyAddress = [NSString stringWithFormat:@"8.8.8.8:1"]; + GRPCCall *call = [[GRPCCall alloc] initWithHost:kDummyAddress + path:@"" + requestsWriter:[GRXWriter writerWithValue:[NSData data]]]; + [GRPCCall setMinConnectTimeout:timeout * 1000 + initialBackoff:backoff * 1000 + maxBackoff:0 + forHost:kDummyAddress]; + NSDate *startTime = [NSDate date]; + id responsesWriteable = [[GRXWriteable alloc] initWithValueHandler:^(id value) { + XCTAssert(NO, @"Received message. Should not reach here"); + } + completionHandler:^(NSError *errorOrNil) { + XCTAssertNotNil(errorOrNil, @"Finished with no error"); + // The call must fail before maxConnectTime. However there is no lower bound on the time + // taken for connection. A shorter time happens when connection is actively refused + // by 8.8.8.8:1 before maxConnectTime elapsed. + XCTAssertLessThan([[NSDate date] timeIntervalSinceDate:startTime], + maxConnectTime + kMargin); + [completion fulfill]; + }]; + + [call startWithWriteable:responsesWriteable]; + + [self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil]; +} + +// The numbers of the following three tests are selected to be smaller than the default values of +// initial backoff (1s) and min_connect_timeout (20s), so that if they fail we know the default +// values fail to be overridden by the channel args. +- (void)testTimeoutBackoff2 { + [self testTimeoutBackoffWithTimeout:0.7 Backoff:0.3]; +} + +- (void)testTimeoutBackoff3 { + [self testTimeoutBackoffWithTimeout:0.3 Backoff:0.7]; +} + @end