Search code examples
macoscore-dataicloudcore-data-migration

iCloud Core Data lightweight migration - entities disappear


In my app, that is available on the Mac Appstore I have iCloud + Core Data integrated for Mavericks users. So I use the latest iCloud implementation, which wasn’t so buggy as the old one… I thought.

Until now everything was fine, with the latest update of the app I made small changes to the Database, just adding a few new properties to an entity and created a new model version for this of course.

If you start the updated version, all data is there and everything is fine. All entities are still there. This is the console output:

storesDidChange: {
    added =     (
        "<NSSQLCore: 0x100410d10> (URL: file:///Users/lars/Library/Containers/de.nulldesign.tyme.osx/Data/Library/Application%20Support/de.nulldesign.tyme/CoreDataUbiquitySupport/lars~053CB071-18DD-5ED8-B2F0-DD0341B523C7/iCloudStore/2B3578B9-3A28-492B-8C62-BFF7FE7076F3/store/TymeCloud.storedata)"
    );
}

-[PFUbiquitySwitchboardEntryMetadata setUseLocalStorage:](760): CoreData: Ubiquity:  lars~053CB071-18DD-5ED8-B2F0-DD0341B523C7:iCloudStore 
Using local storage: 1

After a short while, this importer error occures:

__76-[_PFUbiquityRecordsImporter batchDownloadTransactionLogsAtLocations:error:]_block_invoke(763): CoreData: Ubiquity:  Librian returned a serious error for starting downloads Error Domain=LibrarianErrorDomain Code=1 "Der Vorgang konnte nicht abgeschlossen werden. (LibrarianErrorDomain-Fehler 1 - Unable to initiate download.)" UserInfo=0x600000271440 {NSDescription=Unable to initiate download., Item Errors={
    "file:///Users/lars/Library/Mobile%20Documents/HR22V4547K~de~nulldesign~tyme/CoreData/iCloudStore/lars~053CB071-18DD-5ED8-B2F0-DD0341B523C7/iCloudStore/16Dv7DuLeM4RoFUBtAVIQokfWJRwGABtWC81frFMCp0=/7B142E0C-3C97-4344-B647-28330E5D7FE3.1.cdt" = "Error Domain=UBErrorDomain Code=0 \"Der Vorgang konnte nicht abgeschlossen werden. (UBErrorDomain-Fehler 0 - Error Domain=UBErrorDomain Code=0 \"The operation couldn\U2019t be completed. (UBErrorDomain error 0.)\")\" 
…
UserInfo=0x600000271600 {NSDescription=Error Domain=UBErrorDomain Code=0 \"The operation couldn\U2019t be completed. (UBErrorDomain error 0.)\"}";
    };
    NSDescription = "Unable to initiate download.";
    NSUnderlyingError = "Error Domain=UBErrorDomain Code=0 \"Der Vorgang konnte nicht abgeschlossen werden. (UBErrorDomain-Fehler 0.)\"";
}

Then a stores will change event fires:

storesWillChange: {
    NSPersistentStoreUbiquitousTransitionTypeKey = 4;
    added =     (
        "<NSSQLCore: 0x100211490> (URL: file:///Users/lars/Library/Containers/de.nulldesign.tyme.osx/Data/Library/Application%20Support/de.nulldesign.tyme/CoreDataUbiquitySupport/lars~053CB071-18DD-5ED8-B2F0-DD0341B523C7/iCloudStore/2B3578B9-3A28-492B-8C62-BFF7FE7076F3/store/TymeCloud.storedata)"
    );
    removed =     (
        "<NSSQLCore: 0x100211490> (URL: file:///Users/lars/Library/Containers/de.nulldesign.tyme.osx/Data/Library/Application%20Support/de.nulldesign.tyme/CoreDataUbiquitySupport/lars~053CB071-18DD-5ED8-B2F0-DD0341B523C7/iCloudStore/2B3578B9-3A28-492B-8C62-BFF7FE7076F3/store/TymeCloud.storedata)"
    );
}

And quite a few storesWillChange & storesDidChange shorty after:

storesDidChange: {
    removed =     (
        "<NSSQLCore: 0x100410d10> (URL: file:///Users/lars/Library/Containers/de.nulldesign.tyme.osx/Data/Library/Application%20Support/de.nulldesign.tyme/CoreDataUbiquitySupport/lars~053CB071-18DD-5ED8-B2F0-DD0341B523C7/iCloudStore/2B3578B9-3A28-492B-8C62-BFF7FE7076F3/store/TymeCloud.storedata)"
    );
}
-[PFUbiquitySetupAssistant tryToReplaceLocalStore:withStoreSideLoadedByImporter:](3250): CoreData: Ubiquity:  <PFUbiquitySetupAssistant: 0x100411670>

Error refreshing peer range cache: (null)

storesDidChange: {
    NSPersistentStoreUbiquitousTransitionTypeKey = 4;
    added =     (
        "<NSSQLCore: 0x100410d10> (URL: file:///Users/lars/Library/Containers/de.nulldesign.tyme.osx/Data/Library/Application%20Support/de.nulldesign.tyme/CoreDataUbiquitySupport/lars~053CB071-18DD-5ED8-B2F0-DD0341B523C7/iCloudStore/2B3578B9-3A28-492B-8C62-BFF7FE7076F3/store/TymeCloud.storedata)"
    );
    removed =     (
        "<NSSQLCore: 0x100410d10> (URL: file:///Users/lars/Library/Containers/de.nulldesign.tyme.osx/Data/Library/Application%20Support/de.nulldesign.tyme/CoreDataUbiquitySupport/lars~053CB071-18DD-5ED8-B2F0-DD0341B523C7/iCloudStore/2B3578B9-3A28-492B-8C62-BFF7FE7076F3/store/TymeCloud.storedata)"
    );
}

storesDidChange: {
    added =     (
        "<NSSQLCore: 0x10020cdb0> (URL: file:///Users/lars/Library/Containers/de.nulldesign.tyme.osx/Data/Library/Application%20Support/de.nulldesign.tyme/CoreDataUbiquitySupport/lars~053CB071-18DD-5ED8-B2F0-DD0341B523C7/iCloudStore/2B3578B9-3A28-492B-8C62-BFF7FE7076F3/store/TymeCloud.storedata)"
    );
}

-[PFUbiquitySwitchboardEntryMetadata setUseLocalStorage:](760): CoreData: Ubiquity:  lars~053CB071-18DD-5ED8-B2F0-DD0341B523C7:iCloudStore
Using local storage: 1

After the store has changed, all entities are gone which were not created on this Mac. The other ones remain. Event if it switches to the cloud store:

PFUbiquitySwitchboardEntryMetadata setUseLocalStorage: Using local storage: 0

Only after upgrading all other Mac’s to the latest version of my app, the missing entities are getting synced back again.

The documentation says: Changes to a store are recorded and preserved independently for each model version that is associated with a given NSPersistentStoreUbiquitousContentNameKey. A persistent store configured with a given NSPersistentStoreUbiquitousContentNameKey only syncs data with a store on another device data if the model versions match.

If you migrate a persistent store configured with a NSPersistentStoreUbiquitousContentNameKey option to a new model version, the store’s history of changes originating from the current device will also be migrated and then merged with any other devices configured with that new model version

So I’m wondering, if this is an expected behavior when doing a lightweight migration in iCloud. For my users is definitively not an expected behavior, since their projects vanish until they have upgraded all their devices.

The Core Data stack set up is pretty straight forward:

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if(_persistentStoreCoordinator)
    {
        return _persistentStoreCoordinator;
    }

    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];

    if(_iCloudConnectionPossible)
    {
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(storesWillChange:)
                                                     name:NSPersistentStoreCoordinatorStoresWillChangeNotification
                                                   object:_persistentStoreCoordinator];

        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(persistentStoreDidImportUbiquitousContentChanges:)
                                                     name:NSPersistentStoreDidImportUbiquitousContentChangesNotification
                                                   object:_persistentStoreCoordinator];
    }

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(storesDidChange:)
                                                 name:NSPersistentStoreCoordinatorStoresDidChangeNotification
                                               object:_persistentStoreCoordinator];

    NSURL *localStoragePath = [self dataStorePathUseiCloud:_useCloudStorage];

    NSError *error = nil;

    [_persistentStoreCoordinator lock];
    NSPersistentStore *persistentStore = [_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
                                                                                   configuration:nil
                                                                                             URL:storagePath
                                                                                         options:[self storeOptionsUseiCloud:_useCloudStorage]
                                                                                           error:&error];
    [_persistentStoreCoordinator unlock];
    return _persistentStoreCoordinator;
}

