Skip to content
This repository was archived by the owner on Feb 19, 2020. It is now read-only.

Commit 3a3f1ad

Browse files
author
Benjamin Scholtysik (Reimold)
authored
Merge pull request #471 from bitstadium/fix/MetricsManager-trackevent-threadsafe
Fix/metrics manager trackevent threadsafe
2 parents c9cf976 + 42c86c6 commit 3a3f1ad

File tree

5 files changed

+152
-85
lines changed

5 files changed

+152
-85
lines changed

Classes/BITChannel.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
#import "HockeySDKNullability.h"
1212
NS_ASSUME_NONNULL_BEGIN
1313

14-
FOUNDATION_EXPORT char *BITSafeJsonEventsString;
14+
/**
15+
* Buffer of telemtry events, will be written to disk. Make sure the buffer is used in a threadsafe way.
16+
*/
17+
FOUNDATION_EXPORT char *_Nullable BITTelemetryEventBuffer;
1518

1619
/**
1720
* Items get queued before they are persisted and sent out as a batch. This class managed the queue, and forwards the batch

Classes/BITChannel.m

Lines changed: 116 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#if HOCKEYSDK_FEATURE_METRICS
44

55
#import "HockeySDKPrivate.h"
6+
#import "BITHockeyManager.h"
67
#import "BITChannelPrivate.h"
78
#import "BITHockeyHelper.h"
89
#import "BITTelemetryContext.h"
@@ -12,9 +13,11 @@
1213
#import "BITData.h"
1314
#import "BITDevice.h"
1415
#import "BITPersistencePrivate.h"
16+
#import <libkern/OSAtomic.h>
17+
1518

1619
static char *const BITDataItemsOperationsQueue = "net.hockeyapp.senderQueue";
17-
char *BITSafeJsonEventsString;
20+
char *BITTelemetryEventBuffer;
1821

1922
NSString *const BITChannelBlockedNotification = @"BITChannelBlockedNotification";
2023

@@ -42,7 +45,7 @@ @implementation BITChannel
4245

4346
- (instancetype)init {
4447
if ((self = [super init])) {
45-
bit_resetSafeJsonStream(&BITSafeJsonEventsString);
48+
bit_resetEventBuffer(&BITTelemetryEventBuffer);
4649
_dataItemCount = 0;
4750
if (bit_isDebuggerAttached()) {
4851
_maxBatchSize = BITDebugMaxBatchSize;
@@ -80,7 +83,6 @@ - (void) registerObservers {
8083
void (^notificationBlock)(NSNotification *note) = ^(NSNotification __unused *note) {
8184
typeof(self) strongSelf = weakSelf;
8285
if ([strongSelf timerIsRunning]) {
83-
[strongSelf persistDataItemQueue];
8486

8587
/**
8688
* From the documentation for applicationDidEnterBackground:
@@ -93,12 +95,15 @@ - (void) registerObservers {
9395
[sharedApplication endBackgroundTask:_backgroundTask];
9496
_backgroundTask = UIBackgroundTaskInvalid;
9597
}];
98+
99+
// Do background work that will be done in it's own async queue.
100+
[strongSelf persistDataItemQueue:&BITTelemetryEventBuffer];
96101
}
97102
};
98103
self.appDidEnterBackgroundObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification
99-
object:nil
100-
queue:NSOperationQueue.mainQueue
101-
usingBlock:notificationBlock];
104+
object:nil
105+
queue:NSOperationQueue.mainQueue
106+
usingBlock:notificationBlock];
102107
}
103108
}
104109

@@ -123,59 +128,89 @@ - (BOOL)isQueueBusy {
123128
return self.channelBlocked;
124129
}
125130

126-
- (void)persistDataItemQueue {
131+
- (void)persistDataItemQueue:(char **)eventBuffer {
127132
[self invalidateTimer];
128-
if (!BITSafeJsonEventsString || strlen(BITSafeJsonEventsString) == 0) {
133+
134+
// Make sure string (which points to BITTelemetryEventBuffer) is not changed.
135+
char *previousBuffer = NULL;
136+
char *newEmptyString = NULL;
137+
do {
138+
newEmptyString = strdup("");
139+
previousBuffer = *eventBuffer;
140+
141+
// This swaps pointers and makes sure eventBuffer now has the balue of newEmptyString.
142+
if (OSAtomicCompareAndSwapPtr(previousBuffer, newEmptyString, (void*)eventBuffer)) {
143+
@synchronized(self) {
144+
self.dataItemCount = 0;
145+
}
146+
break;
147+
}
148+
} while(true);
149+
150+
// Nothing to persist, freeing memory and existing.
151+
if (!previousBuffer || strlen(previousBuffer) == 0) {
152+
free(previousBuffer);
129153
return;
130154
}
131155

132-
NSData *bundle = [NSData dataWithBytes:BITSafeJsonEventsString length:strlen(BITSafeJsonEventsString)];
156+
// Persist the data
157+
NSData *bundle = [NSData dataWithBytes:previousBuffer length:strlen(previousBuffer)];
133158
[self.persistence persistBundle:bundle];
159+
free(previousBuffer);
134160

135161
// Reset both, the async-signal-safe and item counter.
136162
[self resetQueue];
137163
}
138164

165+
// Resets the event buffer and count of events in the queue.
139166
- (void)resetQueue {
140-
bit_resetSafeJsonStream(&BITSafeJsonEventsString);
141-
self.dataItemCount = 0;
167+
@synchronized (self) {
168+
bit_resetEventBuffer(&BITTelemetryEventBuffer);
169+
self.dataItemCount = 0;
170+
}
142171
}
143172

144173
#pragma mark - Adding to queue
145174

146175
- (void)enqueueTelemetryItem:(BITTelemetryData *)item {
147176

148177
if (!item) {
149-
// Case 1: Item is nil: Do not enqueue item and abort operation
178+
179+
// Item is nil: Do not enqueue item and abort operation.
150180
BITHockeyLogWarning(@"WARNING: TelemetryItem was nil.");
151181
return;
152182
}
153183

184+
// First assigning self to weakSelf and then assigning this to strongSelf in the block is not very intuitive, this
185+
// blog post explains it very well: https://dhoerl.wordpress.com/2013/04/23/i-finally-figured-out-weakself-and-strongself/
154186
__weak typeof(self) weakSelf = self;
155187
dispatch_async(self.dataItemsOperations, ^{
156-
typeof(self) strongSelf = weakSelf;
157188

189+
typeof(self) strongSelf = weakSelf;
158190
if (strongSelf.isQueueBusy) {
159-
// Case 2: Channel is in blocked state: Trigger sender, start timer to check after again after a while and abort operation.
191+
192+
// Case 1: Channel is in blocked state: Trigger sender, start timer to check after again after a while and abort operation.
160193
BITHockeyLogDebug(@"INFO: The channel is saturated. %@ was dropped.", item.debugDescription);
161194
if (![strongSelf timerIsRunning]) {
162195
[strongSelf startTimer];
163196
}
164197
return;
165198
}
166199

167-
// Enqueue item
168-
NSDictionary *dict = [self dictionaryForTelemetryData:item];
169-
[strongSelf appendDictionaryToJsonStream:dict];
170-
171-
if (strongSelf.dataItemCount >= self.maxBatchSize) {
172-
// Case 3: Max batch count has been reached, so write queue to disk and delete all items.
173-
[strongSelf persistDataItemQueue];
174-
175-
} else if (strongSelf.dataItemCount == 1) {
176-
// Case 4: It is the first item, let's start the timer.
177-
if (![strongSelf timerIsRunning]) {
178-
[strongSelf startTimer];
200+
// Enqueue item.
201+
@synchronized(self) {
202+
NSDictionary *dict = [strongSelf dictionaryForTelemetryData:item];
203+
[strongSelf appendDictionaryToEventBuffer:dict];
204+
if (strongSelf.dataItemCount >= strongSelf.maxBatchSize) {
205+
206+
// Case 2: Max batch count has been reached, so write queue to disk and delete all items.
207+
[strongSelf persistDataItemQueue:&BITTelemetryEventBuffer];
208+
} else if (strongSelf.dataItemCount > 0) {
209+
210+
// Case 3: It is the first item, let's start the timer.
211+
if (![strongSelf timerIsRunning]) {
212+
[strongSelf startTimer];
213+
}
179214
}
180215
}
181216
});
@@ -223,39 +258,72 @@ - (NSString *)serializeDictionaryToJSONString:(NSDictionary *)dictionary {
223258

224259
#pragma mark JSON Stream
225260

226-
- (void)appendDictionaryToJsonStream:(NSDictionary *)dictionary {
261+
- (void)appendDictionaryToEventBuffer:(NSDictionary *)dictionary {
227262
if (dictionary) {
228263
NSString *string = [self serializeDictionaryToJSONString:dictionary];
229264

230265
// Since we can't persist every event right away, we write it to a simple C string.
231266
// This can then be written to disk by a signal handler in case of a crash.
232-
bit_appendStringToSafeJsonStream(string, &(BITSafeJsonEventsString));
233-
self.dataItemCount += 1;
267+
@synchronized (self) {
268+
bit_appendStringToEventBuffer(string, &BITTelemetryEventBuffer);
269+
self.dataItemCount += 1;
270+
}
234271
}
235272
}
236273

237-
void bit_appendStringToSafeJsonStream(NSString *string, char **jsonString) {
238-
if (jsonString == NULL) { return; }
274+
void bit_appendStringToEventBuffer(NSString *string, char **eventBuffer) {
275+
if (eventBuffer == NULL) {
276+
return;
277+
}
239278

240-
if (!string) { return; }
279+
if (!string) {
280+
return;
281+
}
241282

242-
if (*jsonString == NULL || strlen(*jsonString) == 0) {
243-
bit_resetSafeJsonStream(jsonString);
283+
if (*eventBuffer == NULL || strlen(*eventBuffer) == 0) {
284+
bit_resetEventBuffer(eventBuffer);
244285
}
245286

246-
if (string.length == 0) { return; }
287+
if (string.length == 0) {
288+
return;
289+
}
247290

248-
char *new_string = NULL;
249-
// Concatenate old string with new JSON string and add a comma.
250-
asprintf(&new_string, "%s%.*s\n", *jsonString, (int)MIN(string.length, (NSUInteger)INT_MAX), string.UTF8String);
251-
free(*jsonString);
252-
*jsonString = new_string;
291+
do {
292+
char *newBuffer = NULL;
293+
char *previousBuffer = *eventBuffer;
294+
295+
// Concatenate old string with new JSON string and add a comma.
296+
asprintf(&newBuffer, "%s%.*s\n", previousBuffer, (int)MIN(string.length, (NSUInteger)INT_MAX), string.UTF8String);
297+
298+
// Compare newBuffer and previousBuffer. If they point to the same address, we are safe to use them.
299+
if (OSAtomicCompareAndSwapPtr(previousBuffer, newBuffer, (void*)eventBuffer)) {
300+
301+
// Free the intermediate pointer.
302+
free(previousBuffer);
303+
return;
304+
} else {
305+
306+
// newBuffer has been changed by another thread.
307+
free(newBuffer);
308+
}
309+
} while (true);
253310
}
254311

255-
void bit_resetSafeJsonStream(char **string) {
256-
if (!string) { return; }
257-
free(*string);
258-
*string = strdup("");
312+
void bit_resetEventBuffer(char **eventBuffer) {
313+
if (!eventBuffer) { return; }
314+
315+
char *newEmptyString = NULL;
316+
char *prevString = NULL;
317+
do {
318+
prevString = *eventBuffer;
319+
newEmptyString = strdup("");
320+
321+
// Compare pointers to strings to make sure we are still threadsafe!
322+
if (OSAtomicCompareAndSwapPtr(prevString, newEmptyString, (void*)eventBuffer)) {
323+
free(prevString);
324+
return;
325+
}
326+
} while(true);
259327
}
260328

261329
#pragma mark - Batching
@@ -279,28 +347,26 @@ -(BOOL)timerIsRunning {
279347
}
280348

281349
- (void)startTimer {
282-
// Reset timer, if it is already running
350+
351+
// Reset timer, if it is already running.
283352
if ([self timerIsRunning]) {
284353
[self invalidateTimer];
285354
}
286355

287356
self.timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.dataItemsOperations);
288357
dispatch_source_set_timer((dispatch_source_t)self.timerSource, dispatch_walltime(NULL, NSEC_PER_SEC * self.batchInterval), 1ull * NSEC_PER_SEC, 1ull * NSEC_PER_SEC);
289-
290358
__weak typeof(self) weakSelf = self;
291359
dispatch_source_set_event_handler((dispatch_source_t)self.timerSource, ^{
292360
typeof(self) strongSelf = weakSelf;
293-
294-
if(strongSelf) {
361+
if (strongSelf) {
295362
if (strongSelf.dataItemCount > 0) {
296-
[strongSelf persistDataItemQueue];
363+
[strongSelf persistDataItemQueue:&BITTelemetryEventBuffer];
297364
} else {
298365
strongSelf.channelBlocked = NO;
299366
}
300367
[strongSelf invalidateTimer];
301368
}
302369
});
303-
304370
dispatch_resume((dispatch_source_t)self.timerSource);
305371
}
306372

Classes/BITChannelPrivate.h

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,28 +70,28 @@ FOUNDATION_EXPORT NSString *const BITChannelBlockedNotification;
7070
/**
7171
* Manually trigger the BITChannel to persist all items currently in its data item queue.
7272
*/
73-
- (void)persistDataItemQueue;
73+
- (void)persistDataItemQueue:(char *_Nullable*_Nullable)eventBuffer;
7474

