From 0f96341059ba5e7848a0d388938566f537158e5b Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 15 Jun 2017 13:37:10 -0400 Subject: [PATCH] Avoid crashing on startup due to database view creation. * Substitute unread view for unseen view until unseen view is ready. * Register as many views as possible async. * Perform blocking, safe migrations before async registration of views. // FREEBIE --- src/Contacts/TSThread.m | 2 +- src/Devices/OWSReadReceiptsProcessor.m | 2 +- src/Messages/OWSBlockingManager.m | 1 + src/Storage/TSDatabaseSecondaryIndexes.m | 7 +- src/Storage/TSDatabaseView.h | 51 ++++-- src/Storage/TSDatabaseView.m | 190 ++++++++++++++++++----- src/Storage/TSStorageManager.h | 11 +- src/Storage/TSStorageManager.m | 21 ++- 8 files changed, 217 insertions(+), 68 deletions(-) diff --git a/src/Contacts/TSThread.m b/src/Contacts/TSThread.m index c0610d294..b1624eeb0 100644 --- a/src/Contacts/TSThread.m +++ b/src/Contacts/TSThread.m @@ -193,7 +193,7 @@ NS_ASSUME_NONNULL_BEGIN - (NSArray> *)unseenMessagesWithTransaction:(YapDatabaseReadTransaction *)transaction { NSMutableArray> *messages = [NSMutableArray new]; - [[transaction ext:TSUnseenDatabaseViewExtensionName] + [[TSDatabaseView unseenDatabaseViewExtension:transaction] enumerateRowsInGroup:self.uniqueId usingBlock:^( NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { diff --git a/src/Devices/OWSReadReceiptsProcessor.m b/src/Devices/OWSReadReceiptsProcessor.m index 81d2595ba..1d2f92d88 100644 --- a/src/Devices/OWSReadReceiptsProcessor.m +++ b/src/Devices/OWSReadReceiptsProcessor.m @@ -95,7 +95,7 @@ NSString *const OWSReadReceiptsProcessorMarkedMessageAsReadNotification = [interactionsToMarkAsRead addObject:message]; [self.storageManager.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [[transaction ext:TSUnseenDatabaseViewExtensionName] + [[TSDatabaseView unseenDatabaseViewExtension:transaction] enumerateRowsInGroup:message.uniqueThreadId usingBlock:^(NSString *collection, NSString *key, diff --git a/src/Messages/OWSBlockingManager.m b/src/Messages/OWSBlockingManager.m index b65ae98f4..ba9e61e70 100644 --- a/src/Messages/OWSBlockingManager.m +++ b/src/Messages/OWSBlockingManager.m @@ -11,6 +11,7 @@ NS_ASSUME_NONNULL_BEGIN NSString *const kNSNotificationName_BlockedPhoneNumbersDidChange = @"kNSNotificationName_BlockedPhoneNumbersDidChange"; + NSString *const kOWSBlockingManager_BlockedPhoneNumbersCollection = @"kOWSBlockingManager_BlockedPhoneNumbersCollection"; // This key is used to persist the current "blocked phone numbers" state. NSString *const kOWSBlockingManager_BlockedPhoneNumbersKey = @"kOWSBlockingManager_BlockedPhoneNumbersKey"; diff --git a/src/Storage/TSDatabaseSecondaryIndexes.m b/src/Storage/TSDatabaseSecondaryIndexes.m index 94dadf212..f63e63366 100644 --- a/src/Storage/TSDatabaseSecondaryIndexes.m +++ b/src/Storage/TSDatabaseSecondaryIndexes.m @@ -1,9 +1,5 @@ // -// TSDatabaseSecondaryIndexes.m -// Signal -// -// Created by Frederic Jacobs on 26/01/15. -// Copyright (c) 2015 Open Whisper Systems. All rights reserved. +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // #import "TSDatabaseSecondaryIndexes.h" @@ -43,4 +39,5 @@ YapDatabaseQuery *query = [YapDatabaseQuery queryWithFormat:formattedString]; [[transaction ext:@"idx"] enumerateKeysMatchingQuery:query usingBlock:block]; } + @end diff --git a/src/Storage/TSDatabaseView.h b/src/Storage/TSDatabaseView.h index 16a717642..2337bf9e5 100644 --- a/src/Storage/TSDatabaseView.h +++ b/src/Storage/TSDatabaseView.h @@ -5,36 +5,53 @@ #import #import +extern NSString *const kNSNotificationName_DatabaseViewRegistrationComplete; + +extern NSString *const TSInboxGroup; +extern NSString *const TSArchiveGroup; +extern NSString *const TSUnreadIncomingMessagesGroup; +extern NSString *const TSSecondaryDevicesGroup; + +extern NSString *const TSThreadDatabaseViewExtensionName; + +extern NSString *const TSMessageDatabaseViewExtensionName; +extern NSString *const TSUnreadDatabaseViewExtensionName; + +extern NSString *const TSSecondaryDevicesDatabaseViewExtensionName; + @interface TSDatabaseView : NSObject -extern NSString *TSInboxGroup; -extern NSString *TSArchiveGroup; -extern NSString *TSUnreadIncomingMessagesGroup; -extern NSString *TSSecondaryDevicesGroup; +- (instancetype)init NS_UNAVAILABLE; -extern NSString *TSThreadDatabaseViewExtensionName; -extern NSString *TSMessageDatabaseViewExtensionName; -extern NSString *TSThreadOutgoingMessageDatabaseViewExtensionName; -extern NSString *TSUnreadDatabaseViewExtensionName; -extern NSString *TSUnseenDatabaseViewExtensionName; -extern NSString *TSThreadSpecialMessagesDatabaseViewExtensionName; -extern NSString *TSSecondaryDevicesDatabaseViewExtensionName; ++ (BOOL)hasPendingViewRegistrations; -+ (BOOL)registerThreadDatabaseView; -+ (BOOL)registerThreadInteractionsDatabaseView; -+ (BOOL)registerThreadOutgoingMessagesDatabaseView; ++ (void)registerThreadDatabaseView; + ++ (void)registerThreadInteractionsDatabaseView; ++ (void)asyncRegisterThreadOutgoingMessagesDatabaseView; // Instances of OWSReadTracking for wasRead is NO and shouldAffectUnreadCounts is YES. // // Should be used for "unread message counts". -+ (BOOL)registerUnreadDatabaseView; ++ (void)registerUnreadDatabaseView; // Should be used for "unread indicator". // // Instances of OWSReadTracking for wasRead is NO. -+ (BOOL)registerUnseenDatabaseView; ++ (void)asyncRegisterUnseenDatabaseView; + ++ (void)asyncRegisterThreadSpecialMessagesDatabaseView; -+ (BOOL)registerThreadSpecialMessagesDatabaseView; + (void)asyncRegisterSecondaryDevicesDatabaseView; +// Returns the "unseen" database view if it is ready; +// otherwise it returns the "unread" database view. ++ (id)unseenDatabaseViewExtension:(YapDatabaseReadTransaction *)transaction; + +// NOTE: It is not safe to call this method until hasPendingViewRegistrations is YES. ++ (id)threadOutgoingMessageDatabaseView:(YapDatabaseReadTransaction *)transaction; + +// NOTE: It is not safe to call this method until hasPendingViewRegistrations is YES. ++ (id)threadSpecialMessagesDatabaseView:(YapDatabaseReadTransaction *)transaction; + @end diff --git a/src/Storage/TSDatabaseView.m b/src/Storage/TSDatabaseView.m index 6182e27ba..a5efe6435 100644 --- a/src/Storage/TSDatabaseView.m +++ b/src/Storage/TSDatabaseView.m @@ -12,32 +12,88 @@ #import "TSThread.h" #import -NSString *TSInboxGroup = @"TSInboxGroup"; -NSString *TSArchiveGroup = @"TSArchiveGroup"; +NSString *const kNSNotificationName_DatabaseViewRegistrationComplete = + @"kNSNotificationName_DatabaseViewRegistrationComplete"; -NSString *TSUnreadIncomingMessagesGroup = @"TSUnreadIncomingMessagesGroup"; -NSString *TSSecondaryDevicesGroup = @"TSSecondaryDevicesGroup"; +NSString *const TSInboxGroup = @"TSInboxGroup"; +NSString *const TSArchiveGroup = @"TSArchiveGroup"; -NSString *TSThreadDatabaseViewExtensionName = @"TSThreadDatabaseViewExtensionName"; -NSString *TSMessageDatabaseViewExtensionName = @"TSMessageDatabaseViewExtensionName"; -NSString *TSThreadOutgoingMessageDatabaseViewExtensionName = @"TSThreadOutgoingMessageDatabaseViewExtensionName"; -NSString *TSUnreadDatabaseViewExtensionName = @"TSUnreadDatabaseViewExtensionName"; -NSString *TSUnseenDatabaseViewExtensionName = @"TSUnseenDatabaseViewExtensionName"; -NSString *TSThreadSpecialMessagesDatabaseViewExtensionName = @"TSThreadSpecialMessagesDatabaseViewExtensionName"; -NSString *TSSecondaryDevicesDatabaseViewExtensionName = @"TSSecondaryDevicesDatabaseViewExtensionName"; +NSString *const TSUnreadIncomingMessagesGroup = @"TSUnreadIncomingMessagesGroup"; +NSString *const TSSecondaryDevicesGroup = @"TSSecondaryDevicesGroup"; + +NSString *const TSThreadDatabaseViewExtensionName = @"TSThreadDatabaseViewExtensionName"; +NSString *const TSMessageDatabaseViewExtensionName = @"TSMessageDatabaseViewExtensionName"; +NSString *const TSThreadOutgoingMessageDatabaseViewExtensionName = @"TSThreadOutgoingMessageDatabaseViewExtensionName"; +NSString *const TSUnreadDatabaseViewExtensionName = @"TSUnreadDatabaseViewExtensionName"; +NSString *const TSUnseenDatabaseViewExtensionName = @"TSUnseenDatabaseViewExtensionName"; +NSString *const TSThreadSpecialMessagesDatabaseViewExtensionName = @"TSThreadSpecialMessagesDatabaseViewExtensionName"; +NSString *const TSSecondaryDevicesDatabaseViewExtensionName = @"TSSecondaryDevicesDatabaseViewExtensionName"; + +@interface TSDatabaseView () + +@property (nonatomic) int pendingViewRegistrations; + +@end + +#pragma mark - @implementation TSDatabaseView -+ (BOOL)registerMessageDatabaseViewWithName:(NSString *)viewName ++ (instancetype)sharedInstance +{ + static TSDatabaseView *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] initDefault]; + }); + return sharedInstance; +} + +- (instancetype)initDefault +{ + self = [super init]; + + if (!self) { + return self; + } + + OWSSingletonAssert(); + + return self; +} + +- (void)setPendingViewRegistrations:(int)pendingViewRegistrations +{ + OWSAssert([NSThread isMainThread]); + + _pendingViewRegistrations = pendingViewRegistrations; + + if (pendingViewRegistrations == 0) { + [[NSNotificationCenter defaultCenter] postNotificationName:kNSNotificationName_DatabaseViewRegistrationComplete + object:nil + userInfo:nil]; + } +} + ++ (BOOL)hasPendingViewRegistrations +{ + OWSAssert([NSThread isMainThread]); + + return [TSDatabaseView sharedInstance].pendingViewRegistrations > 0; +} + ++ (void)registerMessageDatabaseViewWithName:(NSString *)viewName viewGrouping:(YapDatabaseViewGrouping *)viewGrouping version:(NSString *)version + async:(BOOL)async { + OWSAssert([NSThread isMainThread]); OWSAssert(viewName.length > 0); OWSAssert((viewGrouping)); YapDatabaseView *existingView = [[TSStorageManager sharedManager].database registeredExtension:viewName]; if (existingView) { - return YES; + return; } YapDatabaseViewSorting *viewSorting = [self messagesSorting]; @@ -50,10 +106,27 @@ NSString *TSSecondaryDevicesDatabaseViewExtensionName = @"TSSecondaryDevicesData YapDatabaseView *view = [[YapDatabaseView alloc] initWithGrouping:viewGrouping sorting:viewSorting versionTag:version options:options]; - return [[TSStorageManager sharedManager].database registerExtension:view withName:viewName]; + if (async) { + TSDatabaseView.sharedInstance.pendingViewRegistrations++; + + [[TSStorageManager sharedManager].database + asyncRegisterExtension:view + withName:viewName + completionBlock:^(BOOL ready) { + OWSCAssert(ready); + + DDLogInfo(@"%@ asyncRegisterExtension: %@ -> %d", self.tag, viewName, ready); + + dispatch_async(dispatch_get_main_queue(), ^{ + TSDatabaseView.sharedInstance.pendingViewRegistrations--; + }); + }]; + } else { + [[TSStorageManager sharedManager].database registerExtension:view withName:viewName]; + } } -+ (BOOL)registerUnreadDatabaseView ++ (void)registerUnreadDatabaseView { YapDatabaseViewGrouping *viewGrouping = [YapDatabaseViewGrouping withObjectBlock:^NSString *( YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object) { @@ -66,12 +139,13 @@ NSString *TSSecondaryDevicesDatabaseViewExtensionName = @"TSSecondaryDevicesData return nil; }]; - return [self registerMessageDatabaseViewWithName:TSUnreadDatabaseViewExtensionName - viewGrouping:viewGrouping - version:@"1"]; + [self registerMessageDatabaseViewWithName:TSUnreadDatabaseViewExtensionName + viewGrouping:viewGrouping + version:@"1" + async:NO]; } -+ (BOOL)registerUnseenDatabaseView ++ (void)asyncRegisterUnseenDatabaseView { YapDatabaseViewGrouping *viewGrouping = [YapDatabaseViewGrouping withObjectBlock:^NSString *( YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object) { @@ -84,12 +158,13 @@ NSString *TSSecondaryDevicesDatabaseViewExtensionName = @"TSSecondaryDevicesData return nil; }]; - return [self registerMessageDatabaseViewWithName:TSUnseenDatabaseViewExtensionName - viewGrouping:viewGrouping - version:@"1"]; + [self registerMessageDatabaseViewWithName:TSUnseenDatabaseViewExtensionName + viewGrouping:viewGrouping + version:@"1" + async:YES]; } -+ (BOOL)registerThreadSpecialMessagesDatabaseView ++ (void)asyncRegisterThreadSpecialMessagesDatabaseView { YapDatabaseViewGrouping *viewGrouping = [YapDatabaseViewGrouping withObjectBlock:^NSString *( YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object) { @@ -110,12 +185,13 @@ NSString *TSSecondaryDevicesDatabaseViewExtensionName = @"TSSecondaryDevicesData return nil; }]; - return [self registerMessageDatabaseViewWithName:TSThreadSpecialMessagesDatabaseViewExtensionName - viewGrouping:viewGrouping - version:@"1"]; + [self registerMessageDatabaseViewWithName:TSThreadSpecialMessagesDatabaseViewExtensionName + viewGrouping:viewGrouping + version:@"1" + async:YES]; } -+ (BOOL)registerThreadInteractionsDatabaseView ++ (void)registerThreadInteractionsDatabaseView { YapDatabaseViewGrouping *viewGrouping = [YapDatabaseViewGrouping withObjectBlock:^NSString *( YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object) { @@ -125,12 +201,13 @@ NSString *TSSecondaryDevicesDatabaseViewExtensionName = @"TSSecondaryDevicesData return interaction.uniqueThreadId; }]; - return [self registerMessageDatabaseViewWithName:TSMessageDatabaseViewExtensionName - viewGrouping:viewGrouping - version:@"1"]; + [self registerMessageDatabaseViewWithName:TSMessageDatabaseViewExtensionName + viewGrouping:viewGrouping + version:@"1" + async:NO]; } -+ (BOOL)registerThreadOutgoingMessagesDatabaseView ++ (void)asyncRegisterThreadOutgoingMessagesDatabaseView { YapDatabaseViewGrouping *viewGrouping = [YapDatabaseViewGrouping withObjectBlock:^NSString *( YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object) { @@ -140,16 +217,18 @@ NSString *TSSecondaryDevicesDatabaseViewExtensionName = @"TSSecondaryDevicesData return nil; }]; - return [self registerMessageDatabaseViewWithName:TSThreadOutgoingMessageDatabaseViewExtensionName - viewGrouping:viewGrouping - version:@"2"]; + [self registerMessageDatabaseViewWithName:TSThreadOutgoingMessageDatabaseViewExtensionName + viewGrouping:viewGrouping + version:@"2" + async:YES]; } -+ (BOOL)registerThreadDatabaseView { ++ (void)registerThreadDatabaseView +{ YapDatabaseView *threadView = [[TSStorageManager sharedManager].database registeredExtension:TSThreadDatabaseViewExtensionName]; if (threadView) { - return YES; + return; } YapDatabaseViewGrouping *viewGrouping = [YapDatabaseViewGrouping @@ -178,9 +257,8 @@ NSString *TSSecondaryDevicesDatabaseViewExtensionName = @"TSSecondaryDevicesData YapDatabaseView *databaseView = [[YapDatabaseView alloc] initWithGrouping:viewGrouping sorting:viewSorting versionTag:@"1" options:options]; - return [[TSStorageManager sharedManager] - .database registerExtension:databaseView - withName:TSThreadDatabaseViewExtensionName]; + [[TSStorageManager sharedManager].database registerExtension:databaseView + withName:TSThreadDatabaseViewExtensionName]; } /** @@ -305,6 +383,40 @@ NSString *TSSecondaryDevicesDatabaseViewExtensionName = @"TSSecondaryDevicesData }]; } ++ (id)unseenDatabaseViewExtension:(YapDatabaseReadTransaction *)transaction +{ + OWSAssert(transaction); + + id result = [transaction ext:TSUnseenDatabaseViewExtensionName]; + + if (!result) { + result = [transaction ext:TSUnreadDatabaseViewExtensionName]; + OWSAssert(result); + } + + return result; +} + ++ (id)threadOutgoingMessageDatabaseView:(YapDatabaseReadTransaction *)transaction +{ + OWSAssert(transaction); + + id result = [transaction ext:TSThreadOutgoingMessageDatabaseViewExtensionName]; + OWSAssert(result); + + return result; +} + ++ (id)threadSpecialMessagesDatabaseView:(YapDatabaseReadTransaction *)transaction +{ + OWSAssert(transaction); + + id result = [transaction ext:TSThreadSpecialMessagesDatabaseViewExtensionName]; + OWSAssert(result); + + return result; +} + #pragma mark - Logging + (NSString *)tag diff --git a/src/Storage/TSStorageManager.h b/src/Storage/TSStorageManager.h index da9138fe8..91be8f0bf 100644 --- a/src/Storage/TSStorageManager.h +++ b/src/Storage/TSStorageManager.h @@ -27,7 +27,16 @@ NS_ASSUME_NONNULL_BEGIN */ + (BOOL)isDatabasePasswordAccessible; -- (void)setupDatabase; +/** + * The safeBlockingMigrationsBlock block will + * run any outstanding version migrations that are a) blocking and b) safe + * to be run before the environment and storage is completely configured. + * + * Specifically, these migration should not depend on or affect the data + * of any database view. + */ +- (void)setupDatabaseWithSafeBlockingMigrations:(void (^_Nonnull)())safeBlockingMigrationsBlock; + - (void)deleteThreadsAndMessages; - (void)resetSignalStorage; diff --git a/src/Storage/TSStorageManager.m b/src/Storage/TSStorageManager.m index b435a07d3..6b392dda3 100644 --- a/src/Storage/TSStorageManager.m +++ b/src/Storage/TSStorageManager.m @@ -191,17 +191,30 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass"; }; } -- (void)setupDatabase +- (void)setupDatabaseWithSafeBlockingMigrations:(void (^_Nonnull)())safeBlockingMigrationsBlock { // Synchronously register extensions which are essential for views. [TSDatabaseView registerThreadDatabaseView]; [TSDatabaseView registerThreadInteractionsDatabaseView]; - [TSDatabaseView registerThreadOutgoingMessagesDatabaseView]; [TSDatabaseView registerUnreadDatabaseView]; - [TSDatabaseView registerUnseenDatabaseView]; - [TSDatabaseView registerThreadSpecialMessagesDatabaseView]; [self.database registerExtension:[TSDatabaseSecondaryIndexes registerTimeStampIndex] withName:@"idx"]; + // Run the blocking migrations. + // + // These need to run _before_ the async registered database views or + // they will block on them, which (in the upgrade case) can block + // return of appDidFinishLaunching... which in term can cause the + // app to crash on launch. + safeBlockingMigrationsBlock(); + + // Asynchronously register other extensions. + // + // All sync registrations must be done before all async registrations, + // or the sync registrations will block on the async registrations. + [TSDatabaseView asyncRegisterUnseenDatabaseView]; + [TSDatabaseView asyncRegisterThreadOutgoingMessagesDatabaseView]; + [TSDatabaseView asyncRegisterThreadSpecialMessagesDatabaseView]; + // Register extensions which aren't essential for rendering threads async [[OWSIncomingMessageFinder new] asyncRegisterExtension]; [TSDatabaseView asyncRegisterSecondaryDevicesDatabaseView];