I have the following base NSManagedObject in our Application:
@objc(LaravelEntity)
class LaravelEntity: NSManagedObject {
static func create(moc: NSManagedObjectContext) -> LaravelEntity? {
let entityName = NSStringFromClass(self)
let ent = NSEntityDescription.insertNewObject(forEntityName: entityName, into: moc) as? LaravelEntity
// We set local changes to true, so it will definitely be pushed to the server
ent?.local_changes = true
return ent
}
static func lastUpdatedRequest() -> NSFetchRequest<LaravelEntity> {
let request : NSFetchRequest<LaravelEntity> = self.self.fetchRequest()
let sort = NSSortDescriptor(key: #keyPath(LaravelEntity.updated_at), ascending: false)
request.sortDescriptors = [sort]
request.fetchLimit = 1
return request
}
}
I also have some subclasses of this. They are defined as subclasses in the DataModel as well:
@objc(Answer)
class Answer: LaravelEntity {
}
I try to fetch the last updated entity for a given class like this:
func lastUpdated(klass: LaravelEntity.Type, ctx : NSManagedObjectContext? = nil) -> LaravelEntity? {
do {
let request = klass.lastUpdatedRequest()
let result = try ctx?.fetch(request) ?? self.persistentContainer.newBackgroundContext().fetch(request)
return result.first
} catch {
print("Error while fetching last updated: \(error)!")
}
return nil
}
This works great for the most part. However, sporadically the application just crashes when trying to do something as simple as querying the id: lastUpdated?.id == 1
. I have no idea why this would crash, as all optionals are safely handled (as far as I can tell). Also, when looking at the objects in the debugger, they display fine (e.g. po lastUpdated?.id == 1
returns true in the debugger).
I have created the following example test case to reproduce the problem, somewhat consistently:
func testLastUpdated() {
for i in 0...1000 {
let cases = [Location.self, Position.self, Answer.self, Question.self]
for kase in cases {
let old = kase.create(moc: sm.persistentContainer.viewContext)
let new = kase.create(moc: sm.persistentContainer.viewContext)
XCTAssert(old != nil, "Couldn't create old \(kase) object!")
XCTAssert(new != nil, "Couldn't create new \(kase) object!")
old?.updated_at = Date(timeIntervalSinceNow: TimeInterval(-60*60*24 + i * 60))
old?.id = Int32(i*2)
new?.updated_at = Date(timeIntervalSinceNow: TimeInterval(i*60))
new?.id = Int32(i*2+1)
try! sm.persistentContainer.viewContext.save()
let last_updated = sm.lastUpdated(klass: kase)
XCTAssert(last_updated?.id == new?.id, "Last updated \(kase) doesn't have the correct id \(last_updated?.id ?? -1) vs \(new?.id ?? -1)!") //It crashes on this line.
}
}
}
However, even with a 1000 repetitions, it only crashes about 1/3 of the time I run the test. Additionally, it only crashes in the low 50s of iterations.
The following is the callstack when running the above test:
#0 0x00000001857f8430 in objc_msgSend ()
#1 0x0000000188e1f274 in -[NSPersistentStoreCoordinator _canRouteToStore:forContext:] ()
#2 0x0000000188e21f38 in __110-[NSPersistentStoreCoordinator(_NSInternalMethods) newValueForRelationship:forObjectWithID:withContext:error:]_block_invoke ()
#3 0x0000000188e29af0 in gutsOfBlockToNSPersistentStoreCoordinatorPerform ()
#4 0x0000000185f19048 in _dispatch_client_callout ()
#5 0x0000000185f5b760 in _dispatch_sync_invoke_and_complete_recurse ()
#6 0x0000000185f5b26c in _dispatch_sync_wait ()
#7 0x0000000188e17f74 in _perform ()
#8 0x0000000188e17d2c in -[NSPersistentStoreCoordinator _routeLightweightBlock:toStore:] ()
#9 0x0000000188d4bc94 in -[NSPersistentStoreCoordinator(_NSInternalMethods) newValueForRelationship:forObjectWithID:withContext:error:] ()
#10 0x0000000188d34d7c in _PFFaultHandlerFulfillFault ()
#11 0x0000000188d32a80 in _PFFaultHandlerLookupRow ()
#12 0x0000000188d32334 in _PF_FulfillDeferredFault ()
#13 0x0000000188dd7644 in _pvfk_header ()
#14 0x0000000188dd7450 in _sharedIMPL_pvfk_core_i ()
#15 0x00000001092484a4 in implicit closure #5 in SyncManagerTests.testLastUpdated() at CDTests.swift:74
#16 0x00000001092589d4 in partial apply for implicit closure #5 in SyncManagerTests.testLastUpdated() ()
#17 0x00000001092a5bdc in partial apply for closure #1 in XCTAssertTrue(_:_:file:line:) ()
#18 0x00000001092a5448 in partial apply for closure #1 in _XCTRunThrowableBlock(_:) ()
#19 0x0000000109290224 in thunk for @callee_owned () -> () ()
#20 0x00000001092a6048 in _XCTRunThrowableBlockBridge ()
#21 0x00000001092943bc in specialized _XCTRunThrowableBlock(_:) ()
#22 0x0000000109296118 in specialized XCTAssertTrue(_:_:file:line:) ()
#23 0x000000010929045c in XCTAssert(_:_:file:line:) ()
#24 0x0000000109248010 in SyncManagerTests.testLastUpdated() at CDTests.swift:74
#25 0x0000000109248aa8 in @objc SyncManagerTests.testLastUpdated() ()
#26 0x000000018659d670 in __invoking___ ()
#27 0x000000018647c6cc in -[NSInvocation invoke] ()
#28 0x0000000107996654 in __24-[XCTestCase invokeTest]_block_invoke.275 ()
#29 0x00000001079e4208 in -[XCTMemoryChecker _assertInvalidObjectsDeallocatedAfterScope:] ()
#30 0x0000000107996404 in __24-[XCTestCase invokeTest]_block_invoke ()
#31 0x00000001079dc9d8 in -[XCUITestContext performInScope:] ()
#32 0x000000010799614c in -[XCTestCase invokeTest] ()
#33 0x0000000107997224 in __26-[XCTestCase performTest:]_block_invoke.382 ()
#34 0x00000001079e1a78 in +[XCTContext runInContextForTestCase:block:] ()
#35 0x0000000107996c20 in -[XCTestCase performTest:] ()
#36 0x0000000107992e14 in __27-[XCTestSuite performTest:]_block_invoke ()
#37 0x000000010799283c in -[XCTestSuite _performProtectedSectionForTest:testSection:] ()
#38 0x0000000107992a4c in -[XCTestSuite performTest:] ()
#39 0x0000000107992e14 in __27-[XCTestSuite performTest:]_block_invoke ()
#40 0x000000010799283c in -[XCTestSuite _performProtectedSectionForTest:testSection:] ()
#41 0x0000000107992a4c in -[XCTestSuite performTest:] ()
#42 0x0000000107992e14 in __27-[XCTestSuite performTest:]_block_invoke ()
#43 0x000000010799283c in -[XCTestSuite _performProtectedSectionForTest:testSection:] ()
#44 0x0000000107992a4c in -[XCTestSuite performTest:] ()
#45 0x00000001079eb484 in __44-[XCTTestRunSession runTestsAndReturnError:]_block_invoke ()
#46 0x00000001079a5994 in -[XCTestObservationCenter _observeTestExecutionForBlock:] ()
#47 0x00000001079eb300 in -[XCTTestRunSession runTestsAndReturnError:] ()
#48 0x00000001079823d4 in -[XCTestDriver runTestsAndReturnError:] ()
#49 0x00000001079e0c20 in _XCTestMain ()
#50 0x000000018653e0fc in __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ ()
#51 0x000000018653d9cc in __CFRunLoopDoBlocks ()
#52 0x000000018653b6dc in __CFRunLoopRun ()
#53 0x000000018645bfb8 in CFRunLoopRunSpecific ()
#54 0x00000001882f3f84 in GSEventRunModal ()
#55 0x000000018fa302e8 in UIApplicationMain ()
#56 0x000000010137f528 in main at AppDelegate.swift:13
#57 0x0000000185f7e56c in start ()
I have no idea what could cause such a crash, since all optionals are handled correctly.
Edit 1: So the crash definitely happens on the following line last_updated?.id == new?.id
(Which still doesn't make any sense to me). Even if I comment out all code changing the objects and only create one object, it still crashes (randomly) on lastUpdated?.id != nil
.
Core data is not thread safe. Every context has one and only one thread that is can be read or written from. If you violate this, the behavior is UNDEFINED. Which means that it may crash or it may not. It may wait for 10 minutes and then crash when you do something unrelated.
In your code you are creating a newBackgroundContext
inside func lastUpdated
(because you are passing a nil context). Doing a fetch with this context is illegal. You can only use this context with performBlock
or performBlockAndWait
.
Also you have an extra self.
in lastUpdatedRequest