Skip to content

Commit

Permalink
Release version 2.4.0 (#99)
Browse files Browse the repository at this point in the history
* Add configurable background fetch interval
* Fix eventsArray crash due to multi thread manipulation
  • Loading branch information
arun251 authored Jun 13, 2017
1 parent 0397603 commit da76d06
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 55 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

All notable changes to the LaunchDarkly iOS SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).

## [2.4.0] - 2017-06-09
### Added
- The client's background fetch interval can be configured using `withBackgroundFetchInterval`.

### Changed
- By default, the client allows one background fetch per 60 minutes.

### Fixed
- Memory leak with `NSURLSession` in `LDRequestManager`. Thanks @jimmaye!
- Race condition when the client is used in multiple threads

## [2.3.3] - 2017-05-25
### Changed
- Feature flag persistence is now more efficient
Expand Down
2 changes: 2 additions & 0 deletions Darkly/DarklyConstants.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,6 @@ extern int const kDefaultFlushInterval;
extern int const kMinimumFlushIntervalMillis;
extern int const kDefaultPollingInterval;
extern int const kMinimumPollingInterval;
extern int const kDefaultBackgroundFetchInterval;
extern int const kMinimumBackgroundFetchInterval;
extern int const kMillisInSecs;
4 changes: 3 additions & 1 deletion Darkly/DarklyConstants.m
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

#import "DarklyConstants.h"

NSString * const kClientVersion = @"2.3.3";
NSString * const kClientVersion = @"2.4.0";
NSString * const kBaseUrl = @"https://app.launchdarkly.com";
NSString * const kEventsUrl = @"https://mobile.launchdarkly.com";
NSString * const kStreamUrl = @"https://clientstream.launchdarkly.com/mping";
Expand Down Expand Up @@ -32,4 +32,6 @@
int const kMinimumFlushIntervalMillis = 0;
int const kDefaultPollingInterval = 300;
int const kMinimumPollingInterval = 60;
int const kDefaultBackgroundFetchInterval = 3600;
int const kMinimumBackgroundFetchInterval = 900;
int const kMillisInSecs = 1000;
27 changes: 20 additions & 7 deletions Darkly/LDClientManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
@interface LDClientManager()

@property(nonatomic, strong, readonly) EventSource *eventSource;
@property(nonatomic, strong) NSDate *backgroundTime;

@end

Expand All @@ -34,7 +35,7 @@ +(LDClientManager *)sharedInstance {
[[NSNotificationCenter defaultCenter] addObserver:sharedApiManager selector:@selector(willEnterForeground) name:NSApplicationDidBecomeActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:sharedApiManager selector:@selector(willEnterBackground) name:NSApplicationWillResignActiveNotification object:nil];
#endif
[[NSNotificationCenter defaultCenter] addObserver:sharedApiManager selector:@selector(syncWithServerForConfig) name:kLDBackgroundFetchInitiated object:nil];
[[NSNotificationCenter defaultCenter] addObserver:sharedApiManager selector:@selector(backgroundFetchInitiated) name:kLDBackgroundFetchInitiated object:nil];

});
return sharedApiManager;
Expand Down Expand Up @@ -92,6 +93,9 @@ - (void)willEnterBackground {
}

[self flushEvents];

self.backgroundTime = [NSDate date];

}

- (void)willEnterForeground {
Expand All @@ -113,17 +117,26 @@ - (void)willEnterForeground {
}
}

- (void)backgroundFetchInitiated {
NSTimeInterval time = [[NSDate date] timeIntervalSinceDate:self.backgroundTime];
LDConfig *config = [[LDClient sharedInstance] ldConfig];
if (time >= [config.backgroundFetchInterval doubleValue]) {
[self syncWithServerForConfig];
}
}

-(void)syncWithServerForEvents {
if (!offlineEnabled) {
DEBUG_LOGX(@"ClientManager syncing events with server");

NSArray *eventJsonData = [[LDDataManager sharedManager] allEventsJsonArray];
[[LDDataManager sharedManager] allEventsJsonArray:^(NSArray *array) {
if (array) {
[[LDRequestManager sharedInstance] performEventRequest:array];
} else {
DEBUG_LOGX(@"ClientManager has no events so won't sync events with server");
}
}];

if (eventJsonData) {
[[LDRequestManager sharedInstance] performEventRequest:eventJsonData];
} else {
DEBUG_LOGX(@"ClientManager has no events so won't sync events with server");
}
} else {
DEBUG_LOGX(@"ClientManager is in offline mode so won't sync events with server");
}
Expand Down
9 changes: 9 additions & 0 deletions Darkly/LDConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
@property (nonatomic) NSNumber* connectionTimeout;
@property (nonatomic) NSNumber* flushInterval;
@property (nonatomic) NSNumber* pollingInterval;
@property (nonatomic) NSNumber* backgroundFetchInterval;
@property (nonatomic) BOOL streaming;
@property (nonatomic) BOOL debugEnabled;

Expand Down Expand Up @@ -83,6 +84,14 @@
* @return the configuration builder
*/
- (LDConfigBuilder *)withPollingInterval:(int)pollingInterval;
/**
* Set the background fetch interval (in seconds) for background fetch. An interval
* less than 900 is set to the minimum (15 minutes). The default is 60 minutes. (Optional)
*
* @param backgroundFetchInterval the background fetch interval in seconds
* @return the configuration builder
*/
- (LDConfigBuilder *)withBackgroundFetchInterval:(int)backgroundFetchInterval;
/**
* Enable streaming mode for flags. When streaming is false, disable streaming and switch to polling mode. (Optional)
*
Expand Down
13 changes: 13 additions & 0 deletions Darkly/LDConfig.m
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ @interface LDConfigBuilder() {
NSNumber *connectionTimeout;
NSNumber *flushInterval;
NSNumber *pollingInterval;
NSNumber *backgroundFetchInterval;
BOOL streaming;
BOOL debugEnabled;
}
Expand Down Expand Up @@ -74,6 +75,11 @@ - (LDConfigBuilder *)withPollingInterval:(int)inputPollingInterval
return self;
}

- (LDConfigBuilder *)withBackgroundFetchInterval:(int)inputBackgroundFetchInterval {
backgroundFetchInterval = [NSNumber numberWithInt:MAX(inputBackgroundFetchInterval, kMinimumBackgroundFetchInterval)];
return self;
}

- (LDConfigBuilder *)withStreaming:(BOOL)inputStreamingEnabled
{
streaming = inputStreamingEnabled;
Expand Down Expand Up @@ -139,6 +145,13 @@ -(LDConfig *)build
DEBUG_LOG(@"LDConfigBuilder building LDConfig with default polling interval: %d", kDefaultPollingInterval);
[config setPollingInterval:[NSNumber numberWithInt:kDefaultPollingInterval]];
}
if (backgroundFetchInterval) {
DEBUG_LOG(@"LDConfigBuilder building LDConfig with background fetch interval: %@", backgroundFetchInterval);
[config setBackgroundFetchInterval:backgroundFetchInterval];
} else {
DEBUG_LOG(@"LDConfigBuilder building LDConfig with default background fetch interval: %d", kDefaultBackgroundFetchInterval);
[config setBackgroundFetchInterval:[NSNumber numberWithInt:kDefaultBackgroundFetchInterval]];
}

DEBUG_LOG(@"LDConfigBuilder building LDConfig with streaming enabled: %d", streaming);
[config setStreaming:streaming];
Expand Down
2 changes: 1 addition & 1 deletion Darkly/LDDataManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ extern int const kUserCacheSize;

+(LDDataManager *)sharedManager;

-(NSArray*) allEventsJsonArray;
-(void) allEventsJsonArray:(void (^)(NSArray *array))completion;
-(NSMutableDictionary *)retrieveUserDictionary;
-(NSMutableArray *)retrieveEventsArray;
-(LDUserModel *)findUserWithkey: (NSString *)key;
Expand Down
46 changes: 29 additions & 17 deletions Darkly/LDDataManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,21 @@

@interface LDDataManager()

@property (strong, nonatomic) NSMutableArray *eventsArray;
@property (strong, atomic) NSMutableArray *eventsArray;

@end

@implementation LDDataManager

dispatch_queue_t eventsQueue;

+ (id)sharedManager {
static LDDataManager *sharedDataManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedDataManager = [[self alloc] init];
sharedDataManager.eventsArray = [[NSMutableArray alloc] init];
eventsQueue = dispatch_queue_create("com.launchdarkly.EventQueue", NULL);
});
return sharedDataManager;
}
Expand Down Expand Up @@ -154,7 +157,9 @@ -(void) createFeatureEvent: (NSString *)featureKey keyValue:(NSObject*)keyValue
// No Dictionary exists so create
_eventsArray = [[NSMutableArray alloc] init];
}
[_eventsArray addObject:featureEvent];
dispatch_async(eventsQueue, ^{
[_eventsArray addObject:featureEvent];
});
} else
DEBUG_LOG(@"Events have surpassed capacity. Discarding feature event %@", featureKey);
}
Expand All @@ -170,7 +175,9 @@ -(void) createCustomEvent: (NSString *)eventKey withCustomValuesDictionary: (NSD
// No Dictionary exists so create
_eventsArray = [[NSMutableArray alloc] init];
}
[_eventsArray addObject:customEvent];
dispatch_async(eventsQueue, ^{
[_eventsArray addObject:customEvent];
});
} else
DEBUG_LOG(@"Events have surpassed capacity. Discarding event %@ with dictionary %@", eventKey, customDict);
}
Expand All @@ -182,29 +189,34 @@ -(BOOL)isAtEventCapacity:(NSArray *)currentArray {

-(void) deleteProcessedEvents: (NSArray *) processedJsonArray {
// Loop through processedEvents
NSInteger count = MIN([processedJsonArray count], [_eventsArray count]);
[_eventsArray removeObjectsInRange:NSMakeRange(0, count)];
dispatch_async(eventsQueue, ^{
NSInteger count = MIN([processedJsonArray count], [_eventsArray count]);
[_eventsArray removeObjectsInRange:NSMakeRange(0, count)];
});
}

-(NSArray*) allEventsJsonArray {
NSMutableArray *array = [self retrieveEventsArray];
if (array && [array count]) {
NSMutableArray *eventArray = [[NSMutableArray alloc] init];
for (LDEventModel *currentEvent in array) {
[eventArray addObject:[currentEvent dictionaryValue]];
-(void) allEventsJsonArray:(void (^)(NSArray *array))completion {
dispatch_async(eventsQueue, ^{
NSMutableArray *array = [self retrieveEventsArray];
if (array && [array count]) {

NSMutableArray *eventArray = [[NSMutableArray alloc] init];
for (LDEventModel *currentEvent in array) {
[eventArray addObject:[currentEvent dictionaryValue]];
}

completion(eventArray);
} else {
completion(nil);
}
return eventArray;
} else {
return nil;
}
});
}

-(void)flushEventsDictionary {
[_eventsArray removeAllObjects];
}

- (NSMutableArray *)retrieveEventsArray {
return [_eventsArray mutableCopy];
return [[NSMutableArray alloc] initWithArray:self.eventsArray];
}

@end
27 changes: 18 additions & 9 deletions DarklyTests/LDClientManagerTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,15 @@ - (void)testSyncWithServerForEventsWhenEventsExist {
LDClientManager *clientManager = [LDClientManager sharedInstance];
[clientManager setOfflineEnabled:NO];

OCMStub([dataManagerMock allEventsJsonArray]).andReturn(testData);
[dataManagerMock allEventsJsonArray:^(NSArray *array) {
OCMStub(array).andReturn(testData);

[clientManager syncWithServerForEvents];

OCMVerify([requestManagerMock performEventRequest:[OCMArg isEqual:testData]]);
}];

[clientManager syncWithServerForEvents];

OCMVerify([requestManagerMock performEventRequest:[OCMArg isEqual:testData]]);

}

Expand All @@ -109,15 +113,20 @@ - (void)testDoNotSyncWithServerForEventsWhenEventsDoNotExist {

- (void)testSyncWithServerForEventsNotProcessedWhenOffline {
NSData *testData = [[NSData alloc] init];
OCMStub([dataManagerMock allEventsJsonArray]).andReturn(testData);

[[requestManagerMock reject] performEventRequest:[OCMArg isEqual:testData]];
[dataManagerMock allEventsJsonArray:^(NSArray *array) {
OCMStub(array).andReturn(testData);

[[requestManagerMock reject] performEventRequest:[OCMArg isEqual:testData]];

LDClientManager *clientManager = [LDClientManager sharedInstance];
[clientManager setOfflineEnabled:YES];
[clientManager syncWithServerForEvents];

[requestManagerMock verify];
}];

LDClientManager *clientManager = [LDClientManager sharedInstance];
[clientManager setOfflineEnabled:YES];
[clientManager syncWithServerForEvents];

[requestManagerMock verify];
}

- (void)testStartPolling {
Expand Down
54 changes: 38 additions & 16 deletions DarklyTests/Models/LDDataManagerTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -63,28 +63,42 @@ -(void)testAllEventsDictionaryArray {
[[LDDataManager sharedManager] createFeatureEvent:eventKey1 keyValue:[NSNumber numberWithBool:NO] defaultKeyValue:[NSNumber numberWithBool:NO]];
[[LDDataManager sharedManager] createCustomEvent:eventKey2 withCustomValuesDictionary:@{@"carrot": @"cake"}];

NSArray *eventArray = [[LDDataManager sharedManager] allEventsJsonArray];
NSMutableArray *eventKeyArray = [[NSMutableArray alloc] init];
for (NSDictionary *eventDictionary in eventArray) {
[eventKeyArray addObject:[eventDictionary objectForKey:@"key"]];
}

XCTAssertTrue([eventKeyArray containsObject:eventKey1]);
XCTAssertTrue([eventKeyArray containsObject:eventKey2]);
XCTestExpectation *expectation = [self expectationWithDescription:@"All events dictionary expectation"];

[[LDDataManager sharedManager] allEventsJsonArray:^(NSArray *array) {
NSMutableArray *eventKeyArray = [[NSMutableArray alloc] init];
for (NSDictionary *eventDictionary in array) {
[eventKeyArray addObject:[eventDictionary objectForKey:@"key"]];
}

XCTAssertTrue([eventKeyArray containsObject:eventKey1]);
XCTAssertTrue([eventKeyArray containsObject:eventKey2]);
[expectation fulfill];
}];

[self waitForExpectations:@[expectation] timeout:10];

}

-(void)testAllEventsJsonData {
[[LDDataManager sharedManager] createCustomEvent:@"foo" withCustomValuesDictionary:nil];
[[LDDataManager sharedManager] createCustomEvent:@"fi" withCustomValuesDictionary:nil];

NSArray *eventsArray = [[LDDataManager sharedManager] allEventsJsonArray];

NSMutableDictionary *eventDictionary = [[NSMutableDictionary alloc] init];
for (NSDictionary *currentEventDictionary in eventsArray) {
[eventDictionary setObject:[[LDEventModel alloc] initWithDictionary:currentEventDictionary] forKey:[currentEventDictionary objectForKey:@"key"]];
}
XCTestExpectation *expectation = [self expectationWithDescription:@"All events json data expectation"];

[[LDDataManager sharedManager] allEventsJsonArray:^(NSArray *array) {

NSMutableDictionary *eventDictionary = [[NSMutableDictionary alloc] init];
for (NSDictionary *currentEventDictionary in array) {
[eventDictionary setObject:[[LDEventModel alloc] initWithDictionary:currentEventDictionary] forKey:[currentEventDictionary objectForKey:@"key"]];
}

XCTAssertEqual([eventDictionary count], 2);
[expectation fulfill];
}];

[self waitForExpectations:@[expectation] timeout:10];

XCTAssertEqual([eventDictionary count], 2);
}


Expand Down Expand Up @@ -140,6 +154,8 @@ -(void)testCreateEventAfterCapacityReached{
builder = [builder withMobileKey: @"AMobileKey"];
LDConfig *config = [builder build];

XCTestExpectation *expectation = [self expectationWithDescription:@"All events dictionary expectation"];

OCMStub([clientMock ldConfig]).andReturn(config);

LDDataManager *manager = [LDDataManager sharedManager];
Expand All @@ -149,7 +165,13 @@ -(void)testCreateEventAfterCapacityReached{
[manager createCustomEvent:@"aKey" withCustomValuesDictionary: @{@"carrot": @"cake"}];
[manager createFeatureEvent: @"anotherKet" keyValue: [NSNumber numberWithBool:YES] defaultKeyValue: [NSNumber numberWithBool:NO]];

XCTAssertEqual([[manager retrieveEventsArray] count],2);
[manager allEventsJsonArray:^(NSArray *array) {
XCTAssertEqual([array count],2);
[expectation fulfill];
}];

[self waitForExpectations:@[expectation] timeout:10];

}

@end
Loading

0 comments on commit da76d06

Please sign in to comment.