FIRIAMClearcutUploader.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  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 || (defined(TARGET_OS_VISION) && 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 defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
  91. if (@available(iOS 13.0, tvOS 13.0, *)) {
  92. [[NSNotificationCenter defaultCenter] addObserver:self
  93. selector:@selector(scheduleNextSendFromForeground:)
  94. name:UISceneWillEnterForegroundNotification
  95. object:nil];
  96. }
  97. #endif // defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
  98. _userDefaults = userDefaults ? userDefaults : [GULUserDefaults standardUserDefaults];
  99. // it would be 0 if it does not exist, which is equvilent to saying that
  100. // you can send now
  101. _nextValidSendTimeInMills = (int64_t)
  102. [_userDefaults doubleForKey:FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills];
  103. NSArray<FIRIAMClearcutLogRecord *> *availableLogs =
  104. [logStorage popStillValidRecordsForUpTo:strategy.batchSendSize];
  105. if (availableLogs.count) {
  106. [self scheduleNextSend];
  107. }
  108. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260001",
  109. @"FIRIAMClearcutUploader created with strategy as %@", self.strategy);
  110. }
  111. return self;
  112. }
  113. - (void)dealloc {
  114. [[NSNotificationCenter defaultCenter] removeObserver:self];
  115. }
  116. - (void)scheduleNextSendFromForeground:(NSNotification *)notification {
  117. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260010",
  118. @"App foregrounded, FIRIAMClearcutUploader will seed next send");
  119. [self scheduleNextSend];
  120. }
  121. - (void)addNewLogRecord:(FIRIAMClearcutLogRecord *)record {
  122. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260002",
  123. @"New log record sent to clearcut uploader");
  124. [self.logStorage pushRecords:@[ record ]];
  125. [self scheduleNextSend];
  126. }
  127. - (void)attemptUploading {
  128. NSArray<FIRIAMClearcutLogRecord *> *availableLogs =
  129. [self.logStorage popStillValidRecordsForUpTo:self.strategy.batchSendSize];
  130. if (availableLogs.count > 0) {
  131. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260011", @"Deliver %d clearcut records",
  132. (int)availableLogs.count);
  133. [self.requestSender
  134. sendClearcutHttpRequestForLogs:availableLogs
  135. withCompletion:^(BOOL success, BOOL shouldRetryLogs,
  136. int64_t waitTimeInMills) {
  137. if (success) {
  138. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260003",
  139. @"Delivering %d clearcut records was successful",
  140. (int)availableLogs.count);
  141. // make sure the effective wait time is between two bounds
  142. // defined in strategy
  143. waitTimeInMills =
  144. MAX(self.strategy.minimalWaitTimeInMills, waitTimeInMills);
  145. waitTimeInMills =
  146. MIN(waitTimeInMills, self.strategy.maximumWaitTimeInMills);
  147. } else {
  148. // failed to deliver
  149. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260004",
  150. @"Failed to attempt the delivery of %d clearcut "
  151. @"records and should-retry for them is %@",
  152. (int)availableLogs.count, shouldRetryLogs ? @"YES" : @"NO");
  153. if (shouldRetryLogs) {
  154. /**
  155. * Note that there is a chance that the app crashes before we can
  156. * call pushRecords: on the logStorage below which means we lost
  157. * these log records permanently. This is a trade-off between handling
  158. * duplicate records on server side vs taking the risk of lossing
  159. * data. This implementation picks the latter.
  160. */
  161. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260007",
  162. @"Push failed log records back to storage");
  163. [self.logStorage pushRecords:availableLogs];
  164. }
  165. waitTimeInMills = (int64_t)self.strategy.failureBackoffTimeInMills;
  166. }
  167. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260005",
  168. @"Wait for at least %ld seconds before next upload attempt",
  169. MILLS_TO_SECONDS(waitTimeInMills));
  170. self.nextValidSendTimeInMills =
  171. (int64_t)[self.timeFetcher currentTimestampInSeconds] * 1000 +
  172. waitTimeInMills;
  173. // persisted so that it can be recovered next time the app runs
  174. [self.userDefaults
  175. setDouble:(double)self.nextValidSendTimeInMills
  176. forKey:
  177. FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills];
  178. @synchronized(self) {
  179. self->_nextSendScheduled = NO;
  180. }
  181. [self scheduleNextSend];
  182. }];
  183. } else {
  184. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260007", @"No clearcut records to be uploaded");
  185. @synchronized(self) {
  186. _nextSendScheduled = NO;
  187. }
  188. }
  189. }
  190. - (void)scheduleNextSend {
  191. @synchronized(self) {
  192. if (_nextSendScheduled) {
  193. return;
  194. }
  195. }
  196. int64_t delayTimeInMills =
  197. self.nextValidSendTimeInMills - (int64_t)[self.timeFetcher currentTimestampInSeconds] * 1000;
  198. if (delayTimeInMills <= 0) {
  199. delayTimeInMills = 0; // no need to delay since we can send now
  200. }
  201. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260006",
  202. @"Next upload attempt scheduled in %d seconds", (int)delayTimeInMills / 1000);
  203. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delayTimeInMills * (int64_t)NSEC_PER_MSEC),
  204. _queue, ^{
  205. [self attemptUploading];
  206. });
  207. @synchronized(self) {
  208. _nextSendScheduled = YES;
  209. }
  210. }
  211. @end
  212. #endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION)