FIRIAMClearcutUploader.m 9.8 KB

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