Skip to content
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

[MC-1883] File download concurrency #358

Merged
merged 3 commits into from
Aug 1, 2024
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
6 changes: 6 additions & 0 deletions CleverTapSDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@
6BB778D62BFD26E000A41628 /* CTInAppNotificationDisplayDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB778D52BFD26DF00A41628 /* CTInAppNotificationDisplayDelegate.h */; settings = {ATTRIBUTES = (Private, ); }; };
6BB778D72BFD26E000A41628 /* CTInAppNotificationDisplayDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB778D52BFD26DF00A41628 /* CTInAppNotificationDisplayDelegate.h */; settings = {ATTRIBUTES = (Private, ); }; };
6BB778D92BFD277400A41628 /* CTCustomTemplatesManager-Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB778D82BFD277400A41628 /* CTCustomTemplatesManager-Internal.h */; };
6BBF05CE2C58E3FB0047E3D9 /* NSURLSessionMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BBF05CD2C58E3FB0047E3D9 /* NSURLSessionMock.m */; };
6BD334EA2AF2A41F0099E33E /* CTBatchSentDelegateHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BD334E82AF2A41F0099E33E /* CTBatchSentDelegateHelper.h */; };
6BD334EB2AF2A41F0099E33E /* CTBatchSentDelegateHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BD334E82AF2A41F0099E33E /* CTBatchSentDelegateHelper.h */; };
6BD334EC2AF2A41F0099E33E /* CTBatchSentDelegateHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BD334E92AF2A41F0099E33E /* CTBatchSentDelegateHelper.m */; };
Expand Down Expand Up @@ -953,6 +954,8 @@
6BB778D12BF267B600A41628 /* CTTemplateContextTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTTemplateContextTest.m; sourceTree = "<group>"; };
6BB778D52BFD26DF00A41628 /* CTInAppNotificationDisplayDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTInAppNotificationDisplayDelegate.h; sourceTree = "<group>"; };
6BB778D82BFD277400A41628 /* CTCustomTemplatesManager-Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTCustomTemplatesManager-Internal.h"; sourceTree = "<group>"; };
6BBF05CC2C58E3FB0047E3D9 /* NSURLSessionMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSURLSessionMock.h; sourceTree = "<group>"; };
6BBF05CD2C58E3FB0047E3D9 /* NSURLSessionMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSURLSessionMock.m; sourceTree = "<group>"; };
6BD334E82AF2A41F0099E33E /* CTBatchSentDelegateHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTBatchSentDelegateHelper.h; sourceTree = "<group>"; };
6BD334E92AF2A41F0099E33E /* CTBatchSentDelegateHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTBatchSentDelegateHelper.m; sourceTree = "<group>"; };
6BD334EF2AF545C70099E33E /* CTInAppStoreTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTInAppStoreTest.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1514,6 +1517,8 @@
6B9E95B22C2868470002D557 /* CTFileDownloadManager+Tests.h */,
6B9E95B32C29C2F30002D557 /* NSFileManagerMock.h */,
6B9E95B42C29C2F30002D557 /* NSFileManagerMock.m */,
6BBF05CC2C58E3FB0047E3D9 /* NSURLSessionMock.h */,
6BBF05CD2C58E3FB0047E3D9 /* NSURLSessionMock.m */,
);
path = FileDownload;
sourceTree = "<group>";
Expand Down Expand Up @@ -2495,6 +2500,7 @@
32394C2529FA272600956058 /* CTValidatorTest.m in Sources */,
6BB778CE2BEE48C300A41628 /* CTCustomTemplateInAppDataTest.m in Sources */,
6BA3B2E82B07E207004E834B /* CTTriggersMatcher+Tests.m in Sources */,
6BBF05CE2C58E3FB0047E3D9 /* NSURLSessionMock.m in Sources */,
6B32A0A52B9A0F17009ADC57 /* CTCustomTemplateTest.m in Sources */,
4E1F155B276B662C009387AE /* EventDetail.m in Sources */,
6B4A0F912B45EF6D00A42C6D /* CTInAppTriggerManagerTest.m in Sources */,
Expand Down
3 changes: 2 additions & 1 deletion CleverTapSDK/CTConstants.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ static NSString *const kCLTAP_COMMAND_DELETE = @"$delete";
#define CLTAP_FILE_URLS_EXPIRY_DICT @"file_urls_expiry_dict"
#define CLTAP_FILE_ASSETS_LAST_DELETED_TS @"cs_file_assets_last_deleted_timestamp"
#define CLTAP_FILE_EXPIRY_OFFSET (60 * 60 * 24 * 7 * 2) // 2 weeks
#define CLTAP_FILE_RESOURCE_TIME_OUT_INTERVAL 15
#define CLTAP_FILE_RESOURCE_TIME_OUT_INTERVAL 25
#define CLTAP_FILE_MAX_CONCURRENCY_COUNT 10
#define CLTAP_FILES_DIRECTORY_NAME @"CleverTap_Files"

