Procházet zdrojové kódy

Fix additional thread safety issues in APSManager

- Remove nested `privateContext.perform` in `tir()` that
  silently discarded the return value; the function is already called from
  within a perform block
- Make `loopStats(loopStatRecord:)` async with `await privateContext.perform`
  and `[weak self]` to prevent retain cycles and ensure completion before
  proceeding
- Add `[weak self]` to `pumpManager(_:didUpdate:)` perform closure to
  prevent retain cycle
- Simplify defer block in `loop()` by removing unnecessary `Optional()` wrap
- Remove unused `viewContext` property
Marvin Polscheit před 1 měsícem
rodič
revize
c4c0ee54bf
1 změnil soubory, kde provedl 31 přidání a 30 odebrání
  1. 31 30
      Trio/Sources/APS/APSManager.swift

+ 31 - 30
Trio/Sources/APS/APSManager.swift

@@ -135,7 +135,6 @@ final class BaseAPSManager: APSManager, Injectable {
         }
     }
 
-    let viewContext = CoreDataStack.shared.persistentContainer.viewContext
     let privateContext = CoreDataStack.shared.newTaskContext()
 
     private var openAPS: OpenAPS!
@@ -304,7 +303,7 @@ final class BaseAPSManager: APSManager, Injectable {
             )
 
             defer {
-                if let bgTask = Optional(bgTask), bgTask != .invalid {
+                if bgTask != .invalid {
                     Task { await UIApplication.shared.endBackgroundTask(bgTask) }
                 }
             }
@@ -377,7 +376,7 @@ final class BaseAPSManager: APSManager, Injectable {
             lastError.send(nil)
         }
 
-        loopStats(loopStatRecord: loopStatRecord)
+        await loopStats(loopStatRecord: loopStatRecord)
 
         if settings.closedLoop {
             await reportEnacted(wasEnacted: error == nil)
@@ -823,31 +822,31 @@ final class BaseAPSManager: APSManager, Injectable {
         return Double(sorted[length / 2])
     }
 
+    /// Calculates Time in Range statistics.
+    /// Must be called from within a `privateContext.perform` block (e.g. from `glucoseForStats()`).
     private func tir(_ glucose: [GlucoseStored]) -> (TIR: Double, hypos: Double, hypers: Double, normal_: Double) {
-        privateContext.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
+        // Euglycemic 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])
@@ -1180,8 +1179,9 @@ final class BaseAPSManager: APSManager, Injectable {
         }
     }
 
-    private func loopStats(loopStatRecord: LoopStats) {
-        privateContext.perform {
+    private func loopStats(loopStatRecord: LoopStats) async {
+        await privateContext.perform { [weak self] in
+            guard let self else { return }
             let nLS = LoopStatRecord(context: self.privateContext)
             nLS.start = loopStatRecord.start
             nLS.end = loopStatRecord.end ?? Date()
@@ -1292,7 +1292,8 @@ extension BaseAPSManager: PumpManagerStatusObserver {
     func pumpManager(_: PumpManager, didUpdate status: PumpManagerStatus, oldStatus _: PumpManagerStatus) {
         let percent = Int((status.pumpBatteryChargeRemaining ?? 1) * 100)
 
-        privateContext.perform {
+        privateContext.perform { [weak self] in
+            guard let self else { return }
             /// only update the last item with the current battery infos instead of saving a new one each time
             let fetchRequest: NSFetchRequest<OpenAPS_Battery> = OpenAPS_Battery.fetchRequest()
             fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]