FIRIAMClearcutUploader.m 11 KB

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