#pragma mark Constants for App fields
Expand Down
51 changes: 41 additions & 10 deletions CleverTapSDK/FileDownload/CTFileDownloadManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ @interface CTFileDownloadManager()
@property (nonatomic, strong) NSMutableDictionary<NSURL *, NSMutableArray<DownloadCompletionHandler> *> *downloadInProgressHandlers;
@property (nonatomic, strong) NSURLSession *session;
@property (nonatomic, strong) NSFileManager* fileManager;
@property NSTimeInterval semaphoreTimeout;

@end

Expand Down Expand Up @@ -37,6 +38,8 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig *)config {
NSURLSessionConfiguration *sc = [NSURLSessionConfiguration defaultSessionConfiguration];
sc.timeoutIntervalForRequest = CLTAP_REQUEST_TIME_OUT_INTERVAL;
sc.timeoutIntervalForResource = CLTAP_FILE_RESOURCE_TIME_OUT_INTERVAL;
// Timeout of (request timeout + 5) seconds for acquiring semaphore
self.semaphoreTimeout = CLTAP_FILE_RESOURCE_TIME_OUT_INTERVAL + 5;

self.session = [NSURLSession sessionWithConfiguration:sc];
self.fileManager = [NSFileManager defaultManager];
Expand All @@ -50,9 +53,22 @@ - (void)downloadFiles:(nonnull NSArray<NSURL *> *)urls
withCompletionBlock:(nonnull CTFilesDownloadCompletedBlock)completion {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(CLTAP_FILE_MAX_CONCURRENCY_COUNT);

NSMutableDictionary<NSString *,id> *filesDownloadStatus = [NSMutableDictionary new];
for (NSURL *url in urls) {
dispatch_group_enter(group);

dispatch_time_t semaphore_timeout = dispatch_time(DISPATCH_TIME_NOW,
self.semaphoreTimeout * NSEC_PER_SEC);
if (dispatch_semaphore_wait(semaphore, semaphore_timeout) != 0) {
@synchronized (filesDownloadStatus) {
[filesDownloadStatus setObject:@0 forKey:[url absoluteString]];
}
dispatch_group_leave(group);
continue; // Proceed to next URL
}

@synchronized (self) {
BOOL isAlreadyDownloading = [_downloadInProgressUrls containsObject:url];
if (isAlreadyDownloading) {
Expand All @@ -62,10 +78,11 @@ - (void)downloadFiles:(nonnull NSArray<NSURL *> *)urls
_downloadInProgressHandlers[url] = [NSMutableArray array];
}
[_downloadInProgressHandlers[url] addObject:^(NSURL *completedURL, BOOL success) {
@synchronized (self) {
@synchronized (filesDownloadStatus) {
[filesDownloadStatus setObject:[NSNumber numberWithBool:success] forKey:[completedURL absoluteString]];
dispatch_group_leave(group);
}
dispatch_group_leave(group);
dispatch_semaphore_signal(semaphore); // Signal that a slot is free
}];
continue;
}
Expand All @@ -78,8 +95,9 @@ - (void)downloadFiles:(nonnull NSArray<NSURL *> *)urls
}
dispatch_async(concurrentQueue, ^{
[self downloadSingleFile:url completed:^(BOOL success) {
[filesDownloadStatus setObject:[NSNumber numberWithBool:success] forKey:[url absoluteString]];

@synchronized (filesDownloadStatus) {
[filesDownloadStatus setObject:[NSNumber numberWithBool:success] forKey:[url absoluteString]];
}
// Call the other completion handlers for this file url if present
NSArray<DownloadCompletionHandler> *handlers;
@synchronized (self) {
Expand All @@ -90,15 +108,21 @@ - (void)downloadFiles:(nonnull NSArray<NSURL *> *)urls
for (DownloadCompletionHandler handler in handlers) {
handler(url, success);
}

dispatch_group_leave(group);
dispatch_semaphore_signal(semaphore); // Signal that a slot is free
}];
});
} else {
// Add the file url to callback as success true as it is already present
[filesDownloadStatus setObject:@1 forKey:[url absoluteString]];
@synchronized (filesDownloadStatus) {
// Add the file url to callback as success true as it is already present
[filesDownloadStatus setObject:@1 forKey:[url absoluteString]];
}
dispatch_group_leave(group);
dispatch_semaphore_signal(semaphore); // Signal that a slot is free
}
}

dispatch_group_notify(group, concurrentQueue, ^{
// Callback when all files are downloaded with their success status
completion(filesDownloadStatus);
Expand Down Expand Up @@ -132,13 +156,17 @@ - (void)deleteFiles:(NSArray<NSString *> *)urls withCompletionBlock:(CTFilesDele
dispatch_group_enter(deleteGroup);
dispatch_async(deleteConcurrentQueue, ^{
[self deleteSingleFile:url completed:^(BOOL success) {
[filesDeleteStatus setObject:[NSNumber numberWithBool:success] forKey:urlString];
@synchronized(filesDeleteStatus) {
[filesDeleteStatus setObject:[NSNumber numberWithBool:success] forKey:urlString];
}
dispatch_group_leave(deleteGroup);
}];
});
} else {
// Add the file url to callback as success true as it is already not present
[filesDeleteStatus setObject:@1 forKey:[url absoluteString]];
@synchronized(filesDeleteStatus) {
// Add the file url to callback as success true as it is already not present
[filesDeleteStatus setObject:@1 forKey:[url absoluteString]];
}
}
}
dispatch_group_notify(deleteGroup, deleteConcurrentQueue, ^{
Expand Down Expand Up @@ -170,7 +198,10 @@ - (void)removeAllFilesWithCompletionBlock:(CTFilesRemoveCompletedBlock)completio
if (!success) {
CleverTapLogInternal(self.config.logLevel, @"%@ Failed to remove file %@ - %@", self, [file absoluteString], error);
}
[filesDeleteStatus setObject:@(success) forKey:[file path]];
// Synchronize access to the dictionary
@synchronized (filesDeleteStatus) {
[filesDeleteStatus setObject:@(success) forKey:[file path]];
}
dispatch_group_leave(deleteGroup);
});
}
Expand Down
3 changes: 3 additions & 0 deletions CleverTapSDKTests/FileDownload/CTFileDownloadManager+Tests.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

@property (nonatomic, strong) NSURLSession *session;
@property (nonatomic, strong) NSFileManager* fileManager;
@property NSTimeInterval semaphoreTimeout;

- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config;

- (void)downloadSingleFile:(NSURL *)url
completed:(void(^)(BOOL success))completedBlock;
Expand Down
26 changes: 25 additions & 1 deletion CleverTapSDKTests/FileDownload/CTFileDownloadManagerTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#import "CTConstants.h"
#import "CTFileDownloadTestHelper.h"
#import "NSFileManagerMock.h"
#import "NSURLSessionMock.h"

@interface CTFileDownloadManagerTests : XCTestCase

Expand All @@ -23,7 +24,7 @@ - (void)setUp {
self.helper = [CTFileDownloadTestHelper new];
[self.helper addHTTPStub];
self.config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"testAccountId" accountToken:@"testAccountToken"];
self.fileDownloadManager = [CTFileDownloadManager sharedInstanceWithConfig:self.config];
self.fileDownloadManager = [[CTFileDownloadManager alloc] initWithConfig:self.config];
}

- (void)tearDown {
Expand Down Expand Up @@ -307,6 +308,29 @@ - (void)testDownloadFilesOneUrlTimeOut {
[HTTPStubs removeStub:stub];
}

- (void)testSemaphoreTimeout {
XCTestExpectation *expectation = [self expectationWithDescription:@"Semaphore Timeout Test"];
// Generate URLs more than the max concurrency count CLTAP_FILE_MAX_CONCURRENCY_COUNT
self.fileURLs = [self.helper generateFileURLs:15];

// Set mock session
NSURLSessionMock *mockSession = [[NSURLSessionMock alloc] init];
mockSession.delayInterval = 0.3; // Simulate a delay longer than semaphore timeout
self.fileDownloadManager.semaphoreTimeout = 0.1;
self.fileDownloadManager.session = mockSession;

[self.fileDownloadManager downloadFiles:self.fileURLs withCompletionBlock:^(NSDictionary<NSString *,NSNumber *> * _Nonnull fileDownloadStatus) {
for (NSURL *url in self.fileURLs) {
NSNumber *status = fileDownloadStatus[url.absoluteString];
XCTAssertNotNil(status, @"File download status should not be nil.");
XCTAssertEqual([status integerValue], 0, @"File download should fail due to semaphore timeout.");
}
[expectation fulfill];
}];

[self waitForExpectations:@[expectation] timeout:2.0];
}

- (void)testDownloadSingle {
self.fileURLs = [self.helper generateFileURLs:1];
XCTestExpectation *expectation1 = [self expectationWithDescription:@"Download files callback 1"];
Expand Down
29 changes: 29 additions & 0 deletions CleverTapSDKTests/FileDownload/NSURLSessionMock.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// NSURLSessionMock.h
// CleverTapSDKTests
//
// Created by Nikola Zagorchev on 30.07.24.
// Copyright © 2024 CleverTap. All rights reserved.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSURLSessionMock : NSURLSession

@property (nonatomic, assign) NSTimeInterval delayInterval;

@end

@interface NSURLSessionDownloadTaskMock : NSURLSessionDownloadTask

@property (nonatomic, copy) void (^completionHandler)(NSURL * _Nullable, NSURLResponse * _Nullable, NSError * _Nullable);
@property (nonatomic, assign) NSTimeInterval delayInterval;

- (instancetype)initWithCompletionHandler:(void (^)(NSURL *, NSURLResponse *, NSError *))completionHandler
delayInterval:(NSTimeInterval)delayInterval;

@end

NS_ASSUME_NONNULL_END
41 changes: 41 additions & 0 deletions CleverTapSDKTests/FileDownload/NSURLSessionMock.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// NSURLSessionMock.m
// CleverTapSDKTests
//
// Created by Nikola Zagorchev on 30.07.24.
// Copyright © 2024 CleverTap. All rights reserved.
//

#import "NSURLSessionMock.h"

@implementation NSURLSessionMock

- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSURL * _Nullable, NSURLResponse * _Nullable, NSError * _Nullable))completionHandler {
return [[NSURLSessionDownloadTaskMock alloc] initWithCompletionHandler:completionHandler delayInterval:self.delayInterval];
}

@end

@implementation NSURLSessionDownloadTaskMock

- (instancetype)initWithCompletionHandler:(void (^)(NSURL *, NSURLResponse *, NSError *))completionHandler
delayInterval:(NSTimeInterval)delayInterval {
self = [super init];
if (self) {
_completionHandler = [completionHandler copy];
_delayInterval = delayInterval;
}
return self;
}

- (void)resume {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.delayInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (self.completionHandler) {
self.completionHandler(nil, nil, [NSError errorWithDomain:@"MockErrorDomain" code:-1001 userInfo:@{NSLocalizedDescriptionKey: @"Timeout"}]);
}
});
}

@end