Просмотр исходного кода

Guard recompute pipelines against stale runs and teardown races

Check cancellation after every suspension point in Treatments'
recomputes, run the setup guard and FRC wiring in one main-actor
slice, and guard hideModal() on isActive. Also re-anchor the temp
target controller (frozen "date >= now" disjunct) and the chart
markers on foreground.
Marvin Polscheit 1 день назад
Родитель
Сommit
eb5a2e9887

+ 1 - 1
Trio/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift

@@ -19,7 +19,7 @@ extension Home.StateModel {
         }
     }
 
-    @MainActor private func updateTempTargetsFromController() {
+    @MainActor func updateTempTargetsFromController() {
         guard let objects = tempTargetController.fetchedObjects else { return }
         tempTargetStored = objects
     }

+ 14 - 2
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -355,9 +355,18 @@ extension Home {
 
         // MARK: - Fetch window re-anchoring
 
-        /// Called on `UIApplication.willEnterForegroundNotification`
+        //
+        // Window predicates freeze their anchor at build time, so the unbounded controllers
+        // are re-anchored to "now" on foreground. fetchLimit-1 and override controllers keep
+        // the launch anchor on purpose; temp targets must re-anchor ("date >= now" disjunct).
+
+        /// Called on `willEnterForegroundNotification`; idempotent at launch.
         @MainActor func reanchorFetchWindows() {
-            reanchor(glucoseController, with: NSPredicate.glucose) { self.updateGlucoseFromController() }
+            reanchor(glucoseController, with: NSPredicate.glucose) {
+                self.updateGlucoseFromController()
+                // Re-sync the chart domain even if no new reading arrived while backgrounded.
+                self.updateStartEndMarkers()
+            }
             reanchor(carbsController, with: NSPredicate.carbsForChart) { self.updateCarbsFromController() }
             reanchor(fpuController, with: NSPredicate.fpusForChart) { self.updateFPUsFromController() }
             reanchor(determinationController, with: NSPredicate.determinationsForCobIobCharts) {
@@ -367,6 +376,9 @@ extension Home {
             reanchor(overrideRunController, with: NSPredicate.predicateForStartDateOneDayAgo) {
                 self.updateOverrideRunsFromController()
             }
+            reanchor(tempTargetController, with: NSPredicate.tempTargetsForMainChart) {
+                self.updateTempTargetsFromController()
+            }
             reanchor(tempTargetRunController, with: NSPredicate.predicateForStartDateOneDayAgo) {
                 self.updateTempTargetRunsFromController()
             }

+ 25 - 10
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -262,12 +262,13 @@ extension Treatments {
                     debug(.default, "Failed to setup bolus state concurrently: \(error)")
                 }
 
-                guard !Task.isCancelled else { return }
-
-                // NSFetchedResultsControllers are bound to the viewContext, so set them up on the main actor.
-                await self.setupGlucoseController()
-                await self.setupDeterminationController()
-                await self.setupLastBolusController()
+                // viewContext-bound FRCs: guard and wiring share one main-actor slice.
+                await MainActor.run {
+                    guard !Task.isCancelled, !self.hasCleanedUp else { return }
+                    self.setupGlucoseController()
+                    self.setupDeterminationController()
+                    self.setupLastBolusController()
+                }
             }
         }
 
@@ -457,6 +458,9 @@ extension Treatments {
                 isBackdated: isBackdated
             )
 
+            // A superseded run must not overwrite the breakdown a newer run published.
+            guard !Task.isCancelled else { return apsManager.roundBolus(amount: result.insulinCalculated) }
+
             // Update state properties with calculation results on main thread
             await MainActor.run {
                 targetDifference = result.targetDifference
@@ -772,6 +776,8 @@ extension Treatments.StateModel: DeterminationObserver, BolusFailureObserver {
 
     func bolusDidFail() {
         DispatchQueue.main.async {
+            // A dismissed instance may still observe until dealloc — don't hide an unrelated modal.
+            guard self.isActive else { return }
             debug(.bolusState, "bolusDidFail fired")
             self.isAwaitingDeterminationResult = false
             if self.addButtonPressed {
@@ -930,7 +936,7 @@ extension Treatments.StateModel {
             simulatedDetermination = forecastData
             debugPrint("\(DebuggingIdentifiers.failed) minPredBG: \(minPredBG)")
         } else {
-            simulatedDetermination = await Task { [self] in
+            let simulated = await Task { [self] in
                 debug(.bolusState, "calling simulateDetermineBasal to get forecast data")
                 return await apsManager.simulateDetermineBasal(
                     simulatedCarbsAmount: carbs,
@@ -939,8 +945,12 @@ extension Treatments.StateModel {
                 )
             }.value
 
+            // Stale minPredBG/cob from a superseded run would feed the next bolus calculation.
+            guard !Task.isCancelled else { return }
+            simulatedDetermination = simulated
+
             // Update evBG and minPredBG from simulated determination
-            if let simDetermination = simulatedDetermination {
+            if let simDetermination = simulated {
                 evBG = Decimal(simDetermination.eventualBG ?? 0)
                 minPredBG = simDetermination.minPredBGFromReason ?? 0
                 debugPrint("\(DebuggingIdentifiers.inProgress) minPredBG: \(minPredBG)")
@@ -979,8 +989,13 @@ extension Treatments.StateModel {
             }
         }.value
 
-        minForecast = await minForecastResult
-        maxForecast = await maxForecastResult
+        let minResult = await minForecastResult
+        let maxResult = await maxForecastResult
+
+        guard !Task.isCancelled else { return }
+
+        minForecast = minResult
+        maxForecast = maxResult
     }
 }