Jelajahi Sumber

Fix crash in APSManager due to wrong context usage

Marvin Polscheit 1 bulan lalu
induk
melakukan
8afda2d150

+ 76 - 62
Trio/Sources/APS/APSManager.swift

@@ -442,17 +442,27 @@ final class BaseAPSManager: APSManager, Injectable {
 
 
         try await calculateAndStoreTDD()
         try await calculateAndStoreTDD()
 
 
-        // Fetch glucose asynchronously
-        let glucose = try await fetchGlucose(predicate: NSPredicate.predicateForOneHourAgo, fetchLimit: 6)
-
         var invalidGlucoseError: String?
         var invalidGlucoseError: String?
 
 
-        // Perform the context-related checks and actions
+        // Fetch glucose and run validation on the same context to avoid cross-context property access.
         let validationContext = CoreDataStack.shared.newTaskContext()
         let validationContext = CoreDataStack.shared.newTaskContext()
         validationContext.name = "determineBasal.validation"
         validationContext.name = "determineBasal.validation"
+
         let isValidGlucoseData = await validationContext.perform { [weak self] in
         let isValidGlucoseData = await validationContext.perform { [weak self] in
             guard let self else { return false }
             guard let self else { return false }
 
 
+            let glucose: [GlucoseStored]
+            do {
+                glucose = try self.fetchGlucose(
+                    on: validationContext,
+                    predicate: NSPredicate.predicateForOneHourAgo,
+                    fetchLimit: 6
+                )
+            } catch {
+                debug(.apsManager, "Failed to fetch glucose for validation: \(error)")
+                return false
+            }
+
             guard glucose.count > 2 else {
             guard glucose.count > 2 else {
                 debug(.apsManager, "Not enough glucose data")
                 debug(.apsManager, "Not enough glucose data")
                 invalidGlucoseError =
                 invalidGlucoseError =
@@ -833,33 +843,30 @@ final class BaseAPSManager: APSManager, Injectable {
         return Double(sorted[length / 2])
         return Double(sorted[length / 2])
     }
     }
 
 
+    /// Must be called from within a `perform`/`performAndWait` block on the context that owns `glucose`.
     private func tir(_ glucose: [GlucoseStored]) -> (TIR: Double, hypos: Double, hypers: Double, normal_: Double) {
     private func tir(_ glucose: [GlucoseStored]) -> (TIR: Double, hypos: Double, hypers: Double, normal_: Double) {
-        let context = CoreDataStack.shared.newTaskContext()
-        context.name = "tir"
-        return context.perform {
-            let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
-            let totalReadings = justGlucoseArray.count
-            let highLimit = settingsManager.settings.high
-            let lowLimit = settingsManager.settings.low
-            let hyperArray = glucose.filter({ $0.glucose >= Int(highLimit) })
-            let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count
-            let hyperPercentage = Double(hyperReadings) / Double(totalReadings) * 100
-            let hypoArray = glucose.filter({ $0.glucose <= Int(lowLimit) })
-            let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count
-            let hypoPercentage = Double(hypoReadings) / Double(totalReadings) * 100
-            // Euglyccemic range
-            let normalArray = glucose.filter({ $0.glucose >= 70 && $0.glucose <= 140 })
-            let normalReadings = normalArray.compactMap({ each in each.glucose as Int16 }).count
-            let normalPercentage = Double(normalReadings) / Double(totalReadings) * 100
-            // TIR
-            let tir = 100 - (hypoPercentage + hyperPercentage)
-            return (
-                roundDouble(tir, 1),
-                roundDouble(hypoPercentage, 1),
-                roundDouble(hyperPercentage, 1),
-                roundDouble(normalPercentage, 1)
-            )
-        }
+        let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
+        let totalReadings = justGlucoseArray.count
+        let highLimit = settingsManager.settings.high
+        let lowLimit = settingsManager.settings.low
+        let hyperArray = glucose.filter({ $0.glucose >= Int(highLimit) })
+        let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count
+        let hyperPercentage = Double(hyperReadings) / Double(totalReadings) * 100
+        let hypoArray = glucose.filter({ $0.glucose <= Int(lowLimit) })
+        let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count
+        let hypoPercentage = Double(hypoReadings) / Double(totalReadings) * 100
+        // Euglyccemic range
+        let normalArray = glucose.filter({ $0.glucose >= 70 && $0.glucose <= 140 })
+        let normalReadings = normalArray.compactMap({ each in each.glucose as Int16 }).count
+        let normalPercentage = Double(normalReadings) / Double(totalReadings) * 100
+        // TIR
+        let tir = 100 - (hypoPercentage + hyperPercentage)
+        return (
+            roundDouble(tir, 1),
+            roundDouble(hypoPercentage, 1),
+            roundDouble(hyperPercentage, 1),
+            roundDouble(normalPercentage, 1)
+        )
     }
     }
 
 
     private func glucoseStats(_ fetchedGlucose: [GlucoseStored])
     private func glucoseStats(_ fetchedGlucose: [GlucoseStored])
@@ -954,11 +961,14 @@ final class BaseAPSManager: APSManager, Injectable {
         return output
         return output
     }
     }
 
 
-    // fetch glucose for time interval
-    func fetchGlucose(predicate: NSPredicate, fetchLimit: Int? = nil, batchSize: Int? = nil) async throws -> [GlucoseStored] {
-        let context = CoreDataStack.shared.newTaskContext()
-        context.name = "fetchGlucose"
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+    /// Synchronously fetches glucose on the given context. Must be called from within a `perform`/`performAndWait` block of that context
+    func fetchGlucose(
+        on context: NSManagedObjectContext,
+        predicate: NSPredicate,
+        fetchLimit: Int? = nil,
+        batchSize: Int? = nil
+    ) throws -> [GlucoseStored] {
+        let results = try CoreDataStack.shared.fetchEntities(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: context,
             onContext: context,
             predicate: predicate,
             predicate: predicate,
@@ -967,14 +977,10 @@ final class BaseAPSManager: APSManager, Injectable {
             fetchLimit: fetchLimit,
             fetchLimit: fetchLimit,
             batchSize: batchSize
             batchSize: batchSize
         )
         )
-
-        return try await context.perform {
-            guard let glucoseResults = results as? [GlucoseStored] else {
-                throw CoreDataError.fetchError(function: #function, file: #file)
-            }
-
-            return glucoseResults
+        guard let glucoseResults = results as? [GlucoseStored] else {
+            throw CoreDataError.fetchError(function: #function, file: #file)
         }
         }
+        return glucoseResults
     }
     }
 
 
     private func lastLoopForStats() async -> Date? {
     private func lastLoopForStats() async -> Date? {
@@ -1058,28 +1064,36 @@ final class BaseAPSManager: APSManager, Injectable {
         hbs: Durations,
         hbs: Durations,
         variance: Variance
         variance: Variance
     )? {
     )? {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "glucoseForStats"
         do {
         do {
-            // Get the Glucose Values
-            let glucose24h = try await fetchGlucose(predicate: NSPredicate.predicateForOneDayAgo, fetchLimit: 288, batchSize: 50)
-            let glucoseOneWeek = try await fetchGlucose(
-                predicate: NSPredicate.predicateForOneWeek,
-                fetchLimit: 288 * 7,
-                batchSize: 250
-            )
-            let glucoseOneMonth = try await fetchGlucose(
-                predicate: NSPredicate.predicateForOneMonth,
-                fetchLimit: 288 * 7 * 30,
-                batchSize: 500
-            )
-            let glucoseThreeMonths = try await fetchGlucose(
-                predicate: NSPredicate.predicateForThreeMonths,
-                fetchLimit: 288 * 7 * 30 * 3,
-                batchSize: 1000
-            )
+            return try await context.perform {
+                // Fetch all windows on the same context so subsequent property access is safe.
+                let glucose24h = try self.fetchGlucose(
+                    on: context,
+                    predicate: NSPredicate.predicateForOneDayAgo,
+                    fetchLimit: 288,
+                    batchSize: 50
+                )
+                let glucoseOneWeek = try self.fetchGlucose(
+                    on: context,
+                    predicate: NSPredicate.predicateForOneWeek,
+                    fetchLimit: 288 * 7,
+                    batchSize: 250
+                )
+                let glucoseOneMonth = try self.fetchGlucose(
+                    on: context,
+                    predicate: NSPredicate.predicateForOneMonth,
+                    fetchLimit: 288 * 7 * 30,
+                    batchSize: 500
+                )
+                let glucoseThreeMonths = try self.fetchGlucose(
+                    on: context,
+                    predicate: NSPredicate.predicateForThreeMonths,
+                    fetchLimit: 288 * 7 * 30 * 3,
+                    batchSize: 1000
+                )
 
 
-            let context = CoreDataStack.shared.newTaskContext()
-            context.name = "glucoseForStats"
-            return await context.perform {
                 let units = self.settingsManager.settings.units
                 let units = self.settingsManager.settings.units
 
 
                 // First date
                 // First date

+ 9 - 2
Trio/Sources/Modules/Stat/StatStateModel+Setup/TDDSetup.swift

@@ -43,7 +43,12 @@ extension Stat.StateModel {
         let tddResults = try await fetchTDDStoredRecords(on: tddTaskContext)
         let tddResults = try await fetchTDDStoredRecords(on: tddTaskContext)
 
 
         // Fetch data for hourly statistics (BolusStored and TempBasalStored for day view)
         // Fetch data for hourly statistics (BolusStored and TempBasalStored for day view)
-        let (bolusResults, tempBasalResults, suspendEvents, resumeEvents) = try await fetchHourlyInsulinRecords(on: tddTaskContext)
+        let (
+            bolusResults,
+            tempBasalResults,
+            suspendEvents,
+            resumeEvents
+        ) = try await fetchHourlyInsulinRecords(on: tddTaskContext)
 
 
         // MARK: - Process Data on Background Context
         // MARK: - Process Data on Background Context
 
 
@@ -99,7 +104,9 @@ extension Stat.StateModel {
     /// Fetches BolusStored and TempBasalStored records from CoreData for hourly statistics
     /// Fetches BolusStored and TempBasalStored records from CoreData for hourly statistics
     /// - Returns: A tuple containing the results of both fetch requests
     /// - Returns: A tuple containing the results of both fetch requests
     /// - Note: Fetches records from the last 20 days for detailed hourly view
     /// - Note: Fetches records from the last 20 days for detailed hourly view
-    private func fetchHourlyInsulinRecords(on tddTaskContext: NSManagedObjectContext) async throws -> (bolus: Any, tempBasal: Any, suspendEvents: Any, resumeEvents: Any) {
+    private func fetchHourlyInsulinRecords(on tddTaskContext: NSManagedObjectContext) async throws
+        -> (bolus: Any, tempBasal: Any, suspendEvents: Any, resumeEvents: Any)
+    {
         // Calculate date range for hourly statistics (last 20 days)
         // Calculate date range for hourly statistics (last 20 days)
         let now = Date()
         let now = Date()
         let twentyDaysAgo = Calendar.current.date(byAdding: .day, value: -20, to: now) ?? now
         let twentyDaysAgo = Calendar.current.date(byAdding: .day, value: -20, to: now) ?? now

+ 3 - 3
Trio/Sources/Services/Network/TidepoolManager.swift

@@ -467,7 +467,7 @@ extension BaseTidepoolManager {
 
 
         // Caller (uploadDose) already executes within context.perform, so we run directly here
         // Caller (uploadDose) already executes within context.perform, so we run directly here
         guard let duration = event.duration, let amount = event.amount,
         guard let duration = event.duration, let amount = event.amount,
-              let currentBasalRate = self.getCurrentBasalRate()
+              let currentBasalRate = getCurrentBasalRate()
         else {
         else {
             return insulinDoseEvents
             return insulinDoseEvents
         }
         }
@@ -504,7 +504,7 @@ extension BaseTidepoolManager {
                             unit: .units,
                             unit: .units,
                             deliveredUnits: adjustedDeliveredUnits,
                             deliveredUnits: adjustedDeliveredUnits,
                             syncIdentifier: predecessorEntrySyncIdentifier,
                             syncIdentifier: predecessorEntrySyncIdentifier,
-                            insulinType: self.apsManager.pumpManager?.status.insulinType ?? nil,
+                            insulinType: apsManager.pumpManager?.status.insulinType ?? nil,
                             automatic: true,
                             automatic: true,
                             manuallyEntered: false,
                             manuallyEntered: false,
                             isMutable: false
                             isMutable: false
@@ -529,7 +529,7 @@ extension BaseTidepoolManager {
                     unit: .internationalUnitsPerHour,
                     unit: .internationalUnitsPerHour,
                     doubleValue: Double(currentBasalRate.rate)
                     doubleValue: Double(currentBasalRate.rate)
                 ),
                 ),
-                insulinType: self.apsManager.pumpManager?.status.insulinType ?? nil,
+                insulinType: apsManager.pumpManager?.status.insulinType ?? nil,
                 automatic: true,
                 automatic: true,
                 manuallyEntered: false,
                 manuallyEntered: false,
                 isMutable: false
                 isMutable: false