Skip to content

Commit

Permalink
Merge pull request #853 from bfahey/feature/fix-persistent-container-…
Browse files Browse the repository at this point in the history
…loading

Stopping Core Data Crashes When Disk is Full
  • Loading branch information
evantk91 authored Nov 20, 2024
2 parents 86f01d7 + 9cd5af0 commit d3a9433
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 115 deletions.
11 changes: 3 additions & 8 deletions swift-sdk/Internal/DependencyContainerProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ protocol DependencyContainerProtocol: RedirectNetworkSessionProvider {
var apnsTypeChecker: APNSTypeCheckerProtocol { get }

func createInAppFetcher(apiClient: ApiClientProtocol) -> InAppFetcherProtocol
func createPersistenceContextProvider() -> IterablePersistenceContextProvider?
func createPersistenceContextProvider() -> IterablePersistenceContextProvider
func createRequestHandler(apiKey: String,
config: IterableConfig,
endpoint: String,
Expand Down Expand Up @@ -83,12 +83,7 @@ extension DependencyContainerProtocol {
dateProvider: dateProvider)
lazy var offlineProcessor: OfflineRequestProcessor? = nil
lazy var healthMonitor: HealthMonitor? = nil
guard let persistenceContextProvider = createPersistenceContextProvider() else {
return RequestHandler(onlineProcessor: onlineProcessor,
offlineProcessor: nil,
healthMonitor: nil,
offlineMode: offlineMode)
}
let persistenceContextProvider = createPersistenceContextProvider()
if offlineMode {

let healthMonitorDataProvider = createHealthMonitorDataProvider(persistenceContextProvider: persistenceContextProvider)
Expand Down Expand Up @@ -124,7 +119,7 @@ extension DependencyContainerProtocol {
HealthMonitorDataProvider(maxTasks: 1000, persistenceContextProvider: persistenceContextProvider)
}

func createPersistenceContextProvider() -> IterablePersistenceContextProvider? {
func createPersistenceContextProvider() -> IterablePersistenceContextProvider {
CoreDataPersistenceContextProvider(dateProvider: dateProvider)
}

Expand Down
174 changes: 95 additions & 79 deletions swift-sdk/Internal/IterableCoreDataPersistence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ enum PersistenceConst {
enum Entity {
enum Task {
static let name = "IterableTaskManagedObject"

enum Column {
static let id = "id"
static let scheduledAt = "scheduledAt"
Expand All @@ -21,143 +21,152 @@ enum PersistenceConst {
}
}

class PersistentContainer: NSPersistentContainer {
static var shared: PersistentContainer?

static func initialize() -> PersistentContainer? {
if shared == nil {
shared = create()
}
return shared
let sharedManagedObjectModel: NSManagedObjectModel? = {
let firstBundleURL: URL? = [Bundle.main, Bundle(for: PersistentContainer.self)].lazy.compactMap { bundle in
ResourceHelper.url(
forResource: PersistenceConst.dataModelFileName,
withExtension: PersistenceConst.dataModelExtension,
fromBundle: bundle
)
}.first

guard let url = firstBundleURL else {
ITBError("Could not find \(PersistenceConst.dataModelFileName).\(PersistenceConst.dataModelExtension) in bundle")
return nil
}

ITBInfo("DB Bundle url: \(url)")
return NSManagedObjectModel(contentsOf: url)
}()

final class PersistentContainer: NSPersistentContainer, @unchecked Sendable {

override func newBackgroundContext() -> NSManagedObjectContext {
let backgroundContext = super.newBackgroundContext()
backgroundContext.automaticallyMergesChangesFromParent = true
backgroundContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyStoreTrumpMergePolicyType)
return backgroundContext
}

private static func create() -> PersistentContainer? {
guard let managedObjectModel = createManagedObjectModel() else {
ITBError("Could not initialize managed object model")
return nil
init() {
let name = PersistenceConst.dataModelFileName
if let managedObjectModel = sharedManagedObjectModel {
super.init(name: name, managedObjectModel: managedObjectModel)
} else {
super.init(name: name)
}
let container = PersistentContainer(name: PersistenceConst.dataModelFileName, managedObjectModel: managedObjectModel)
container.loadPersistentStores { desc, error in
if let error = error {
ITBError("Unresolved error when creating PersistentContainer: \(error)")
}

ITBInfo("Successfully loaded persistent store at: \(desc.url?.description ?? "nil")")
}

container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyStoreTrumpMergePolicyType)

return container
}

private static func createManagedObjectModel() -> NSManagedObjectModel? {
guard let url = dataModelUrl(fromBundles: [Bundle.main, Bundle(for: PersistentContainer.self)]) else {
ITBError("Could not find \(PersistenceConst.dataModelFileName).\(PersistenceConst.dataModelExtension) in bundle")
return nil
}
ITBInfo("DB Bundle url: \(url)")
return NSManagedObjectModel(contentsOf: url)
}

private static func dataModelUrl(fromBundles bundles: [Bundle]) -> URL? {
bundles.lazy.compactMap(dataModelUrl(fromBundle:)).first
}

private static func dataModelUrl(fromBundle bundle: Bundle) -> URL? {
ResourceHelper.url(forResource: PersistenceConst.dataModelFileName,
withExtension: PersistenceConst.dataModelExtension,
fromBundle: bundle)
viewContext.automaticallyMergesChangesFromParent = true
viewContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyStoreTrumpMergePolicyType)
}
}

struct CoreDataPersistenceContextProvider: IterablePersistenceContextProvider {
init?(dateProvider: DateProviderProtocol = SystemDateProvider()) {
guard let persistentContainer = PersistentContainer.initialize() else {
return nil
}
final class CoreDataPersistenceContextProvider: IterablePersistenceContextProvider {
init(
dateProvider: DateProviderProtocol = SystemDateProvider(),
persistentContainer: NSPersistentContainer = PersistentContainer()
) {
self.persistentContainer = persistentContainer
self.dateProvider = dateProvider
}

func newBackgroundContext() -> IterablePersistenceContext {
if !isStoreLoaded {
isStoreLoaded = loadStore(into: persistentContainer)
}
return CoreDataPersistenceContext(managedObjectContext: persistentContainer.newBackgroundContext(), dateProvider: dateProvider)
}

func mainQueueContext() -> IterablePersistenceContext {
if !isStoreLoaded {
isStoreLoaded = loadStore(into: persistentContainer)
}
return CoreDataPersistenceContext(managedObjectContext: persistentContainer.viewContext, dateProvider: dateProvider)
}
private let persistentContainer: PersistentContainer

private let persistentContainer: NSPersistentContainer
private let dateProvider: DateProviderProtocol
private var isStoreLoaded = false

/// Loads the persistent container synchronously so we can easily capture loading errors.
private func loadStore(into container: NSPersistentContainer) -> Bool {
if let descriptor = container.persistentStoreDescriptions.first {
descriptor.shouldAddStoreAsynchronously = false
}

// This closure runs synchronously because of the settings above
var loadError: (any Error)?
container.loadPersistentStores { _, error in
loadError = error
}

if let error = loadError {
ITBError("Failed to load Iterable's store. \(error.localizedDescription)")
return false
}
return true
}
}

struct CoreDataPersistenceContext: IterablePersistenceContext {
init(managedObjectContext: NSManagedObjectContext, dateProvider: DateProviderProtocol) {
self.managedObjectContext = managedObjectContext
self.dateProvider = dateProvider
}

func create(task: IterableTask) throws -> IterableTask {
guard let taskManagedObject = createTaskManagedObject() else {
throw IterableDBError.general("Could not create task managed object")
}

PersistenceHelper.copy(from: task, to: taskManagedObject)
taskManagedObject.createdAt = dateProvider.currentDate
return PersistenceHelper.task(from: taskManagedObject)
}

func update(task: IterableTask) throws -> IterableTask {
guard let taskManagedObject = try findTaskManagedObject(id: task.id) else {
throw IterableDBError.general("Could not find task to update")
}

PersistenceHelper.copy(from: task, to: taskManagedObject)
taskManagedObject.modifiedAt = dateProvider.currentDate
return PersistenceHelper.task(from: taskManagedObject)
}

func delete(task: IterableTask) throws {
try deleteTask(withId: task.id)
}

func nextTask() throws -> IterableTask? {
let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findSortedEntities(context: managedObjectContext,
entity: PersistenceConst.Entity.Task.name,
column: PersistenceConst.Entity.Task.Column.scheduledAt,
ascending: true,
limit: 1)
let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findSortedEntities(
context: managedObjectContext,
entity: PersistenceConst.Entity.Task.name,
column: PersistenceConst.Entity.Task.Column.scheduledAt,
ascending: true,
limit: 1
)
return taskManagedObjects.first.map(PersistenceHelper.task(from:))
}

func findTask(withId id: String) throws -> IterableTask? {
guard let taskManagedObject = try findTaskManagedObject(id: id) else {
return nil
}
return PersistenceHelper.task(from: taskManagedObject)
}

func deleteTask(withId id: String) throws {
guard let taskManagedObject = try findTaskManagedObject(id: id) else {
return
}
managedObjectContext.delete(taskManagedObject)
}

func findAllTasks() throws -> [IterableTask] {
let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findAll(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name)

return taskManagedObjects.map(PersistenceHelper.task(from:))
}

func deleteAllTasks() throws {
let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findAll(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name)
taskManagedObjects.forEach {
Expand All @@ -168,34 +177,41 @@ struct CoreDataPersistenceContext: IterablePersistenceContext {
}
}
}

func countTasks() throws -> Int {
return try CoreDataUtil.count(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name)
try CoreDataUtil.count(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name)
}

func save() throws {
// Guard against Objective-C exceptions which cannot be caught in Swift.
guard
let coordinator = managedObjectContext.persistentStoreCoordinator,
!coordinator.persistentStores.isEmpty
else {
throw NSError(domain: NSCocoaErrorDomain, code: NSPersistentStoreSaveError)
}
try managedObjectContext.save()
}

func perform(_ block: @escaping () -> Void) {
managedObjectContext.perform(block)
}

func performAndWait(_ block: () -> Void) {
managedObjectContext.performAndWait(block)
}

func performAndWait<T>(_ block: () throws -> T) throws -> T {
try managedObjectContext.performAndWait(block)
}

private let managedObjectContext: NSManagedObjectContext
private let dateProvider: DateProviderProtocol

private func findTaskManagedObject(id: String) throws -> IterableTaskManagedObject? {
try CoreDataUtil.findEntitiyByColumn(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name, columnName: PersistenceConst.Entity.Task.Column.id, columnValue: id)
}

private func createTaskManagedObject() -> IterableTaskManagedObject? {
CoreDataUtil.create(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name)
}
Expand Down
2 changes: 1 addition & 1 deletion tests/common/CommonExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ class MockDependencyContainer: DependencyContainerProtocol {
HealthMonitorDataProvider(maxTasks: maxTasks, persistenceContextProvider: persistenceContextProvider)
}

func createPersistenceContextProvider() -> IterablePersistenceContextProvider? {
func createPersistenceContextProvider() -> IterablePersistenceContextProvider {
if let persistenceContextProvider = persistenceContextProvider {
return persistenceContextProvider
} else {
Expand Down
45 changes: 24 additions & 21 deletions tests/endpoint-tests/OfflineModeE2ETests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,42 +81,45 @@ class OfflineModeEndpointTests: XCTestCase {
"messageId": "msg_1",
],
]
api.trackPushOpen(pushPayload,
dataFields: ["data_field1": "value1"],
onSuccess: { _ in
expectation1.fulfill()
}) { reason, _ in
api.trackPushOpen(
pushPayload,
dataFields: ["data_field1": "value1"],
onSuccess: { _ in
expectation1.fulfill()
}
) { reason, _ in
XCTFail(reason ?? "failed")
}

wait(for: [expectation1], timeout: 15)
}

func test04TrackEvent() throws {
let expectation1 = expectation(description: #function)
let localStorage = MockLocalStorage()
localStorage.offlineMode = true
let api = InternalIterableAPI.initializeForE2E(apiKey: Self.apiKey,
localStorage: localStorage)
let api = InternalIterableAPI.initializeForE2E(
apiKey: Self.apiKey,
localStorage: localStorage
)
api.email = "[email protected]"

api.track("event1",
dataFields: ["data_field1": "value1"],
onSuccess: { _ in
expectation1.fulfill()
}) { reason, _ in

api.track(
"event1",
dataFields: ["data_field1": "value1"],
onSuccess: { _ in
expectation1.fulfill()
}
) { reason, _ in
XCTFail(reason ?? "failed")
}

wait(for: [expectation1], timeout: 15)
}

private static let apiKey = Environment.apiKey!
private static let pushCampaignId = Environment.pushCampaignId!
private static let pushTemplateId = Environment.pushTemplateId!
private static let inAppCampaignId = Environment.inAppCampaignId!
private lazy var persistenceContextProvider: IterablePersistenceContextProvider = {
let provider = CoreDataPersistenceContextProvider()!
return provider
}()
private lazy var persistenceContextProvider: IterablePersistenceContextProvider = CoreDataPersistenceContextProvider()
}
2 changes: 1 addition & 1 deletion tests/offline-events-tests/HealthMonitorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ class HealthMonitorTests: XCTestCase {
private let dateProvider = MockDateProvider()

private lazy var persistenceProvider: IterablePersistenceContextProvider = {
let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider)!
let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider)
try! provider.mainQueueContext().deleteAllTasks()
try! provider.mainQueueContext().save()
return provider
Expand Down
Loading

0 comments on commit d3a9433

Please sign in to comment.