Skip to content
Open
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
12 changes: 12 additions & 0 deletions Countly.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,15 @@
96E680422BFF89AC0091E105 /* CountlyCrashReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E680412BFF89AC0091E105 /* CountlyCrashReporterTests.swift */; };
96F80BE72F17D066006B4F71 /* CountlyWebViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 96F80BE62F17D066006B4F71 /* CountlyWebViewController.h */; };
96F80BE92F17D06F006B4F71 /* CountlyWebViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 96F80BE82F17D06F006B4F71 /* CountlyWebViewController.m */; };
A431BFDB50E23B1C056F93A1 /* CountlyQueueFlushRunnablesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B81507954AACF61B41133AA /* CountlyQueueFlushRunnablesTests.swift */; };
CD99EB53BFF33C2DA9C50715 /* CountlyCallbackBaseTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0A9216F7158834687B2631 /* CountlyCallbackBaseTestCase.swift */; };
D219374B248AC71C00E5798B /* CountlyPerformanceMonitoring.h in Headers */ = {isa = PBXBuildFile; fileRef = D2193749248AC71C00E5798B /* CountlyPerformanceMonitoring.h */; };
D219374C248AC71C00E5798B /* CountlyPerformanceMonitoring.m in Sources */ = {isa = PBXBuildFile; fileRef = D219374A248AC71C00E5798B /* CountlyPerformanceMonitoring.m */; };
D249BF5E254D3D180058A6C2 /* CountlyFeedbackWidget.h in Headers */ = {isa = PBXBuildFile; fileRef = D249BF5C254D3D170058A6C2 /* CountlyFeedbackWidget.h */; settings = {ATTRIBUTES = (Public, ); }; };
D249BF5F254D3D180058A6C2 /* CountlyFeedbackWidget.m in Sources */ = {isa = PBXBuildFile; fileRef = D249BF5D254D3D180058A6C2 /* CountlyFeedbackWidget.m */; };
D2CFEF972545FBE80026B044 /* CountlyFeedbacksInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = D2CFEF952545FBE80026B044 /* CountlyFeedbacksInternal.h */; };
D2CFEF982545FBE80026B044 /* CountlyFeedbacksInternal.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CFEF962545FBE80026B044 /* CountlyFeedbacksInternal.m */; };
F041D46C963F3EA85882CB0D /* CountlyRequestCallbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1C3803ECB0A386136072D6 /* CountlyRequestCallbackTests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand All @@ -120,6 +123,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
1A0A9216F7158834687B2631 /* CountlyCallbackBaseTestCase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CountlyCallbackBaseTestCase.swift; sourceTree = "<group>"; };
1A3110622A7128CD001CB507 /* CountlyViewData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyViewData.m; sourceTree = "<group>"; };
1A3110642A7128DC001CB507 /* CountlyViewData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyViewData.h; sourceTree = "<group>"; };
1A31106E2A7141AF001CB507 /* CountlyViewTracking.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyViewTracking.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -199,6 +203,8 @@
3B20A9AF2245228600E3D7AE /* CountlyPersistency.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyPersistency.m; sourceTree = "<group>"; };
3B20A9B12245228700E3D7AE /* CountlyConsentManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyConsentManager.h; sourceTree = "<group>"; };
4C3A4C9F2EB4C40000827FEA /* EventThreadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventThreadTests.swift; sourceTree = "<group>"; };
6B81507954AACF61B41133AA /* CountlyQueueFlushRunnablesTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CountlyQueueFlushRunnablesTests.swift; sourceTree = "<group>"; };
7C1C3803ECB0A386136072D6 /* CountlyRequestCallbackTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CountlyRequestCallbackTests.swift; sourceTree = "<group>"; };
96095A5E2F20105600FDE933 /* TouchDelegatingView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TouchDelegatingView.h; sourceTree = "<group>"; };
962485B92D9E971400FA3C20 /* TestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = "<group>"; };
96329DDF2D9426F300BFD641 /* CountlyServerConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyServerConfigTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -268,6 +274,9 @@
3964A3E62C2AF8E90091E677 /* CountlySegmentationTests.swift */,
399B464F2C52813700AD384E /* CountlyLocationTests.swift */,
3969D0222CB80848000F8A32 /* CountlyViewTests.swift */,
6B81507954AACF61B41133AA /* CountlyQueueFlushRunnablesTests.swift */,
7C1C3803ECB0A386136072D6 /* CountlyRequestCallbackTests.swift */,
1A0A9216F7158834687B2631 /* CountlyCallbackBaseTestCase.swift */,
);
path = CountlyTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -548,6 +557,9 @@
3972EDDB2C08A38D00EB9D3E /* CountlyEventStruct.swift in Sources */,
9673567F2EC60CD400C742D8 /* TestURLProtocol.swift in Sources */,
96E680422BFF89AC0091E105 /* CountlyCrashReporterTests.swift in Sources */,
A431BFDB50E23B1C056F93A1 /* CountlyQueueFlushRunnablesTests.swift in Sources */,
F041D46C963F3EA85882CB0D /* CountlyRequestCallbackTests.swift in Sources */,
CD99EB53BFF33C2DA9C50715 /* CountlyCallbackBaseTestCase.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
39 changes: 39 additions & 0 deletions CountlyConnectionManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,43 @@ extern const NSInteger kCountlyGETRequestMaxLength;

- (BOOL)isSessionStarted;

#pragma mark - Request Callbacks

/**
* Callback block type for individual request results.
* @param response Response string from server (or error description if failed)
* @param success YES if request succeeded, NO if failed
*/
typedef void (^CLYRequestCallback)(NSString * _Nullable response, BOOL success);

