Bladeren bron

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 dag geleden
bovenliggende
commit
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 }
         guard let objects = tempTargetController.fetchedObjects else { return }
         tempTargetStored = objects
         tempTargetStored = objects
     }
     }

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

@@ -355,9 +355,18 @@ extension Home {
 
 
         // MARK: - Fetch window re-anchoring
         // 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() {
         @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(carbsController, with: NSPredicate.carbsForChart) { self.updateCarbsFromController() }
             reanchor(fpuController, with: NSPredicate.fpusForChart) { self.updateFPUsFromController() }
             reanchor(fpuController, with: NSPredicate.fpusForChart) { self.updateFPUsFromController() }
             reanchor(determinationController, with: NSPredicate.determinationsForCobIobCharts) {
             reanchor(determinationController, with: NSPredicate.determinationsForCobIobCharts) {
@@ -367,6 +376,9 @@ extension Home {
             reanchor(overrideRunController, with: NSPredicate.predicateForStartDateOneDayAgo) {
             reanchor(overrideRunController, with: NSPredicate.predicateForStartDateOneDayAgo) {
                 self.updateOverrideRunsFromController()
                 self.updateOverrideRunsFromController()
             }
             }
+            reanchor(tempTargetController, with: NSPredicate.tempTargetsForMainChart) {
+                self.updateTempTargetsFromController()
+            }
             reanchor(tempTargetRunController, with: NSPredicate.predicateForStartDateOneDayAgo) {
             reanchor(tempTargetRunController, with: NSPredicate.predicateForStartDateOneDayAgo) {
                 self.updateTempTargetRunsFromController()
                 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)")
                     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
                 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
             // Update state properties with calculation results on main thread
             await MainActor.run {
             await MainActor.run {
                 targetDifference = result.targetDifference
                 targetDifference = result.targetDifference
@@ -772,6 +776,8 @@ extension Treatments.StateModel: DeterminationObserver, BolusFailureObserver {
 
 
     func bolusDidFail() {
     func bolusDidFail() {
         DispatchQueue.main.async {
         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")
             debug(.bolusState, "bolusDidFail fired")
             self.isAwaitingDeterminationResult = false
             self.isAwaitingDeterminationResult = false
             if self.addButtonPressed {
             if self.addButtonPressed {
@@ -930,7 +936,7 @@ extension Treatments.StateModel {
             simulatedDetermination = forecastData
             simulatedDetermination = forecastData
             debugPrint("\(DebuggingIdentifiers.failed) minPredBG: \(minPredBG)")
             debugPrint("\(DebuggingIdentifiers.failed) minPredBG: \(minPredBG)")
         } else {
         } else {
-            simulatedDetermination = await Task { [self] in
+            let simulated = await Task { [self] in
                 debug(.bolusState, "calling simulateDetermineBasal to get forecast data")
                 debug(.bolusState, "calling simulateDetermineBasal to get forecast data")
                 return await apsManager.simulateDetermineBasal(
                 return await apsManager.simulateDetermineBasal(
                     simulatedCarbsAmount: carbs,
                     simulatedCarbsAmount: carbs,
@@ -939,8 +945,12 @@ extension Treatments.StateModel {
                 )
                 )
             }.value
             }.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
             // Update evBG and minPredBG from simulated determination
-            if let simDetermination = simulatedDetermination {
+            if let simDetermination = simulated {
                 evBG = Decimal(simDetermination.eventualBG ?? 0)
                 evBG = Decimal(simDetermination.eventualBG ?? 0)
                 minPredBG = simDetermination.minPredBGFromReason ?? 0
                 minPredBG = simDetermination.minPredBGFromReason ?? 0
                 debugPrint("\(DebuggingIdentifiers.inProgress) minPredBG: \(minPredBG)")
                 debugPrint("\(DebuggingIdentifiers.inProgress) minPredBG: \(minPredBG)")
@@ -979,8 +989,13 @@ extension Treatments.StateModel {
             }
             }
         }.value
         }.value
 
 
-        minForecast = await minForecastResult
-        maxForecast = await maxForecastResult
+        let minResult = await minForecastResult
+        let maxResult = await maxForecastResult
+
+        guard !Task.isCancelled else { return }
+
+        minForecast = minResult
+        maxForecast = maxResult
     }
     }
 }
 }