7575
/**
7676
* Adds the specified dictionary to the JSON Stream string.
7777
*
7878
* @param dictionary the dictionary object which is to be added to the JSON Stream queue string.
7979
*/
80-
- (void)appendDictionaryToJsonStream:(NSDictionary *)dictionary;
80+
- (void)appendDictionaryToEventBuffer:(NSDictionary *)dictionary;
8181

8282
/**
8383
* A C function that serializes a given dictionary to JSON and appends it to a char string
8484
*
8585
* @param string The C string which the dictionary's JSON representation will be appended to.
8686
*/
87-
void bit_appendStringToSafeJsonStream(NSString *string, char *__nonnull*__nonnull jsonStream);
87+
void bit_appendStringToEventBuffer(NSString *string, char *__nonnull*__nonnull eventBuffer);
8888

8989
/**
90-
* Reset BITSafeJsonEventsString so we can start appending JSON dictionaries.
90+
* Reset the event buffer so we can start appending JSON dictionaries.
9191
*
92-
* @param jsonStream The string that will be reset.
92+
* @param eventBuffer The string that will be reset.
9393
*/
94-
void bit_resetSafeJsonStream(char *__nonnull*__nonnull jsonStream);
94+
void bit_resetEventBuffer(char *__nonnull*__nonnull eventBuffer);
9595

9696
/**
9797
* A method which indicates whether the telemetry pipeline is busy and no new data should be enqueued.

Classes/BITCrashManager.m

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107
static void bit_save_events_callback(siginfo_t __unused *info, ucontext_t __unused *uap, void __unused *context) {
108108

109109
// Do not flush metrics queue if queue is empty (metrics module disabled) to not freeze the app
110-
if (!BITSafeJsonEventsString) {
110+
if (!BITTelemetryEventBuffer) {
111111
return;
112112
}
113113

@@ -117,10 +117,10 @@ static void bit_save_events_callback(siginfo_t __unused *info, ucontext_t __unus
117117
return;
118118
}
119119

120-
size_t len = strlen(BITSafeJsonEventsString);
120+
size_t len = strlen(BITTelemetryEventBuffer);
121121
if (len > 0) {
122122
// Simply write the whole string to disk
123-
write(fd, BITSafeJsonEventsString, len);
123+
write(fd, BITTelemetryEventBuffer, len);
124124
}
125125
close(fd);
126126
}

0 commit comments

Comments
 (0)