FIRIAMClearcutUploader.m 11 KB

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