FIRIAMClearcutUploader.m 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. /*
  2. * Copyright 2018 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import <FirebaseCore/FIRLogger.h>
  17. #import <UIKit/UIKit.h>
  18. #import "FIRCore+InAppMessaging.h"
  19. #import "FIRIAMClearcutUploader.h"
  20. #import "FIRIAMTimeFetcher.h"
  21. #import "FIRIAMClearcutHttpRequestSender.h"
  22. #import "FIRIAMClearcutLogStorage.h"
  23. // a macro for turning a millisecond value into seconds
  24. #define MILLS_TO_SECONDS(x) (((long)x) / 1000)
  25. @implementation FIRIAMClearcutStrategy
  26. - (instancetype)initWithMinWaitTimeInMills:(NSInteger)minWaitTimeInMills
  27. maxWaitTimeInMills:(NSInteger)maxWaitTimeInMills
  28. failureBackoffTimeInMills:(NSInteger)failureBackoffTimeInMills
  29. batchSendSize:(NSInteger)batchSendSize {
  30. if (self = [super init]) {
  31. _minimalWaitTimeInMills = minWaitTimeInMills;
  32. _maximumWaitTimeInMills = maxWaitTimeInMills;
  33. _failureBackoffTimeInMills = failureBackoffTimeInMills;
  34. _batchSendSize = batchSendSize;
  35. }
  36. return self;
  37. }
  38. - (NSString *)description {
  39. return [NSString stringWithFormat:@"min wait time in seconds:%ld;max wait time in seconds:%ld;"
  40. "failure backoff time in seconds:%ld;batch send size:%d",
  41. MILLS_TO_SECONDS(self.minimalWaitTimeInMills),
  42. MILLS_TO_SECONDS(self.maximumWaitTimeInMills),
  43. MILLS_TO_SECONDS(self.failureBackoffTimeInMills),
  44. (int)self.batchSendSize];
  45. }
  46. @end
  47. @interface FIRIAMClearcutUploader () {
  48. dispatch_queue_t _queue;
  49. BOOL _nextSendScheduled;
  50. }
  51. @property(readwrite, nonatomic) FIRIAMClearcutHttpRequestSender *requestSender;
  52. @property(nonatomic, assign) int64_t nextValidSendTimeInMills;
  53. @property(nonatomic, readonly) id<FIRIAMTimeFetcher> timeFetcher;
  54. @property(nonatomic, readonly) FIRIAMClearcutLogStorage *logStorage;
  55. @property(nonatomic, readonly) FIRIAMClearcutStrategy *strategy;
  56. @property(nonatomic, readonly) NSUserDefaults *userDefaults;
  57. @end
  58. static NSString *FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills =
  59. @"firebase-iam-next-clearcut-upload-timestamp-in-mills";
  60. /**
  61. * The high level behavior in this implementation is like this
  62. * 1 New records always pushed into FIRIAMClearcutLogStorage first.
  63. * 2 Upload log records in batches.
  64. * 3 If prior upload was successful, next upload would wait for the time parsed out of the
  65. * clearcut response body.
  66. * 4 If prior upload failed, next upload attempt would wait for failureBackoffTimeInMills defined
  67. * in strategy
  68. * 5 When app
  69. */
  70. @implementation FIRIAMClearcutUploader
  71. - (instancetype)initWithRequestSender:(FIRIAMClearcutHttpRequestSender *)requestSender
  72. timeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher
  73. logStorage:(FIRIAMClearcutLogStorage *)logStorage
  74. usingStrategy:(FIRIAMClearcutStrategy *)strategy
  75. usingUserDefaults:(nullable NSUserDefaults *)userDefaults {
  76. if (self = [super init]) {
  77. _nextSendScheduled = NO;
  78. _timeFetcher = timeFetcher;
  79. _requestSender = requestSender;
  80. _logStorage = logStorage;
  81. _strategy = strategy;
  82. _queue = dispatch_queue_create("com.google.firebase.inappmessaging.clearcut_upload", NULL);
  83. [[NSNotificationCenter defaultCenter] addObserver:self
  84. selector:@selector(scheduleNextSendFromForeground:)
  85. name:UIApplicationWillEnterForegroundNotification
  86. object:nil];
  87. #if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
  88. if (@available(iOS 13.0, *)) {
  89. [[NSNotificationCenter defaultCenter] addObserver:self
  90. selector:@selector(scheduleNextSendFromForeground:)
  91. name:UISceneWillEnterForegroundNotification
  92. object:nil];
  93. }
  94. #endif // defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
  95. _userDefaults = userDefaults ? userDefaults : [NSUserDefaults standardUserDefaults];
  96. // it would be 0 if it does not exist, which is equvilent to saying that
  97. // you can send now
  98. _nextValidSendTimeInMills = (int64_t)
  99. [_userDefaults doubleForKey:FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills];
  100. NSArray<FIRIAMClearcutLogRecord *> *availableLogs =
  101. [logStorage popStillValidRecordsForUpTo:strategy.batchSendSize];
  102. if (availableLogs.count) {
  103. [self scheduleNextSend];
  104. }
  105. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260001",
  106. @"FIRIAMClearcutUploader created with strategy as %@", self.strategy);
  107. }
  108. return self;
  109. }
  110. - (void)dealloc {
  111. [[NSNotificationCenter defaultCenter] removeObserver:self];
  112. }
  113. - (void)scheduleNextSendFromForeground:(NSNotification *)notification {
  114. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260010",
  115. @"App foregrounded, FIRIAMClearcutUploader will seed next send");
  116. [self scheduleNextSend];
  117. }
  118. - (void)addNewLogRecord:(FIRIAMClearcutLogRecord *)record {
  119. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260002",
  120. @"New log record sent to clearcut uploader");
  121. [self.logStorage pushRecords:@[ record ]];
  122. [self scheduleNextSend];
  123. }
  124. - (void)attemptUploading {
  125. NSArray<FIRIAMClearcutLogRecord *> *availableLogs =
  126. [self.logStorage popStillValidRecordsForUpTo:self.strategy.batchSendSize];
  127. if (availableLogs.count > 0) {
  128. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260011", @"Deliver %d clearcut records",
  129. (int)availableLogs.count);
  130. [self.requestSender
  131. sendClearcutHttpRequestForLogs:availableLogs
  132. withCompletion:^(BOOL success, BOOL shouldRetryLogs,
  133. int64_t waitTimeInMills) {
  134. if (success) {
  135. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260003",
  136. @"Delivering %d clearcut records was successful",
  137. (int)availableLogs.count);
  138. // make sure the effective wait time is between two bounds
  139. // defined in strategy
  140. waitTimeInMills =
  141. MAX(self.strategy.minimalWaitTimeInMills, waitTimeInMills);
  142. waitTimeInMills =
  143. MIN(waitTimeInMills, self.strategy.maximumWaitTimeInMills);
  144. } else {
  145. // failed to deliver
  146. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260004",
  147. @"Failed to attempt the delivery of %d clearcut "
  148. @"records and should-retry for them is %@",
  149. (int)availableLogs.count, shouldRetryLogs ? @"YES" : @"NO");
  150. if (shouldRetryLogs) {
  151. /**
  152. * Note that there is a chance that the app crashes before we can
  153. * call pushRecords: on the logStorage below which means we lost
  154. * these log records permanently. This is a trade-off between handling
  155. * duplicate records on server side vs taking the risk of lossing
  156. * data. This implementation picks the latter.
  157. */
  158. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260007",
  159. @"Push failed log records back to storage");
  160. [self.logStorage pushRecords:availableLogs];
  161. }
  162. waitTimeInMills = (int64_t)self.strategy.failureBackoffTimeInMills;
  163. }
  164. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260005",
  165. @"Wait for at least %ld seconds before next upload attempt",
  166. MILLS_TO_SECONDS(waitTimeInMills));
  167. self.nextValidSendTimeInMills =
  168. (int64_t)[self.timeFetcher currentTimestampInSeconds] * 1000 +
  169. waitTimeInMills;
  170. // persisted so that it can be recovered next time the app runs
  171. [self.userDefaults
  172. setDouble:(double)self.nextValidSendTimeInMills
  173. forKey:
  174. FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills];
  175. @synchronized(self) {
  176. self->_nextSendScheduled = NO;
  177. }
  178. [self scheduleNextSend];
  179. }];
  180. } else {
  181. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260007", @"No clearcut records to be uploaded");
  182. @synchronized(self) {
  183. _nextSendScheduled = NO;
  184. }
  185. }
  186. }
  187. - (void)scheduleNextSend {
  188. @synchronized(self) {
  189. if (_nextSendScheduled) {
  190. return;
  191. }
  192. }
  193. int64_t delayTimeInMills =
  194. self.nextValidSendTimeInMills - (int64_t)[self.timeFetcher currentTimestampInSeconds] * 1000;
  195. if (delayTimeInMills <= 0) {
  196. delayTimeInMills = 0; // no need to delay since we can send now
  197. }
  198. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260006",
  199. @"Next upload attempt scheduled in %d seconds", (int)delayTimeInMills / 1000);
  200. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delayTimeInMills * (int64_t)NSEC_PER_MSEC),
  201. _queue, ^{
  202. [self attemptUploading];
  203. });
  204. @synchronized(self) {
  205. _nextSendScheduled = YES;
  206. }
  207. }
  208. @end