/**
* Runnable block type for queue flush completion.
* @discussion Executed when all requests in the queue complete successfully.
*/
typedef void (^CLYQueueFlushRunnable)(void);

/**
* Adds a runnable to be executed when the request queue is successfully flushed.
* @discussion Multiple runnables can be registered and will all execute when queue becomes empty.
* @discussion Runnables are automatically removed after successful execution.
* @discussion Runnables only execute when ALL requests complete successfully.
* @param runnable Block to be executed when queue is successfully flushed
*/
- (void)addQueueFlushRunnable:(CLYQueueFlushRunnable _Nonnull)runnable;

/**
* Removes all registered queue flush runnables.
*/
- (void)clearQueueFlushRunnables;

/**
* Adds a request to the queue with an associated callback.
* @discussion The callback will be executed when this specific request completes.
* @discussion A unique callback ID will be automatically generated internally using UUID.
* @discussion Callback IDs are managed internally and cannot be accessed or modified by developers.
* @param queryString Query string for the request
* @param callback Block to be executed when this request completes
*/
- (void)addToQueueWithCallback:(NSString *)queryString callback:(CLYRequestCallback)callback;

@end
203 changes: 190 additions & 13 deletions CountlyConnectionManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ @interface CountlyConnectionManager ()

@property (nonatomic, strong) NSDate *startTime;
@property (nonatomic, assign) atomic_bool backoff;

@property (nonatomic, strong) NSMutableDictionary<NSString *, CLYRequestCallback> *internalRequestCallbacks;
@property (nonatomic, strong) NSMutableArray<CLYQueueFlushRunnable> *queueFlushRunnables;
@property (nonatomic) BOOL hasAnyRequestFailed;
@property (nonatomic, strong) dispatch_queue_t callbackQueue; // Serial queue for thread-safe callback/runnable access

@end

Expand Down Expand Up @@ -71,6 +74,7 @@ @interface CountlyConnectionManager ()
NSString* const kCountlyRCKeyABOptOut = @"ab_opt_out";
NSString* const kCountlyEndPointOverrideTag = @"&new_end_point=";
NSString* const kCountlyNewEndPoint = @"new_end_point";
NSString* const kCountlyCallbackID = @"callback_id";

