Reputation: 2414
I'm running into an annoying bug in Core Data that is causing some issues with some of my end users. Some background:
My data model contains two configurations, a CRM configuration, and a Catalog configuration. The CRM configuration has entities that relate to such things as contacts/accounts among other related entities. The Catalog has other entities such as Collections/Products/etc.
After the user first logs in, they are forced to do a sync of both stores though a web service. All this works fine. In the latest update of the app, I prompt the user that they need to sync their catalog data again. This sends them back to the sync screen so they can sync the catalog again.
Here lies the problem. When they go to perform the sync, our code goes and removes the persistent store from the context, removes the .sqlite file, then adds the store back in to the persistent store.
Code:
- (void) resetStoreAtPath:(NSURL *)path withConfiguration:(NSString *)configuration
{
NSPersistentStoreCoordinator *coordinator = self.persistentStoreCoordinator;
//Get the store for this database.
NSPersistentStore *store = [coordinator persistentStoreForURL:path];
NSError *__autoreleasing error = nil;
if(store != nil)
{
if(![coordinator removePersistentStore:store error:&error])
{
//Log The Error
}
}
//Remove the file. Even if it errors, try to add it, but log it.
error = nil;
if(![[NSFileManager defaultManager] removeItemAtURL:path error:&error])
{
//Log the error
}
error = nil;
[self addStoreWithConfiguration:configuration URL:path error:&error];
//Log Error
}
None of the above code errors. Using iFunBox, I can even browse the app and check the file system, and the .sqlite file is recreated with no problems.
Once this is done, we then sync the catalog data again. The strange thing here is, occasionally (not every time), Core Data will decide to place the Catalog configuration entities into the CRM db instead of the new Catalog db!
Then later in the app, when the user goes to browse the catalog, the catalog data does not show up anymore.
I've pulled the .sqlite files off the device to confirm the catalog data is inside the crm.sqlite db. and the catalog.sqlite db is void of any rows of data. Is there any particular reason this is happening? Is there a better approach to this?
EDIT: This is the other method that's readding it.
- (NSPersistentStore *) addStoreWithConfiguration:(NSString *)configuration URL:(NSURL *)url error:(NSError **)error
{
NSDictionary *options = @{ NSSQLitePragmasOption: @{@"journal_mode": @"DELETE"}, NSMigratePersistentStoresAutomaticallyOption: @YES, NSInferMappingModelAutomaticallyOption: @YES };
NSPersistentStore *store = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:configuration URL:url options:options error:error];
if(store == nil)
{
//Something went wrong. Log it. Remove File, try to readd.
[[NSFileManager defaultManager] removeItemAtURL:url error:nil];
store = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:configuration URL:url options:options error:error];
if(store == nil)
{
//Something is dreadfully wrong, log and throw.
[NSException raise:@"NSInternalInconsistencyException" message:@"After two attempts, we were unable to create the database. User may need to uninstall their app to continue." parameters:nil];
}
}
return store;
}
I am only removing the store that I need to, in this case, the Catalog store.
EDIT 2: In regards to your questions...
The removal code happens on the main thread in response to a button press. We then fire off an operation to download a zip file, and once that's done, we process the zip file in a background operation. The background process uses a private queue context, which gets merged on the main context once its saved.
I don't think locking will assist in this case, as the data for the catalog is saved MUCH later after the store has already been reset. It doesn't hurt to try though.
Maybe describing how the problem happens would maybe shed some light.
I can PREVENT this from happening by doing the following:
I wonder if at this point that the recreation of the ENTIRE core data stack is what will fix this issue. I really want to avoid that because it seems entirely unnecessary.
Upvotes: 2
Views: 558
Reputation: 2414
Marcus definitely got me on the right path, so thanks for that.
In the end, refreshing the entire core data stack is what fixes my issue, though I'm a bit baffled why this is necessary.
I would definitely have to say that this is an underlying issue with the persistent store coordinator not inserting the correct entities into the correct stores when using more than one configuration and resetting the store.
Upvotes: 0
Reputation: 46728
Is this error occurring under iOS 7? If so, you are running into an issue with the journal files. Apple changed the configuration of SQLite under Core Data in iOS 7 to use journal files. If you are going to be deleting sqlite files then you need to check for and delete the journal files as well. They will have the same core file name as your main sqlite file.
Another option would be to turn journaling off so that you get the same behavior you had prior to iOS 7.
Are there journal files still on disk or are you getting this behavior in a clean environment?
What does your -addStore...
method look like?
Are you dropping both stores at the same time?
What does your drop stores code look like?
It appears you are not checking the error coming back from -addPersistentStore...
. Are you getting an error? I could easily see a situation where one of the -addPersistentStore...
calls fails and then there is only one store on disk so Core Data writes data into it.
It is recommended that you always check the error, even if it is just to log it to disk. I would go even further and recommend a NSAssert
that the store is coming back !nil
.
Eliminating the rest, my thoughts turn to threading. When are you deleting this store? Is there a possibility of a save being fired between the removal and the add?
Is this firing on a background thread?
What would happen if you were to lock the NSPersistentStoreCoordinator
at the top of this process and then unlock it at the bottom?
When you use configurations, Core Data actually puts all of the tables in all of the stores, even if those tables are not being used. If we run into a race condition, I can see a situation where Core Data could write to the "wrong" store because at that moment in time it is the only store. Locking the PSC will test that theory.
Recreating the whole Core Data stack is not that expensive, you are already doing the expensive part. This definitely smells like a collision with that store being unavailable at the wrong time but without the app in front of me that is hard to prove/disprove. If your app is structured well enough to allow the rebuild of the stack I would agree that doing so would be your shortest path. In this case nuking from orbit is a viable strategy.
Upvotes: 3