Skip to content

Commit b53f584

Browse files
authored
Load the current Core Data model once per process (#25728)
* Format ContextManager.swift with swift-format * Load the current Core Data model once per process Each ContextManager instance used to load its own NSManagedObjectModel from WordPress.momd. Every loaded model registers its entities in Core Data's process-global class-to-entity table, so in unit tests (where each test creates a ContextManager) identical models accumulated and +entity could not disambiguate them. The current model is now cached in a static and shared by all instances, while versioned models for migrations still load per request.
1 parent 0c24e62 commit b53f584

1 file changed

Lines changed: 86 additions & 29 deletions

File tree

Modules/Sources/WordPressData/Swift/ContextManager.swift

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -78,34 +78,50 @@ public class ContextManager: NSObject, CoreDataStack, CoreDataStackSwift {
7878
}
7979

8080
@objc(performAndSaveUsingBlock:completion:onQueue:)
81-
public func performAndSave(_ block: @escaping (NSManagedObjectContext) -> Void, completion: (() -> Void)?, on queue: DispatchQueue) {
81+
public func performAndSave(
82+
_ block: @escaping (NSManagedObjectContext) -> Void,
83+
completion: (() -> Void)?,
84+
on queue: DispatchQueue
85+
) {
8286
let context = newDerivedContext()
83-
self.writerQueue.addOperation(AsyncBlockOperation { done in
84-
context.perform {
85-
block(context)
87+
self.writerQueue.addOperation(
88+
AsyncBlockOperation { done in
89+
context.perform {
90+
block(context)
8691

87-
self.save(context, .alreadyInContextQueue)
88-
queue.async { completion?() }
89-
done()
92+
self.save(context, .alreadyInContextQueue)
93+
queue.async { completion?() }
94+
done()
95+
}
9096
}
91-
})
97+
)
9298
}
9399

94-
public func performAndSave<T>(_ block: @escaping (NSManagedObjectContext) throws -> T, completion: ((Result<T, Error>) -> Void)?, on queue: DispatchQueue) {
100+
public func performAndSave<T>(
101+
_ block: @escaping (NSManagedObjectContext) throws -> T,
102+
completion: ((Result<T, Error>) -> Void)?,
103+
on queue: DispatchQueue
104+
) {
95105
let context = newDerivedContext()
96-
self.writerQueue.addOperation(AsyncBlockOperation { done in
97-
context.perform {
98-
let result = Result(catching: { try block(context) })
99-
if case .success = result {
100-
self.save(context, .alreadyInContextQueue)
106+
self.writerQueue.addOperation(
107+
AsyncBlockOperation { done in
108+
context.perform {
109+
let result = Result(catching: { try block(context) })
110+
if case .success = result {
111+
self.save(context, .alreadyInContextQueue)
112+
}
113+
queue.async { completion?(result) }
114+
done()
101115
}
102-
queue.async { completion?(result) }
103-
done()
104116
}
105-
})
117+
)
106118
}
107119

108-
public func performAndSave<T>(_ block: @escaping (NSManagedObjectContext) -> T, completion: ((T) -> Void)?, on queue: DispatchQueue) {
120+
public func performAndSave<T>(
121+
_ block: @escaping (NSManagedObjectContext) -> T,
122+
completion: ((T) -> Void)?,
123+
on queue: DispatchQueue
124+
) {
109125
performAndSave(
110126
block,
111127
completion: { (result: Result<T, Error>) in
@@ -147,7 +163,11 @@ public class ContextManager: NSObject, CoreDataStack, CoreDataStackSwift {
147163
return
148164
}
149165

150-
guard let metadata = try? NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: NSSQLiteStoreType, at: storeURL),
166+
guard
167+
let metadata = try? NSPersistentStoreCoordinator.metadataForPersistentStore(
168+
ofType: NSSQLiteStoreType,
169+
at: storeURL
170+
),
151171
!objectModel.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata)
152172
else {
153173
return
@@ -159,7 +179,8 @@ public class ContextManager: NSObject, CoreDataStack, CoreDataStackSwift {
159179
fatalError("Can't find WordPress.momd")
160180
}
161181

162-
guard let versionInfo = NSDictionary(contentsOf: modelFileURL.appendingPathComponent("VersionInfo.plist")) else {
182+
guard let versionInfo = NSDictionary(contentsOf: modelFileURL.appendingPathComponent("VersionInfo.plist"))
183+
else {
163184
fatalError("Can't get the object model's version info")
164185
}
165186

@@ -181,7 +202,14 @@ public class ContextManager: NSObject, CoreDataStack, CoreDataStackSwift {
181202

182203
private extension ContextManager {
183204
static var localDatabasePath: URL {
184-
guard let url = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else {
205+
guard
206+
let url = try? FileManager.default.url(
207+
for: .documentDirectory,
208+
in: .userDomainMask,
209+
appropriateFor: nil,
210+
create: true
211+
)
212+
else {
185213
fatalError("Failed to find the document directory")
186214
}
187215

@@ -202,7 +230,10 @@ private extension ContextManager {
202230
}
203231

204232
// Ensure that the `context`'s concurrency type is not `confinementConcurrencyType`, since it will crash if `perform` or `performAndWait` is called.
205-
guard context.concurrencyType == .mainQueueConcurrencyType || context.concurrencyType == .privateQueueConcurrencyType else {
233+
guard
234+
context.concurrencyType == .mainQueueConcurrencyType
235+
|| context.concurrencyType == .privateQueueConcurrencyType
236+
else {
206237
block()
207238
return
208239
}
@@ -221,19 +252,45 @@ private extension ContextManager {
221252
// MARK: - Initialise Core Data stack
222253

223254
private extension ContextManager {
224-
static func createPersistentContainer(storeURL: URL, modelName: String) -> NSPersistentContainer {
225-
guard var modelFileURL = Bundle.wordPressData.url(forResource: "WordPress", withExtension: "momd") else {
255+
/// The current version of the data model, loaded once per process.
256+
///
257+
/// Every loaded `NSManagedObjectModel` registers its entities in Core Data's global
258+
/// class-to-entity table. Loading the model file more than once (which happens in unit
259+
/// tests, where each test creates its own `ContextManager` instance) leaves multiple
260+
/// entity descriptions claiming the same `NSManagedObject` subclasses, and
261+
/// `+[NSManagedObject entity]` fails to resolve them.
262+
static let currentObjectModel: NSManagedObjectModel = {
263+
guard let modelFileURL = Bundle.wordPressData.url(forResource: "WordPress", withExtension: "momd") else {
226264
fatalError("Can't find WordPress.momd")
227265
}
228266

229-
if modelName != ContextManagerModelNameCurrent {
230-
modelFileURL = modelFileURL.appendingPathComponent(modelName).appendingPathExtension("mom")
267+
guard let objectModel = NSManagedObjectModel(contentsOf: modelFileURL) else {
268+
fatalError("Can't create object model at \(modelFileURL)")
231269
}
232270

233-
guard let objectModel = NSManagedObjectModel(contentsOf: modelFileURL) else {
234-
fatalError("Can't create object model named \(modelName) at \(modelFileURL)")
271+
return objectModel
272+
}()
273+
274+
static func objectModel(named modelName: String) -> NSManagedObjectModel {
275+
guard modelName != ContextManagerModelNameCurrent else {
276+
return currentObjectModel
277+
}
278+
279+
guard let modelFileURL = Bundle.wordPressData.url(forResource: "WordPress", withExtension: "momd") else {
280+
fatalError("Can't find WordPress.momd")
235281
}
236282

283+
let versionedModelURL = modelFileURL.appendingPathComponent(modelName).appendingPathExtension("mom")
284+
guard let objectModel = NSManagedObjectModel(contentsOf: versionedModelURL) else {
285+
fatalError("Can't create object model named \(modelName) at \(versionedModelURL)")
286+
}
287+
288+
return objectModel
289+
}
290+
291+
static func createPersistentContainer(storeURL: URL, modelName: String) -> NSPersistentContainer {
292+
let objectModel = objectModel(named: modelName)
293+
237294
// FIXME: Import the Sentry stuff, too — But it accesses the app delegate!
238295
// let startupEvent = SentryStartupEvent()
239296

@@ -292,7 +349,7 @@ extension ContextManager {
292349
}
293350

294351
public static var shared: ContextManager {
295-
return sharedInstance()
352+
sharedInstance()
296353
}
297354
}
298355

0 commit comments

Comments
 (0)