- (NSDictionary *)storeOptionsUseiCloud:(BOOL)useiCloud
{
    NSMutableDictionary *options = [NSMutableDictionary dictionary];
    [options setObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption];
    [options setObject:[NSNumber numberWithBool:YES] forKey:NSInferMappingModelAutomaticallyOption];

    if(useiCloud)
    {
        [options setObject:@"iCloudStore" forKey:NSPersistentStoreUbiquitousContentNameKey];
    }

    return options;
}

- (NSManagedObjectContext *)managedObjectContext
{
    if(_managedObjectContext)
    {
        return _managedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];

    if(coordinator != nil)
    {
        NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;

        [moc performBlockAndWait:^
        {
            [moc setPersistentStoreCoordinator:coordinator];
        }];

        _managedObjectContext = moc;
    }

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(contextWillSaveNotification:)
                                                 name:NSManagedObjectContextWillSaveNotification
                                               object:_managedObjectContext];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(mergeChangesAfterDidSaveNotification:)
                                                 name:NSManagedObjectContextDidSaveNotification
                                               object:_managedObjectContext];

    return _managedObjectContext;
}

And the storesWill / DidChange handlers:

- (void)storesWillChange:(NSNotification *)note
{
    NSManagedObjectContext *moc = self.managedObjectContext;
    [moc performBlockAndWait:^
    {
        NSError *error = nil;
        if([moc hasChanges])
        {
            [moc save:&error];
        }

        [moc reset];
    }];
}

- (void)storesDidChange:(NSNotification *)note
{
    // refresh UI, ...
}

Is there any way around this? What’s wrong here?


Solution

  • Meanwhile I received a statement about this issue from Apple Tech Support: Yes, this is the expected behavior. I should file an "enhancement" report, if I expect a better behavior.

    Which means: CoreData migration is broken for iCloud. This is not an acceptable experience for a user. How should you be able to tell your customers, that they temporarily loose data until all of their devices are upgraded. If they reinstalled one device or it's broken, the data is lost in the void of iCloud forever.