From 4d9cc40527b0374c41625f6099d6b5fe26a2d9be Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Wed, 5 Apr 2017 11:30:04 -0700 Subject: [PATCH] Wrap write transactions in background tasks on supported platforms This asks the OS to not suspend the app while a write is in progress, which can help prevent apps which share realm files between processes from getting "stuck" due to a suspended app holding the write lock. --- CHANGELOG.md | 3 ++ Realm/RLMRealm.mm | 107 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 102 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19c4fa886f..e18ac9909f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ x.x.x Release notes (yyyy-MM-dd) * Add a `{RLM}SyncUser.isAdmin` property indicating whether a user is a Realm Object Server administrator. +* Write transactions are automatically marked as background tasks on platforms + which support them to avoid having an app suspended while it holds the write + lock. ### Bugfixes diff --git a/Realm/RLMRealm.mm b/Realm/RLMRealm.mm index 952f43a3a9..7919f5af83 100644 --- a/Realm/RLMRealm.mm +++ b/Realm/RLMRealm.mm @@ -46,6 +46,10 @@ #include #include +#if REALM_IOS +#import +#endif + using namespace realm; using util::File; @@ -120,12 +124,19 @@ static bool shouldForciblyDisableEncryption() { @implementation RLMRealm { NSHashTable *_collectionEnumerators; bool _sendingNotifications; + + // Needs to be atomic because the timeout callback is called on the main + // thread and not the RLMRealm's thread, so we could try to end the task + // from multiple threads at once + std::atomic _backgroundTaskIdentifier; + bool _addedBackgroundNotification; } + (BOOL)isCoreDebug { return realm::Version::has_feature(realm::feature_Debug); } +id g_sharedApplication; + (void)initialize { static bool initialized; if (initialized) { @@ -133,10 +144,78 @@ + (void)initialize { } initialized = true; +#if REALM_IOS + // Using NSClassFromString rather than directly referencing UIApplication + // avoids the need to link UIKit.framework, and we can't actually write + // `[UIApplication sharedApplication]` due to compiling as extension-safe + g_sharedApplication = [NSClassFromString(@"UIApplication") sharedApplication]; + + // Background tasks aren't supported if we're running inside an extension + if (![g_sharedApplication respondsToSelector:@selector(beginBackgroundTaskWithExpirationHandler:)]) { + g_sharedApplication = nil; + } +#endif + RLMCheckForUpdates(); RLMSendAnalytics(); } +#if REALM_IOS +// On iOS we want to wrap our write transactions in background tasks so that the +// OS doesn't suspend the app while we hold the write lock, as that will block +// any other apps in the same app group from using the Realm. Doing this +// unconditionally is extremely slow, so instead we only do it if we're already +// in the background, and just register for the background state transition +// notification otherwise +- (void)beginBackgroundTask { + if (!g_sharedApplication) { + return; + } + + if ([g_sharedApplication applicationState] == UIApplicationStateBackground) { + [self applicationDidEnterBackground:nil]; + return; + } + + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(applicationDidEnterBackground:) + name:@"UIApplicationDidEnterBackground" + object:nil]; + _addedBackgroundNotification = true; +} + +- (void)applicationDidEnterBackground:(NSNotification *)notification { + if (notification && !self.inWriteTransaction) { + // No need for a task if we got the notification when not actually in + // a write transaction + return; + } + + // The block needs to not retain `self` or we'll leak any Realms which the + // user fails to commit/cancel + __weak RLMRealm *weakSelf = self; + _backgroundTaskIdentifier = [g_sharedApplication beginBackgroundTaskWithExpirationHandler:^{ + // If we time out just end the task and let the OS suspend us rather + // than terminating + if (auto self = weakSelf) { + [self endBackgroundTask]; + } + }]; +} + +- (void)endBackgroundTask { + if (auto identifier = _backgroundTaskIdentifier.exchange(0)) { + [g_sharedApplication endBackgroundTask:identifier]; + } +} +#else +- (void)beginBackgroundTask { +} + +- (void)endBackgroundTask { +} +#endif // REALM_IOS + - (instancetype)initPrivate { self = [super init]; return self; @@ -450,10 +529,12 @@ - (RLMRealmConfiguration *)configuration { } - (void)beginWriteTransaction { + [self beginBackgroundTask]; try { _realm->begin_transaction(); } catch (std::exception &ex) { + [self endBackgroundTask]; @throw RLMException(ex); } } @@ -465,15 +546,18 @@ - (void)commitWriteTransaction { - (BOOL)commitWriteTransaction:(NSError **)outError { try { _realm->commit_transaction(); + [self endBackgroundTask]; return YES; } catch (...) { + [self endBackgroundTask]; RLMRealmTranslateException(outError); return NO; } } -- (BOOL)commitWriteTransactionWithoutNotifying:(NSArray *)tokens error:(NSError **)error { +- (BOOL)commitWriteTransactionWithoutNotifying:(NSArray *)tokens + error:(NSError **)error { for (RLMNotificationToken *token in tokens) { if (token.realm != self) { @throw RLMException(@"Incorrect Realm: only notifications for the Realm being modified can be skipped."); @@ -483,9 +567,11 @@ - (BOOL)commitWriteTransactionWithoutNotifying:(NSArray try { _realm->commit_transaction(); + [self endBackgroundTask]; return YES; } catch (...) { + [self endBackgroundTask]; RLMRealmTranslateException(error); return NO; } @@ -507,6 +593,7 @@ - (BOOL)transactionWithBlock:(void(^)(void))block error:(NSError **)outError { - (void)cancelWriteTransaction { try { _realm->cancel_transaction(); + [self endBackgroundTask]; } catch (std::exception &ex) { @throw RLMException(ex); @@ -528,6 +615,7 @@ - (void)invalidate { } _realm->invalidate(); + [self endBackgroundTask]; for (auto& objectInfo : _info) { for (RLMObservationInfo *info : objectInfo.second.observedObjects) { @@ -579,13 +667,16 @@ - (BOOL)compact { } - (void)dealloc { - if (_realm) { - if (_realm->is_in_transaction()) { - [self cancelWriteTransaction]; - NSLog(@"WARNING: An RLMRealm instance was deallocated during a write transaction and all " - "pending changes have been rolled back. Make sure to retain a reference to the " - "RLMRealm for the duration of the write transaction."); - } + if (_realm && _realm->is_in_transaction()) { + [self cancelWriteTransaction]; + NSLog(@"WARNING: An RLMRealm instance was deallocated during a write transaction and all " + "pending changes have been rolled back. Make sure to retain a reference to the " + "RLMRealm for the duration of the write transaction."); + } + if (_addedBackgroundNotification) { + [NSNotificationCenter.defaultCenter removeObserver:self + name:@"UIApplicationDidEnterBackground" + object:nil]; } }