Skip to content

Commit

Permalink
LOOP-5088 Update Loop for LoopKit api changes for avoiding thread blo…
Browse files Browse the repository at this point in the history
…cking (#712)

* Update Loop for LoopKit api changes for avoiding thread blocking

* Fix non-deterministic test behavior

* Updates to use latest LoopAlgorithm package
  • Loading branch information
ps2 authored Oct 11, 2024
1 parent 63c11b4 commit 72078f0
Show file tree
Hide file tree
Showing 15 changed files with 185 additions and 176 deletions.
39 changes: 19 additions & 20 deletions Loop Widget Extension/Timeline/StatusWidgetTimelineProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,16 @@ class StatusWidgetTimelineProvider: TimelineProvider {
store: cacheStore,
expireAfter: localCacheDuration)

lazy var glucoseStore = GlucoseStore(
cacheStore: cacheStore,
provenanceIdentifier: HKSource.default().bundleIdentifier
)
var glucoseStore: GlucoseStore!

init() {
Task {
glucoseStore = await GlucoseStore(
cacheStore: cacheStore,
provenanceIdentifier: HKSource.default().bundleIdentifier
)
}
}

func placeholder(in context: Context) -> StatusWidgetTimelimeEntry {
log.default("%{public}@: context=%{public}@", #function, String(describing: context))
Expand Down Expand Up @@ -90,29 +96,22 @@ class StatusWidgetTimelineProvider: TimelineProvider {
}

func update(completion: @escaping (StatusWidgetTimelimeEntry) -> Void) {
let group = DispatchGroup()

var glucose: [StoredGlucoseSample] = []

let startDate = Date(timeIntervalSinceNow: -LoopAlgorithm.inputDataRecencyInterval)

group.enter()
glucoseStore.getGlucoseSamples(start: startDate) { (result) in
switch result {
case .failure:
Task {

var glucose: [StoredGlucoseSample] = []

do {
glucose = try await glucoseStore.getGlucoseSamples(start: startDate)
self.log.default("Fetched glucose: last = %{public}@, %{public}@", String(describing: glucose.last?.startDate), String(describing: glucose.last?.quantity))
} catch {
self.log.error("Failed to fetch glucose after %{public}@", String(describing: startDate))
glucose = []
case .success(let samples):
self.log.default("Fetched glucose: last = %{public}@, %{public}@", String(describing: samples.last?.startDate), String(describing: samples.last?.quantity))
glucose = samples
}
group.leave()
}
group.wait()

let finalGlucose = glucose
let finalGlucose = glucose

Task { @MainActor in
guard let defaults = self.defaults,
let context = defaults.statusExtensionContext,
let contextUpdatedAt = context.createdAt,
Expand Down
4 changes: 2 additions & 2 deletions Loop/Extensions/GlucoseStore+SimulatedCoreData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ extension GlucoseStore {
return addError
}

func purgeHistoricalGlucoseObjects(completion: @escaping (Error?) -> Void) {
purgeCachedGlucoseObjects(before: historicalEndDate, completion: completion)
func purgeHistoricalGlucoseObjects() async throws {
try await purgeCachedGlucoseObjects(before: historicalEndDate)
}
}

Expand Down
44 changes: 18 additions & 26 deletions Loop/Managers/CGMStalenessMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import LoopCore
import LoopAlgorithm

protocol CGMStalenessMonitorDelegate: AnyObject {
func getLatestCGMGlucose(since: Date, completion: @escaping (_ result: Swift.Result<StoredGlucoseSample?, Error>) -> Void)
func getLatestCGMGlucose(since: Date) async throws -> StoredGlucoseSample?
}

class CGMStalenessMonitor {
Expand All @@ -21,13 +21,7 @@ class CGMStalenessMonitor {

private var cgmStalenessTimer: Timer?

weak var delegate: CGMStalenessMonitorDelegate? = nil {
didSet {
if delegate != nil {
checkCGMStaleness()
}
}
}
weak var delegate: CGMStalenessMonitorDelegate?

@Published var cgmDataIsStale: Bool = true {
didSet {
Expand Down Expand Up @@ -57,29 +51,27 @@ class CGMStalenessMonitor {
cgmStalenessTimer?.invalidate()
cgmStalenessTimer = Timer.scheduledTimer(withTimeInterval: expiration.timeIntervalSinceNow, repeats: false) { [weak self] _ in
self?.log.debug("cgmStalenessTimer fired")
self?.checkCGMStaleness()
Task {
await self?.checkCGMStaleness()
}
}
cgmStalenessTimer?.tolerance = CGMStalenessMonitor.cgmStalenessTimerTolerance
}

private func checkCGMStaleness() {
delegate?.getLatestCGMGlucose(since: Date(timeIntervalSinceNow: -LoopAlgorithm.inputDataRecencyInterval)) { (result) in
DispatchQueue.main.async {
self.log.debug("Fetched latest CGM Glucose for checkCGMStaleness: %{public}@", String(describing: result))
switch result {
case .success(let sample):
if let sample = sample {
self.cgmDataIsStale = false
self.updateCGMStalenessTimer(expiration: sample.startDate.addingTimeInterval(LoopAlgorithm.inputDataRecencyInterval + CGMStalenessMonitor.cgmStalenessTimerTolerance))
} else {
self.cgmDataIsStale = true
}
case .failure(let error):
self.log.error("Unable to get latest CGM clucose: %{public}@ ", String(describing: error))
// Some kind of system error; check again in 5 minutes
self.updateCGMStalenessTimer(expiration: Date(timeIntervalSinceNow: .minutes(5)))
}
func checkCGMStaleness() async {
do {
let sample = try await delegate?.getLatestCGMGlucose(since: Date(timeIntervalSinceNow: -LoopAlgorithm.inputDataRecencyInterval))
self.log.debug("Fetched latest CGM Glucose for checkCGMStaleness: %{public}@", String(describing: sample))
if let sample = sample {
self.cgmDataIsStale = false
self.updateCGMStalenessTimer(expiration: sample.startDate.addingTimeInterval(LoopAlgorithm.inputDataRecencyInterval + CGMStalenessMonitor.cgmStalenessTimerTolerance))
} else {
self.cgmDataIsStale = true
}
} catch {
self.log.error("Unable to get latest CGM clucose: %{public}@ ", String(describing: error))
// Some kind of system error; check again in 5 minutes
self.updateCGMStalenessTimer(expiration: Date(timeIntervalSinceNow: .minutes(5)))
}
}
}
18 changes: 2 additions & 16 deletions Loop/Managers/CriticalEventLogExportManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -199,16 +199,6 @@ public class CriticalEventLogExportManager {
calendar.timeZone = TimeZone(identifier: "UTC")!
return calendar
}()

// MARK: - Background Tasks

func registerBackgroundTasks() {
if Self.registerCriticalEventLogHistoricalExportBackgroundTask({ self.handleCriticalEventLogHistoricalExportBackgroundTask($0) }) {
log.debug("Critical event log export background task registered")
} else {
log.error("Critical event log export background task not registered")
}
}
}

// MARK: - CriticalEventLogBaseExporter
Expand Down Expand Up @@ -567,11 +557,7 @@ fileprivate extension FileManager {
// MARK: - Critical Event Log Export

extension CriticalEventLogExportManager {
private static var criticalEventLogHistoricalExportBackgroundTaskIdentifier: String { "com.loopkit.background-task.critical-event-log.historical-export" }

public static func registerCriticalEventLogHistoricalExportBackgroundTask(_ handler: @escaping (BGProcessingTask) -> Void) -> Bool {
return BGTaskScheduler.shared.register(forTaskWithIdentifier: criticalEventLogHistoricalExportBackgroundTaskIdentifier, using: nil) { handler($0 as! BGProcessingTask) }
}
static var historicalExportBackgroundTaskIdentifier: String { "com.loopkit.background-task.critical-event-log.historical-export" }

public func handleCriticalEventLogHistoricalExportBackgroundTask(_ task: BGProcessingTask) {
dispatchPrecondition(condition: .notOnQueue(.main))
Expand Down Expand Up @@ -602,7 +588,7 @@ extension CriticalEventLogExportManager {
public func scheduleCriticalEventLogHistoricalExportBackgroundTask(isRetry: Bool = false) {
do {
let earliestBeginDate = isRetry ? retryExportHistoricalDate() : nextExportHistoricalDate()
let request = BGProcessingTaskRequest(identifier: Self.criticalEventLogHistoricalExportBackgroundTaskIdentifier)
let request = BGProcessingTaskRequest(identifier: Self.historicalExportBackgroundTaskIdentifier)
request.earliestBeginDate = earliestBeginDate
request.requiresExternalPower = true

Expand Down
59 changes: 33 additions & 26 deletions Loop/Managers/DeviceDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,11 @@ final class DeviceDataManager {
glucoseStore.delegate = self
cgmEventStore.delegate = self
doseStore.insulinDeliveryStore.delegate = self


Task {
await cgmStalenessMonitor.checkCGMStaleness()
}

setupPump()
setupCGM()

Expand Down Expand Up @@ -1179,28 +1183,25 @@ extension DeviceDataManager {
return
}

let devicePredicate = HKQuery.predicateForObjects(from: [testingPumpManager.testingDevice])
let insulinDeliveryStore = doseStore.insulinDeliveryStore

Task {
do {
try await doseStore.resetPumpData()
} catch {
completion?(error)
return
}

let insulinSharingDenied = self.healthStore.authorizationStatus(for: HealthKitSampleStore.insulinQuantityType) == .sharingDenied
guard !insulinSharingDenied else {
// only clear cache since access to health kit is denied
insulinDeliveryStore.purgeCachedInsulinDeliveryObjects() { error in
completion?(error)
let insulinSharingDenied = self.healthStore.authorizationStatus(for: HealthKitSampleStore.insulinQuantityType) == .sharingDenied
guard !insulinSharingDenied else {
// only clear cache since access to health kit is denied
await insulinDeliveryStore.purgeCachedInsulinDeliveryObjects()
completion?(nil)
return
}
return
}

insulinDeliveryStore.purgeAllDoseEntries(healthKitPredicate: devicePredicate) { error in

try await insulinDeliveryStore.purgeDoseEntriesForDevice(testingPumpManager.testingDevice)
completion?(nil)
} catch {
completion?(error)
return
}
}
}
Expand All @@ -1210,19 +1211,25 @@ extension DeviceDataManager {
completion?(nil)
return
}

let glucoseSharingDenied = self.healthStore.authorizationStatus(for: HealthKitSampleStore.glucoseType) == .sharingDenied
guard !glucoseSharingDenied else {
// only clear cache since access to health kit is denied
glucoseStore.purgeCachedGlucoseObjects() { error in
completion?(error)

Task {
let glucoseSharingDenied = self.healthStore.authorizationStatus(for: HealthKitSampleStore.glucoseType) == .sharingDenied
guard !glucoseSharingDenied else {
// only clear cache since access to health kit is denied
do {
try await glucoseStore.purgeCachedGlucoseObjects()
} catch {
completion?(error)
}
return
}
return
}

let predicate = HKQuery.predicateForObjects(from: [testingCGMManager.testingDevice])
glucoseStore.purgeAllGlucoseSamples(healthKitPredicate: predicate) { error in
completion?(error)
do {
try await glucoseStore.purgeAllGlucose(for: testingCGMManager.testingDevice)
completion?(nil)
} catch {
completion?(error)
}
}
}
}
Expand Down
35 changes: 23 additions & 12 deletions Loop/Managers/LoopAppManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import UIKit
import Intents
import BackgroundTasks
import Combine
import LoopKit
import LoopKitUI
Expand Down Expand Up @@ -133,9 +134,27 @@ class LoopAppManager: NSObject {
self.state = state.next
}

func registerBackgroundTasks() {
let taskIdentifier = CriticalEventLogExportManager.historicalExportBackgroundTaskIdentifier
let registered = BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil) { task in
guard let criticalEventLogExportManager = self.criticalEventLogExportManager else {
self.log.error("Critical event log export launch handler called before initialization complete!")
return
}
criticalEventLogExportManager.handleCriticalEventLogHistoricalExportBackgroundTask(task as! BGProcessingTask)
}
if registered {
log.debug("Critical event log export background task registered")
} else {
log.error("Critical event log export background task not registered")
}
}

func launch() {
precondition(isLaunchPending)

registerBackgroundTasks()

Task {
await resumeLaunch()
}
Expand Down Expand Up @@ -248,7 +267,7 @@ class LoopAppManager: NSObject {
observationStart: Date().addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval)
)

self.doseStore = DoseStore(
self.doseStore = await DoseStore(
healthKitSampleStore: insulinHealthStore,
cacheStore: cacheStore,
cacheLength: localCacheDuration,
Expand All @@ -263,7 +282,7 @@ class LoopAppManager: NSObject {
observationStart: Date().addingTimeInterval(-.hours(24))
)

self.glucoseStore = GlucoseStore(
self.glucoseStore = await GlucoseStore(
healthKitSampleStore: glucoseHealthStore,
cacheStore: cacheStore,
cacheLength: localCacheDuration,
Expand Down Expand Up @@ -390,9 +409,6 @@ class LoopAppManager: NSObject {
directory: FileManager.default.exportsDirectoryURL,
historicalDuration: localCacheDuration)

criticalEventLogExportManager.registerBackgroundTasks()


statusExtensionManager = ExtensionDataManager(
deviceDataManager: deviceDataManager,
loopDataManager: loopDataManager,
Expand Down Expand Up @@ -1045,6 +1061,7 @@ extension LoopAppManager: SimulatedData {
Task { @MainActor in
do {
try await self.doseStore.purgeHistoricalPumpEvents()
try await self.glucoseStore.purgeHistoricalGlucoseObjects()
} catch {
completion(error)
return
Expand All @@ -1059,13 +1076,7 @@ extension LoopAppManager: SimulatedData {
completion(error)
return
}
self.glucoseStore.purgeHistoricalGlucoseObjects() { error in
guard error == nil else {
completion(error)
return
}
self.settingsManager.purgeHistoricalSettingsObjects(completion: completion)
}
self.settingsManager.purgeHistoricalSettingsObjects(completion: completion)
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion Loop/Managers/LoopDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,8 @@ final class LoopDataManager: ObservableObject {
carbAbsorptionModel: carbAbsorptionModel,
recommendationInsulinModel: insulinModel(for: deliveryDelegate?.pumpInsulinType ?? .novolog),
recommendationType: .manualBolus,
automaticBolusApplicationFactor: effectiveBolusApplicationFactor)
automaticBolusApplicationFactor: effectiveBolusApplicationFactor,
useMidAbsorptionISF: false)
}

func loopingReEnabled() async {
Expand Down
Loading

0 comments on commit 72078f0

Please sign in to comment.