New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add jitter and extra cache for background processes #366
Changes from 13 commits
7a1cd4e
2008fd6
249e032
ff4af1e
f6ca9b9
6c0baf1
b42704c
23660b2
268bb07
f301e32
b0f4a24
4dfd0ee
50240f6
9e3a02a
724a491
8680bce
3904ea4
15bbf4a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,15 +8,12 @@ | |
|
||
#import "RCDeviceCache.h" | ||
#import "RCDeviceCache+Protected.h" | ||
#import "RCLogUtils.h" | ||
#import "NSDictionary+RCExtensions.h" | ||
Comment on lines
-11
to
-12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unused imports |
||
|
||
@interface RCDeviceCache () | ||
|
||
@property (nonatomic) NSUserDefaults *userDefaults; | ||
@property (nonatomic) NSNotificationCenter *notificationCenter; | ||
@property (nonatomic, nonnull) RCInMemoryCachedObject<RCOfferings *> *offeringsCachedObject; | ||
@property (nonatomic, nullable) NSDate *purchaserInfoCachesLastUpdated; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We were storing this value in memory before. This doesn't work well for widgets, since the process for the widget will likely be killed before the next refresh, so it'll always refresh cache. |
||
|
||
@end | ||
|
||
|
@@ -26,11 +23,12 @@ @interface RCDeviceCache () | |
NSString *RCLegacyGeneratedAppUserDefaultsKey = RC_CACHE_KEY_PREFIX @".appUserID"; | ||
NSString *RCAppUserDefaultsKey = RC_CACHE_KEY_PREFIX @".appUserID.new"; | ||
NSString *RCPurchaserInfoAppUserDefaultsKeyBase = RC_CACHE_KEY_PREFIX @".purchaserInfo."; | ||
NSString *RCPurchaserInfoLastUpdatedKeyBase = RC_CACHE_KEY_PREFIX @".purchaserInfoLastUpdated."; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. new |
||
NSString *RCLegacySubscriberAttributesKeyBase = RC_CACHE_KEY_PREFIX @".subscriberAttributes."; | ||
NSString *RCSubscriberAttributesKey = RC_CACHE_KEY_PREFIX @".subscriberAttributes"; | ||
NSString *RCAttributionDataDefaultsKeyBase = RC_CACHE_KEY_PREFIX @".attribution."; | ||
#define CACHE_DURATION_IN_SECONDS 60 * 5 | ||
|
||
int cacheDurationInSecondsInForeground = 60 * 5; | ||
int cacheDurationInSecondsInBackground = 60 * 60 * 24; | ||
|
||
@implementation RCDeviceCache | ||
|
||
|
@@ -44,8 +42,7 @@ - (instancetype)initWith:(NSUserDefaults *)userDefaults | |
self = [super init]; | ||
if (self) { | ||
if (offeringsCachedObject == nil) { | ||
offeringsCachedObject = | ||
[[RCInMemoryCachedObject alloc] initWithCacheDurationInSeconds:CACHE_DURATION_IN_SECONDS]; | ||
offeringsCachedObject = [[RCInMemoryCachedObject alloc] init]; | ||
} | ||
self.offeringsCachedObject = offeringsCachedObject; | ||
|
||
|
@@ -106,7 +103,7 @@ - (void)clearCachesForAppUserID:(NSString *)oldAppUserID andSaveNewUserID:(NSStr | |
@synchronized (self) { | ||
[self.userDefaults removeObjectForKey:RCLegacyGeneratedAppUserDefaultsKey]; | ||
[self.userDefaults removeObjectForKey:[self purchaserInfoUserDefaultCacheKeyForAppUserID:oldAppUserID]]; | ||
[self clearPurchaserInfoCacheTimestamp]; | ||
[self clearPurchaserInfoCacheTimestampForAppUserID:oldAppUserID]; | ||
[self clearOfferingsCache]; | ||
|
||
[self deleteAttributesIfSyncedForAppUserID:oldAppUserID]; | ||
|
@@ -125,34 +122,55 @@ - (void)cachePurchaserInfo:(NSData *)data forAppUserID:(NSString *)appUserID { | |
@synchronized (self) { | ||
[self.userDefaults setObject:data | ||
forKey:[self purchaserInfoUserDefaultCacheKeyForAppUserID:appUserID]]; | ||
[self setPurchaserInfoCacheTimestampToNow]; | ||
[self setPurchaserInfoCacheTimestampToNowForAppUserID:appUserID]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this didn't used to be tied to an |
||
} | ||
} | ||
|
||
- (BOOL)isPurchaserInfoCacheStale { | ||
NSTimeInterval timeSinceLastCheck = -[self.purchaserInfoCachesLastUpdated timeIntervalSinceNow]; | ||
return !(self.purchaserInfoCachesLastUpdated != nil && timeSinceLastCheck < CACHE_DURATION_IN_SECONDS); | ||
- (BOOL)isPurchaserInfoCacheStaleForAppUserID:(NSString *)appUserID isAppBackgrounded:(BOOL)isAppBackgrounded { | ||
NSDate * _Nullable purchaserInfoCachesLastUpdated = [self purchaserInfoCachesLastUpdatedForAppUserID:appUserID]; | ||
NSTimeInterval timeSinceLastCheck = -[purchaserInfoCachesLastUpdated timeIntervalSinceNow]; | ||
int cacheDurationInSeconds = [self cacheDurationInSecondsWithIsAppBackgrounded:isAppBackgrounded]; | ||
return !(purchaserInfoCachesLastUpdated != nil && timeSinceLastCheck < cacheDurationInSeconds); | ||
} | ||
|
||
- (void)clearPurchaserInfoCacheTimestamp { | ||
self.purchaserInfoCachesLastUpdated = nil; | ||
- (int)cacheDurationInSecondsWithIsAppBackgrounded:(BOOL)isAppBackgrounded { | ||
return (isAppBackgrounded ? cacheDurationInSecondsInBackground | ||
: cacheDurationInSecondsInForeground); | ||
} | ||
|
||
- (void)clearPurchaserInfoCacheTimestampForAppUserID:(NSString *)appUserID { | ||
NSString *cacheKey = [self purchaserInfoLastUpdatedCacheKeyForAppUserID:appUserID]; | ||
[self.userDefaults removeObjectForKey:cacheKey]; | ||
} | ||
|
||
- (void)clearPurchaserInfoCacheForAppUserID:(NSString *)appUserID { | ||
@synchronized (self) { | ||
[self clearPurchaserInfoCacheTimestamp]; | ||
[self clearPurchaserInfoCacheTimestampForAppUserID:appUserID]; | ||
[self.userDefaults removeObjectForKey:[self purchaserInfoUserDefaultCacheKeyForAppUserID:appUserID]]; | ||
} | ||
} | ||
|
||
- (void)setPurchaserInfoCacheTimestampToNow { | ||
self.purchaserInfoCachesLastUpdated = [NSDate date]; | ||
- (void)setPurchaserInfoCacheTimestampToNowForAppUserID:(NSString *)appUserID { | ||
[self setPurchaserInfoCacheTimestamp:[NSDate date] forAppUserID:appUserID]; | ||
} | ||
|
||
- (void)setPurchaserInfoCacheTimestamp:(NSDate *)timestamp forAppUserID:(NSString *)appUserID { | ||
NSString *cacheKey = [self purchaserInfoLastUpdatedCacheKeyForAppUserID:appUserID]; | ||
[self.userDefaults setObject:timestamp forKey:cacheKey]; | ||
} | ||
|
||
- (nullable NSDate *)purchaserInfoCachesLastUpdatedForAppUserID:(NSString *)appUserID { | ||
return (NSDate*)[self.userDefaults objectForKey:[self purchaserInfoLastUpdatedCacheKeyForAppUserID:appUserID]]; | ||
} | ||
|
||
- (NSString *)purchaserInfoUserDefaultCacheKeyForAppUserID:(NSString *)appUserID { | ||
return [RCPurchaserInfoAppUserDefaultsKeyBase stringByAppendingString:appUserID]; | ||
} | ||
|
||
- (NSString *)purchaserInfoLastUpdatedCacheKeyForAppUserID:(NSString *)appUserID { | ||
return [RCPurchaserInfoLastUpdatedKeyBase stringByAppendingString:appUserID]; | ||
} | ||
|
||
#pragma mark - offerings | ||
|
||
- (nullable RCOfferings *)cachedOfferings { | ||
|
@@ -163,8 +181,9 @@ - (void)cacheOfferings:(RCOfferings *)offerings { | |
[self.offeringsCachedObject cacheInstance:offerings]; | ||
} | ||
|
||
- (BOOL)isOfferingsCacheStale { | ||
return self.offeringsCachedObject.isCacheStale; | ||
- (BOOL)isOfferingsCacheStaleWithIsAppBackgrounded:(BOOL)isAppBackgrounded { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. having a different cache might not be strictly necessary for offerings, really - it's unlikely that you'd need offerings while the app is running in the background. can't hurt, though. |
||
int cacheDurationInSeconds = [self cacheDurationInSecondsWithIsAppBackgrounded:isAppBackgrounded]; | ||
return [self.offeringsCachedObject isCacheStaleWithDurationInSeconds:cacheDurationInSeconds]; | ||
} | ||
|
||
- (void)clearOfferingsCacheTimestamp { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -299,7 +299,7 @@ - (instancetype)initWithAppUserID:(nullable NSString *)appUserID | |
|
||
[self.systemInfo isApplicationBackgroundedWithCompletion:^(BOOL isBackgrounded) { | ||
if (!isBackgrounded) { | ||
[self.operationDispatcher dispatchOnWorkerThread:^{ | ||
[self.operationDispatcher dispatchOnWorkerThreadWithRandomDelay:NO block:^{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think about keeping around There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, I'll separate into two methods. I'd started by doing that, then I moved to having the randomDelay be optional, then the optional randomDelay looked great in swift but ported very poorly to obj-c, so I killed the optionality. I'll go back to the start. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok, I was trying this and remembered why I ended up going with the param: it's so that callers don't have to choose between methods, since almost every time they pass in the value of So if there are two separate methods, you'd have to do |
||
[self updateAllCachesWithCompletionBlock:callDelegate]; | ||
}]; | ||
} else { | ||
|
@@ -421,18 +421,20 @@ - (void)resetWithCompletionBlock:(nullable RCReceivePurchaserInfoBlock)completio | |
} | ||
|
||
- (void)purchaserInfoWithCompletionBlock:(RCReceivePurchaserInfoBlock)completion { | ||
RCPurchaserInfo *infoFromCache = [self readPurchaserInfoFromCache]; | ||
if (infoFromCache) { | ||
RCDebugLog(@"Vending purchaserInfo from cache"); | ||
CALL_IF_SET_ON_MAIN_THREAD(completion, infoFromCache, nil); | ||
if ([self.deviceCache isPurchaserInfoCacheStale]) { | ||
RCDebugLog(@"Cache is stale, updating caches"); | ||
[self fetchAndCachePurchaserInfoWithCompletion:nil]; | ||
[self.systemInfo isApplicationBackgroundedWithCompletion:^(BOOL isAppBackgrounded) { | ||
RCPurchaserInfo *infoFromCache = [self readPurchaserInfoFromCache]; | ||
if (infoFromCache) { | ||
RCDebugLog(@"Vending purchaserInfo from cache"); | ||
CALL_IF_SET_ON_MAIN_THREAD(completion, infoFromCache, nil); | ||
if ([self.deviceCache isPurchaserInfoCacheStaleForAppUserID:self.appUserID isAppBackgrounded:isAppBackgrounded]) { | ||
RCDebugLog(@"Cache is stale, updating caches"); | ||
[self fetchAndCachePurchaserInfoWithCompletion:nil isAppBackgrounded:isAppBackgrounded]; | ||
} | ||
} else { | ||
RCDebugLog(@"No cached purchaser info, fetching"); | ||
[self fetchAndCachePurchaserInfoWithCompletion:completion isAppBackgrounded:isAppBackgrounded]; | ||
Comment on lines
+424
to
+435
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. really don't love making these even more complex, but I couldn't think of a way around it given that the isAppBackgrounded method is async. |
||
} | ||
} else { | ||
RCDebugLog(@"No cached purchaser info, fetching"); | ||
[self fetchAndCachePurchaserInfoWithCompletion:completion]; | ||
} | ||
}]; | ||
} | ||
|
||
#pragma mark Purchasing | ||
|
@@ -534,11 +536,12 @@ - (void) purchaseProduct:(SKProduct *)product | |
if (!self.finishTransactions) { | ||
RCDebugLog(@"makePurchase - Observer mode is active (finishTransactions is set to false) and makePurchase has been called. Are you sure you want to do this?"); | ||
} | ||
payment.applicationUsername = self.appUserID; | ||
NSString *appUserID = self.appUserID; | ||
payment.applicationUsername = appUserID; | ||
|
||
// This is to prevent the UIApplicationDidBecomeActive call from the purchase popup | ||
// from triggering a refresh. | ||
[self.deviceCache setPurchaserInfoCacheTimestampToNow]; | ||
[self.deviceCache setPurchaserInfoCacheTimestampToNowForAppUserID:appUserID]; | ||
[self.deviceCache setOfferingsCacheTimestampToNow]; | ||
|
||
if (presentedOfferingIdentifier) { | ||
|
@@ -821,14 +824,16 @@ - (void)sendCachedPurchaserInfoIfAvailable { | |
|
||
- (void)updateAllCachesIfNeeded { | ||
RCDebugLog(@"applicationDidBecomeActive"); | ||
if ([self.deviceCache isPurchaserInfoCacheStale]) { | ||
RCDebugLog(@"PurchaserInfo cache is stale, updating caches"); | ||
[self fetchAndCachePurchaserInfoWithCompletion:nil]; | ||
} | ||
if ([self.deviceCache isOfferingsCacheStale]) { | ||
RCDebugLog(@"Offerings cache is stale, updating caches"); | ||
[self updateOfferingsCache:nil]; | ||
} | ||
[self.systemInfo isApplicationBackgroundedWithCompletion:^(BOOL isAppBackgrounded) { | ||
if ([self.deviceCache isPurchaserInfoCacheStaleForAppUserID:self.appUserID isAppBackgrounded:isAppBackgrounded]) { | ||
RCDebugLog(@"PurchaserInfo cache is stale, updating caches"); | ||
[self fetchAndCachePurchaserInfoWithCompletion:nil isAppBackgrounded:isAppBackgrounded]; | ||
} | ||
if ([self.deviceCache isOfferingsCacheStaleWithIsAppBackgrounded:isAppBackgrounded]) { | ||
RCDebugLog(@"Offerings cache is stale, updating caches"); | ||
[self updateOfferingsCache:nil isAppBackgrounded:isAppBackgrounded]; | ||
} | ||
}]; | ||
Comment on lines
+827
to
+836
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would love to move all this stuff into a new class, but it feels too risky for this particular feature |
||
} | ||
|
||
- (RCPurchaserInfo *)readPurchaserInfoFromCache { | ||
|
@@ -863,25 +868,30 @@ - (void)cachePurchaserInfo:(RCPurchaserInfo *)info forAppUserID:(NSString *)appU | |
} | ||
|
||
- (void)updateAllCachesWithCompletionBlock:(nullable RCReceivePurchaserInfoBlock)completion { | ||
[self fetchAndCachePurchaserInfoWithCompletion:completion]; | ||
[self updateOfferingsCache:nil]; | ||
[self.systemInfo isApplicationBackgroundedWithCompletion:^(BOOL isAppBackgrounded) { | ||
[self fetchAndCachePurchaserInfoWithCompletion:completion isAppBackgrounded:isAppBackgrounded]; | ||
[self updateOfferingsCache:nil isAppBackgrounded:isAppBackgrounded]; | ||
}]; | ||
} | ||
|
||
- (void)fetchAndCachePurchaserInfoWithCompletion:(nullable RCReceivePurchaserInfoBlock)completion { | ||
[self.deviceCache setPurchaserInfoCacheTimestampToNow]; | ||
- (void)fetchAndCachePurchaserInfoWithCompletion:(nullable RCReceivePurchaserInfoBlock)completion | ||
isAppBackgrounded:(BOOL)isAppBackgrounded { | ||
NSString *appUserID = self.identityManager.currentAppUserID; | ||
[self.backend getSubscriberDataWithAppUserID:appUserID | ||
completion:^(RCPurchaserInfo * _Nullable info, | ||
NSError * _Nullable error) { | ||
if (error == nil) { | ||
[self cachePurchaserInfo:info forAppUserID:appUserID]; | ||
[self sendUpdatedPurchaserInfoToDelegateIfChanged:info]; | ||
} else { | ||
[self.deviceCache clearPurchaserInfoCacheTimestamp]; | ||
} | ||
|
||
CALL_IF_SET_ON_MAIN_THREAD(completion, info, error); | ||
}]; | ||
[self.deviceCache setPurchaserInfoCacheTimestampToNowForAppUserID:appUserID]; | ||
[self.operationDispatcher dispatchOnWorkerThreadWithRandomDelay:isAppBackgrounded block:^{ | ||
[self.backend getSubscriberDataWithAppUserID:appUserID | ||
completion:^(RCPurchaserInfo *_Nullable info, | ||
aboedo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
NSError *_Nullable error) { | ||
if (error == nil) { | ||
[self cachePurchaserInfo:info forAppUserID:appUserID]; | ||
[self sendUpdatedPurchaserInfoToDelegateIfChanged:info]; | ||
} else { | ||
[self.deviceCache clearPurchaserInfoCacheTimestampForAppUserID:appUserID]; | ||
} | ||
|
||
CALL_IF_SET_ON_MAIN_THREAD(completion, info, error); | ||
}]; | ||
}]; | ||
} | ||
|
||
- (void)performOnEachProductIdentifierInOfferings:(NSDictionary *)offeringsData | ||
|
@@ -894,31 +904,34 @@ - (void)performOnEachProductIdentifierInOfferings:(NSDictionary *)offeringsData | |
} | ||
|
||
- (void)offeringsWithCompletionBlock:(RCReceiveOfferingsBlock)completion { | ||
if (self.deviceCache.cachedOfferings) { | ||
RCDebugLog(@"Vending offerings from cache"); | ||
CALL_IF_SET_ON_MAIN_THREAD(completion, self.deviceCache.cachedOfferings, nil); | ||
if (self.deviceCache.isOfferingsCacheStale) { | ||
RCDebugLog(@"Offerings cache is stale, updating cache"); | ||
[self updateOfferingsCache:nil]; | ||
[self.systemInfo isApplicationBackgroundedWithCompletion:^(BOOL isAppBackgrounded) { | ||
if (self.deviceCache.cachedOfferings) { | ||
RCDebugLog(@"Vending offerings from cache"); | ||
CALL_IF_SET_ON_MAIN_THREAD(completion, self.deviceCache.cachedOfferings, nil); | ||
if ([self.deviceCache isOfferingsCacheStaleWithIsAppBackgrounded:isAppBackgrounded]) { | ||
RCDebugLog(@"Offerings cache is stale, updating cache"); | ||
[self updateOfferingsCache:nil isAppBackgrounded:isAppBackgrounded]; | ||
} | ||
} else { | ||
RCDebugLog(@"No cached offerings, fetching"); | ||
[self updateOfferingsCache:completion isAppBackgrounded:isAppBackgrounded]; | ||
} | ||
} else { | ||
RCDebugLog(@"No cached offerings, fetching"); | ||
[self updateOfferingsCache:completion]; | ||
} | ||
}]; | ||
} | ||
|
||
- (void)updateOfferingsCache:(nullable RCReceiveOfferingsBlock)completion { | ||
- (void)updateOfferingsCache:(nullable RCReceiveOfferingsBlock)completion isAppBackgrounded:(BOOL)isAppBackgrounded { | ||
[self.deviceCache setOfferingsCacheTimestampToNow]; | ||
__weak typeof(self) weakSelf = self; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I remember you added this a while ago. We don't need it anymore? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we needed it in the first place - I believe I made a mistake in thinking that there was a retention cycle there, but really, the block is used only here and the completion block isn't stored elsewhere, so as soon as the completion is executed, it dies and the block is released, then the instance is ready to be released. The case handled here is not trivial, though: if you call reset after calling offerings and the callback hasn't returned:
I think we should at some point actually audit this behavior and check out what happens after a reset with some of these completion blocks. As of right now, I'm not sure completion not returning is better or worse than it returning but potentially messing up if there's anything outdated in memory from the previous purchases instance. |
||
[self.backend getOfferingsForAppUserID:self.appUserID | ||
completion:^(NSDictionary *data, NSError *error) { | ||
__strong typeof(self) strongSelf = weakSelf; | ||
if (error != nil) { | ||
[strongSelf handleOfferingsUpdateError:error completion:completion]; | ||
return; | ||
} | ||
[strongSelf handleOfferingsBackendResultWithData:data completion:completion]; | ||
}]; | ||
[self.operationDispatcher dispatchOnWorkerThreadWithRandomDelay:isAppBackgrounded block:^{ | ||
[self.backend getOfferingsForAppUserID:self.appUserID | ||
completion:^(NSDictionary *data, NSError *error) { | ||
if (error != nil) { | ||
[self handleOfferingsUpdateError:error completion:completion]; | ||
return; | ||
} | ||
[self handleOfferingsBackendResultWithData:data completion:completion]; | ||
}]; | ||
}]; | ||
|
||
} | ||
|
||
- (void)handleOfferingsBackendResultWithData:(NSDictionary *)data completion:(RCReceiveOfferingsBlock)completion { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not a huge fan of how this API is turning out, it feels overly complicated. But I didn't want to perform a major refactor for this.