Skip to content

Commit 8ae0aab

Browse files
author
Rover Release Bot 🤖
committed
Releasing 4.11.4
1 parent ec1daf6 commit 8ae0aab

File tree

4 files changed

+95
-40
lines changed

4 files changed

+95
-40
lines changed

Sources/Foundation/Meta.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ import Foundation
1717

1818
public enum Meta {
1919
public static let APIVersion: Int = 2
20-
public static let SDKVersion: String = "4.11.3"
20+
public static let SDKVersion: String = "4.11.4"
2121
}

Sources/Notifications/Communication Hub/Data/RCHPersistentContainer.swift

Lines changed: 79 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@ import Combine
2020
import os.log
2121
final class RCHPersistentContainer: NSPersistentContainer, @unchecked Sendable {
2222

23-
/// Observe this bool to discover when initial restore has completed.
24-
///
25-
/// WIll never revert back to false.
26-
@Published var loaded: Bool = false
23+
/// Observe this state to discover when initial restore has completed.
24+
@Published var state: State = .loading
2725

2826
init(storage: Storage) {
2927
os_log("Initializing Core Data CommunicationPersistentContainer in storage mode: %{public}@", log: .communicationHub, type: .debug, storage.rawValue)
@@ -60,36 +58,49 @@ final class RCHPersistentContainer: NSPersistentContainer, @unchecked Sendable {
6058
}
6159

6260
private func configureWithSqlite() {
61+
loadPersistentStoresWithRetry(isRetry: false)
62+
}
63+
64+
private func loadPersistentStoresWithRetry(isRetry: Bool) {
6365
loadPersistentStores { description, error in
64-
defer {
65-
self.loaded = true
66-
}
67-
6866
if let error = error {
69-
os_log("Error loading Core Data persistent store: %{public}@", log: .communicationHub, type: .error, error.localizedDescription)
70-
71-
// Get the URL from the description
72-
if let url = description.url {
73-
do {
74-
let coordinator = self.persistentStoreCoordinator
75-
76-
// Destroy the persistent store
77-
try coordinator.destroyPersistentStore(at: url, ofType: NSSQLiteStoreType, options: nil)
78-
os_log("Successfully destroyed Core Data persistent store at: %{public}@", log: .communicationHub, type: .info, url.path)
79-
80-
// Recreate the persistent store
81-
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil)
82-
os_log("Successfully recreated Core Data persistent store at: %{public}@", log: .communicationHub, type: .info, url.path)
83-
84-
} catch {
85-
os_log("Failed to reset Core Data persistent store: %{public}@", log: .communicationHub, type: .error, error.localizedDescription)
86-
fatalError("Unable to recover from Core Data store failure")
87-
}
88-
} else {
89-
fatalError("Failed to get URL from Core Data persistent store description")
67+
os_log("Error loading Core Data persistent store%{public}@: %{public}@", log: .communicationHub, type: .error, isRetry ? " (retry)" : "", error.localizedDescription)
68+
69+
// If this was already a retry attempt, fall back to in-memory storage
70+
guard !isRetry else {
71+
os_log("Retry attempt failed, falling back to in-memory storage", log: .communicationHub, type: .error)
72+
self.fallbackToInMemoryStorage()
73+
return
74+
}
75+
76+
// Get the URL from the description in order to attempt dropping and recreating the DB.
77+
guard let storeURL = description.url else {
78+
os_log("loadPersistentStores() error state occurred, but unable to obtain store URLs to attempt dropping DB, falling back to in-memory storage", log: .communicationHub, type: .error)
79+
self.fallbackToInMemoryStorage()
80+
return
9081
}
82+
83+
do {
84+
let coordinator = self.persistentStoreCoordinator
85+
86+
// Destroy the persistent store
87+
try coordinator.destroyPersistentStore(at: storeURL, ofType: NSSQLiteStoreType, options: nil)
88+
os_log("Successfully destroyed Core Data persistent store at: %{public}@", log: .communicationHub, type: .info, storeURL.path)
89+
90+
// Attempt to load the persistent stores one more time
91+
os_log("Attempting to retry loading persistent stores after recreation", log: .communicationHub, type: .info)
92+
self.loadPersistentStoresWithRetry(isRetry: true)
93+
94+
} catch {
95+
os_log("Failed to reset Core Data persistent store: %{public}@", log: .communicationHub, type: .error, error.localizedDescription)
96+
os_log("Falling back to in-memory storage after failed store recreation", log: .communicationHub, type: .error)
97+
self.fallbackToInMemoryStorage()
98+
99+
}
100+
91101
} else {
92-
os_log("Successfully loaded Core Data Communication Hub persistent stores", log: .communicationHub, type: .info)
102+
os_log("Successfully loaded Core Data Communication Hub persistent stores%{public}@", log: .communicationHub, type: .info, isRetry ? " (after retry)" : "")
103+
self.state = .loaded
93104
}
94105
}
95106
}
@@ -102,14 +113,44 @@ final class RCHPersistentContainer: NSPersistentContainer, @unchecked Sendable {
102113

103114
self.loadPersistentStores { description, error in
104115
defer {
105-
self.loaded = true
116+
self.state = .loaded
106117
}
107118

108119
if let error = error {
109120
fatalError("Failed to create in-memory persistent store: \(error)")
110121
}
111122
}
112123
}
124+
125+
private func fallbackToInMemoryStorage() {
126+
os_log("Configuring fallback to in-memory storage due to persistent store failures", log: .communicationHub, type: .info)
127+
128+
// Clean up any existing persistent stores
129+
let coordinator = self.persistentStoreCoordinator
130+
for store in coordinator.persistentStores {
131+
do {
132+
try coordinator.remove(store)
133+
} catch {
134+
os_log("Failed to remove existing persistent store during fallback: %{public}@", log: .communicationHub, type: .error, error.localizedDescription)
135+
}
136+
}
137+
138+
// Configure for in-memory storage
139+
let description = NSPersistentStoreDescription()
140+
description.type = NSInMemoryStoreType
141+
self.persistentStoreDescriptions = [description]
142+
143+
// Load the in-memory store
144+
self.loadPersistentStores { description, error in
145+
if let error = error {
146+
os_log("Critical error: Failed to create in-memory fallback store: %{public}@", log: .communicationHub, type: .fault, error.localizedDescription)
147+
self.state = .failed
148+
} else {
149+
os_log("Successfully configured in-memory fallback storage", log: .communicationHub, type: .info)
150+
self.state = .loaded
151+
}
152+
}
153+
}
113154

114155
/// Drop the database and reset the persistent container. Note that will leave the store in a dropped state, and the app (and Rover SDK) should be restarted afterward.
115156
func reset() {
@@ -135,8 +176,15 @@ final class RCHPersistentContainer: NSPersistentContainer, @unchecked Sendable {
135176
/// Using in-memory, meant for SwiftUI previews and other testing scenarios.
136177
case inMemory
137178
}
179+
180+
enum State: String {
181+
case loading
182+
case loaded
183+
case failed
184+
}
138185
}
139186

187+
140188
// MARK: - Badge Count Extension
141189
extension RCHPersistentContainer {
142190
/// Computes the current badge count based on unread posts.

Sources/Notifications/Communication Hub/Data/RCHSync.swift

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,24 @@ actor RCHSync: ObservableObject, SyncStandaloneParticipant {
5959
}
6060

6161
// Wait for persistence to finish loading before proceeding with sync
62-
let isLoaded = await MainActor.run { persistentContainer.loaded }
63-
if !isLoaded {
62+
let state = await MainActor.run { persistentContainer.state }
63+
if state != .loaded {
6464
os_log(
6565
.debug, log: .communicationHub,
6666
"Sync waiting for persistence (Core Data) to finish initialization")
6767

68-
// Wait for loaded to become true using the publisher
69-
for await loaded in await MainActor.run(body: { persistentContainer.$loaded.values }) {
70-
if loaded {
71-
os_log(.debug, log: .communicationHub, "Persistence loading completed")
72-
break
68+
// Wait for the loaded state before proceeding
69+
waitLoop: for await state in await MainActor.run(body: { persistentContainer.$state.values }) {
70+
switch state {
71+
case .loading:
72+
// continue to wait.
73+
continue waitLoop
74+
case .failed:
75+
os_log("Communication Hub persistent container not available, aborting sync")
76+
return false
77+
case .loaded:
78+
os_log(.debug, log: .communicationHub, "Persistence loading completed, now can sync")
79+
break waitLoop
7380
}
7481
}
7582
}

Tests/NotificationsTests/CommunicationHub/Sync/RCHSyncTriggerTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ final class RCHSyncTriggerTests: RCHSyncTestBase {
5757

5858
// Test that persistence container is properly loaded before sync
5959
// This is handled internally by RCHSync.performActualSync()
60-
let isLoaded = await MainActor.run { testContainer.loaded }
60+
let isLoaded = await MainActor.run { testContainer.state == .loaded }
6161
XCTAssertTrue(isLoaded, "Persistence container should be loaded during app launch sync")
6262
}
6363

0 commit comments

Comments
 (0)