CLYAttributionKey const CLYAttributionKeyIDFA = kCountlyQSKeyIDFA;
CLYAttributionKey const CLYAttributionKeyADID = kCountlyQSKeyADID;
Expand Down Expand Up @@ -105,6 +109,10 @@ - (instancetype)init
unsentSessionLength = 0.0;
isSessionStarted = NO;
atomic_init(&_backoff, NO);
_internalRequestCallbacks = [NSMutableDictionary dictionary];
_queueFlushRunnables = [NSMutableArray array];
_hasAnyRequestFailed = NO;
_callbackQueue = dispatch_queue_create("ly.count.callbackQueue", DISPATCH_QUEUE_SERIAL);
}

return self;
Expand All @@ -120,6 +128,11 @@ - (void)resetInstance {
onceToken = 0;
s_sharedInstance = nil;
isSessionStarted = NO;
dispatch_sync(_callbackQueue, ^{
[self->_internalRequestCallbacks removeAllObjects];
[self->_queueFlushRunnables removeAllObjects];
});
_hasAnyRequestFailed = NO;
}

- (void)setHost:(NSString *)host
Expand Down Expand Up @@ -213,17 +226,43 @@ - (void)proceedOnQueue

if (!self.startTime) {
self.startTime = [NSDate date]; // Record start time only when it's not already recorded
CLY_LOG_D(@"Proceeding on queue started, queued request count %lu", [CountlyPersistency.sharedInstance remainingRequestCount]);
self.hasAnyRequestFailed = NO; // Reset failure flag when starting queue processing
CLY_LOG_D(@"%s, Proceeding on queue started, queued request count %lu", __FUNCTION__, [CountlyPersistency.sharedInstance remainingRequestCount]);
}

NSString* firstItemInQueue = [CountlyPersistency.sharedInstance firstItemInQueue];
if (!firstItemInQueue)
{
// Calculate total time when the queue becomes empty
NSTimeInterval elapsedTime = -[self.startTime timeIntervalSinceNow];
CLY_LOG_D(@"Queue is empty. All requests are processed. Total time taken: %.2f seconds", elapsedTime);
// Reset start time for future queue processing
CLY_LOG_D(@"%s, Queue is empty. All requests are processed. Total time taken: %.2f seconds", __FUNCTION__, elapsedTime);

// Execute and clear runnables only if all requests succeeded
if (!self.hasAnyRequestFailed) {
// Thread-safe copy and clear of runnables
__block NSArray<CLYQueueFlushRunnable> *runnablesToExecute = nil;
dispatch_sync(_callbackQueue, ^{
if (self->_queueFlushRunnables.count > 0) {
CLY_LOG_D(@"%s, All requests succeeded. Executing %lu queue flush runnables.", __FUNCTION__, (unsigned long)self->_queueFlushRunnables.count);
runnablesToExecute = [self->_queueFlushRunnables copy];
[self->_queueFlushRunnables removeAllObjects];
}
});

// Execute runnables outside the lock to prevent deadlocks
if (runnablesToExecute) {
for (CLYQueueFlushRunnable runnable in runnablesToExecute) {
runnable();
}
CLY_LOG_D(@"%s, All queue flush runnables executed and removed.", __FUNCTION__);
}
} else {
CLY_LOG_D(@"%s, Some requests failed. Runnables will not be executed.", __FUNCTION__);
}

// Reset start time and failure flag for future queue processing
self.startTime = nil;
self.hasAnyRequestFailed = NO;
return;
}

Expand Down Expand Up @@ -252,11 +291,20 @@ - (void)proceedOnQueue
NSString* queryString = firstItemInQueue;
NSString* endPoint = kCountlyEndpointI;

NSString* overrideEndPoint = [self extractAndRemoveOverrideEndPoint:&queryString];
NSString* overrideEndPoint = [self extractAndRemoveParameter:&queryString parameter: kCountlyNewEndPoint];
if(overrideEndPoint) {
endPoint = overrideEndPoint;
}

NSString* callbackID = [self extractAndRemoveParameter:&queryString parameter: kCountlyCallbackID];
__block CLYRequestCallback requestCallback = nil;
if(callbackID){
dispatch_sync(_callbackQueue, ^{
requestCallback = self.internalRequestCallbacks[callbackID];
});
}


[CountlyCommon.sharedInstance startBackgroundTask];

queryString = [self appendRemainingRequest:queryString];
Expand Down Expand Up @@ -329,13 +377,24 @@ - (void)proceedOnQueue
{
CLY_LOG_D(@"Request <%p> successfully completed.", request);

if(requestCallback){
requestCallback([response description], YES);
// Clean up callback after execution
if (callbackID) {
dispatch_sync(self->_callbackQueue, ^{
[self.internalRequestCallbacks removeObjectForKey:callbackID];
});
}
}

[CountlyPersistency.sharedInstance removeFromQueue:firstItemInQueue];

[CountlyPersistency.sharedInstance saveToFile];

if(CountlyServerConfig.sharedInstance.backoffMechanism && [self backoff:duration queryString:queryString]){
CLY_LOG_D(@"%s, backed off dropping proceeding the queue", __FUNCTION__);
self.startTime = nil;
self.hasAnyRequestFailed = NO; // Reset on backoff
[self backoffCountdown];
} else {
[self proceedOnQueue];
Expand All @@ -345,6 +404,19 @@ - (void)proceedOnQueue
else
{
CLY_LOG_D(@"%s, request:[ <%p> ] failed! response:[ %@ ]", __FUNCTION__, request, [data cly_stringUTF8]);

self.hasAnyRequestFailed = YES; // Mark that a request has failed

if(requestCallback){
requestCallback([data cly_stringUTF8], NO);
// Clean up callback after execution
if (callbackID) {
dispatch_sync(self->_callbackQueue, ^{
[self.internalRequestCallbacks removeObjectForKey:callbackID];
});
}
}

[CountlyHealthTracker.sharedInstance logFailedNetworkRequestWithStatusCode:((NSHTTPURLResponse*)response).statusCode errorResponse: [data cly_stringUTF8]];
[CountlyHealthTracker.sharedInstance saveState];
self.startTime = nil;
Expand All @@ -353,6 +425,18 @@ - (void)proceedOnQueue
else
{
CLY_LOG_D(@"%s, request:[ <%p> ] failed! error:[ %@ ]", __FUNCTION__, request, error);

self.hasAnyRequestFailed = YES; // Mark that a request has failed

if(requestCallback){
requestCallback([error description], NO);
// Clean up callback after execution
if (callbackID) {
dispatch_sync(self->_callbackQueue, ^{
[self.internalRequestCallbacks removeObjectForKey:callbackID];
});
}
}
#if (TARGET_OS_WATCH)
[CountlyPersistency.sharedInstance saveToFile];
#endif
Expand Down Expand Up @@ -442,16 +526,21 @@ - (void)backoffCountdown
});
}


- (NSString*)extractAndRemoveOverrideEndPoint:(NSString **)queryString
- (NSString*)extractAndRemoveParameter:(NSString **)queryString parameter:(NSString*)parameter
{
if([*queryString containsString:kCountlyNewEndPoint]) {
NSString* overrideEndPoint = [*queryString cly_valueForQueryStringKey:kCountlyNewEndPoint];
if(overrideEndPoint) {
NSString* stringToRemove = [kCountlyEndPointOverrideTag stringByAppendingString:overrideEndPoint];
CLY_LOG_D(@"%s, Extracting parameter: %@", __FUNCTION__, parameter);

if([*queryString containsString:parameter]) {
NSString* parameterExtracted = [*queryString cly_valueForQueryStringKey:parameter];
if(parameterExtracted) {
NSString* stringToRemove = [NSString stringWithFormat:@"&%@=%@",parameter,parameterExtracted];
*queryString = [*queryString stringByReplacingOccurrencesOfString:stringToRemove withString:@""];
return overrideEndPoint;
CLY_LOG_D(@"%s, Parameter extracted successfully: %@ = %@", __FUNCTION__, parameter, parameterExtracted);
return parameterExtracted;
}
CLY_LOG_D(@"%s, Parameter found but value extraction failed for: %@", __FUNCTION__, parameter);
} else {
CLY_LOG_D(@"%s, Parameter not found in query string: %@", __FUNCTION__, parameter);
}
return nil;
}
Expand Down Expand Up @@ -1235,4 +1324,92 @@ - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticat
CFRelease(policy);
}

#pragma mark - Request Callbacks

- (void)registerRequestCallback:(NSString *)callbackID callback:(CLYRequestCallback)callback
{
if (!callbackID || callbackID.length == 0)
{
CLY_LOG_W(@"%s, Callback ID is nil or empty. Callback registration ignored.", __FUNCTION__);
return;
}

if (!callback)
{
CLY_LOG_W(@"%s, Callback block is nil. Callback registration ignored.", __FUNCTION__);
return;
}

dispatch_sync(_callbackQueue, ^{
CLY_LOG_D(@"%s, Registering request callback with ID: %@", __FUNCTION__, callbackID);
self.internalRequestCallbacks[callbackID] = callback;
});
}

- (void)removeRequestCallback:(NSString *)callbackID
{
if (!callbackID || callbackID.length == 0)
{
CLY_LOG_W(@"%s, Callback ID is nil or empty. Callback removal ignored.", __FUNCTION__);
return;
}

dispatch_sync(_callbackQueue, ^{
CLY_LOG_D(@"%s, Removing request callback with ID: %@", __FUNCTION__, callbackID);
[self.internalRequestCallbacks removeObjectForKey:callbackID];
});
}

- (void)addQueueFlushRunnable:(CLYQueueFlushRunnable)runnable
{
if (!runnable)
{
CLY_LOG_W(@"%s, Runnable is nil. Cannot add to queue flush runnables.", __FUNCTION__);
return;
}

CLYQueueFlushRunnable runnableCopy = [runnable copy];
dispatch_sync(_callbackQueue, ^{
CLY_LOG_D(@"%s, Adding queue flush runnable. Total count: %lu", __FUNCTION__, (unsigned long)(self->_queueFlushRunnables.count + 1));
[self->_queueFlushRunnables addObject:runnableCopy];
});
}

- (void)clearQueueFlushRunnables
{
dispatch_sync(_callbackQueue, ^{
CLY_LOG_D(@"%s, Clearing %lu queue flush runnables.", __FUNCTION__, (unsigned long)self->_queueFlushRunnables.count);
[self->_queueFlushRunnables removeAllObjects];
});
}

- (void)addToQueueWithCallback:(NSString *)queryString callback:(CLYRequestCallback)callback
{
if (!queryString || queryString.length == 0)
{
CLY_LOG_W(@"%s, Query string is nil or empty. Cannot add to queue with callback.", __FUNCTION__);
return;
}

if (!callback)
{
CLY_LOG_W(@"%s, Callback is nil. Adding request without callback.", __FUNCTION__);
[CountlyPersistency.sharedInstance addToQueue:queryString];
return;
}

// Generate a unique callback ID
NSString* callbackID = [[NSUUID UUID] UUIDString];
CLY_LOG_D(@"%s, Adding request to queue with callback ID: %@", __FUNCTION__, callbackID);

// Register the callback
[self registerRequestCallback:callbackID callback:callback];

// Append callback_id parameter to query string
NSString* queryStringWithCallback = [queryString stringByAppendingFormat:@"&callback_id=%@", callbackID];

// Add to queue
[CountlyPersistency.sharedInstance addToQueue:queryStringWithCallback];
}

@end
Loading
Loading