Skip to content

Commit 72078f0

Browse files
authored
LOOP-5088 Update Loop for LoopKit api changes for avoiding thread blocking (#712)
* Update Loop for LoopKit api changes for avoiding thread blocking * Fix non-deterministic test behavior * Updates to use latest LoopAlgorithm package
1 parent 63c11b4 commit 72078f0

15 files changed

+185
-176
lines changed

Loop Widget Extension/Timeline/StatusWidgetTimelineProvider.swift

+19-20
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,16 @@ class StatusWidgetTimelineProvider: TimelineProvider {
3030
store: cacheStore,
3131
expireAfter: localCacheDuration)
3232

33-
lazy var glucoseStore = GlucoseStore(
34-
cacheStore: cacheStore,
35-
provenanceIdentifier: HKSource.default().bundleIdentifier
36-
)
33+
var glucoseStore: GlucoseStore!
34+
35+
init() {
36+
Task {
37+
glucoseStore = await GlucoseStore(
38+
cacheStore: cacheStore,
39+
provenanceIdentifier: HKSource.default().bundleIdentifier
40+
)
41+
}
42+
}
3743

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

9298
func update(completion: @escaping (StatusWidgetTimelimeEntry) -> Void) {
93-
let group = DispatchGroup()
94-
95-
var glucose: [StoredGlucoseSample] = []
9699

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

99-
group.enter()
100-
glucoseStore.getGlucoseSamples(start: startDate) { (result) in
101-
switch result {
102-
case .failure:
102+
Task {
103+
104+
var glucose: [StoredGlucoseSample] = []
105+
106+
do {
107+
glucose = try await glucoseStore.getGlucoseSamples(start: startDate)
108+
self.log.default("Fetched glucose: last = %{public}@, %{public}@", String(describing: glucose.last?.startDate), String(describing: glucose.last?.quantity))
109+
} catch {
103110
self.log.error("Failed to fetch glucose after %{public}@", String(describing: startDate))
104-
glucose = []
105-
case .success(let samples):
106-
self.log.default("Fetched glucose: last = %{public}@, %{public}@", String(describing: samples.last?.startDate), String(describing: samples.last?.quantity))
107-
glucose = samples
108111
}
109-
group.leave()
110-
}
111-
group.wait()
112112

113-
let finalGlucose = glucose
113+
let finalGlucose = glucose
114114

115-
Task { @MainActor in
116115
guard let defaults = self.defaults,
117116
let context = defaults.statusExtensionContext,
118117
let contextUpdatedAt = context.createdAt,

Loop/Extensions/GlucoseStore+SimulatedCoreData.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ extension GlucoseStore {
8282
return addError
8383
}
8484

85-
func purgeHistoricalGlucoseObjects(completion: @escaping (Error?) -> Void) {
86-
purgeCachedGlucoseObjects(before: historicalEndDate, completion: completion)
85+
func purgeHistoricalGlucoseObjects() async throws {
86+
try await purgeCachedGlucoseObjects(before: historicalEndDate)
8787
}
8888
}
8989

Loop/Managers/CGMStalenessMonitor.swift

+18-26
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import LoopCore
1212
import LoopAlgorithm
1313

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

1818
class CGMStalenessMonitor {
@@ -21,13 +21,7 @@ class CGMStalenessMonitor {
2121

2222
private var cgmStalenessTimer: Timer?
2323

24-
weak var delegate: CGMStalenessMonitorDelegate? = nil {
25-
didSet {
26-
if delegate != nil {
27-
checkCGMStaleness()
28-
}
29-
}
30-
}
24+
weak var delegate: CGMStalenessMonitorDelegate?
3125

3226
@Published var cgmDataIsStale: Bool = true {
3327
didSet {
@@ -57,29 +51,27 @@ class CGMStalenessMonitor {
5751
cgmStalenessTimer?.invalidate()
5852
cgmStalenessTimer = Timer.scheduledTimer(withTimeInterval: expiration.timeIntervalSinceNow, repeats: false) { [weak self] _ in
5953
self?.log.debug("cgmStalenessTimer fired")
60-
self?.checkCGMStaleness()
54+
Task {
55+
await self?.checkCGMStaleness()
56+
}
6157
}
6258
cgmStalenessTimer?.tolerance = CGMStalenessMonitor.cgmStalenessTimerTolerance
6359
}
6460

65-
private func checkCGMStaleness() {
66-
delegate?.getLatestCGMGlucose(since: Date(timeIntervalSinceNow: -LoopAlgorithm.inputDataRecencyInterval)) { (result) in
67-
DispatchQueue.main.async {
68-
self.log.debug("Fetched latest CGM Glucose for checkCGMStaleness: %{public}@", String(describing: result))
69-
switch result {
70-
case .success(let sample):
71-
if let sample = sample {
72-
self.cgmDataIsStale = false
73-
self.updateCGMStalenessTimer(expiration: sample.startDate.addingTimeInterval(LoopAlgorithm.inputDataRecencyInterval + CGMStalenessMonitor.cgmStalenessTimerTolerance))
74-
} else {
75-
self.cgmDataIsStale = true
76-
}
77-
case .failure(let error):
78-
self.log.error("Unable to get latest CGM clucose: %{public}@ ", String(describing: error))
79-
// Some kind of system error; check again in 5 minutes
80-
self.updateCGMStalenessTimer(expiration: Date(timeIntervalSinceNow: .minutes(5)))
81-
}
61+
func checkCGMStaleness() async {
62+
do {
63+
let sample = try await delegate?.getLatestCGMGlucose(since: Date(timeIntervalSinceNow: -LoopAlgorithm.inputDataRecencyInterval))
64+
self.log.debug("Fetched latest CGM Glucose for checkCGMStaleness: %{public}@", String(describing: sample))
65+
if let sample = sample {
66+
self.cgmDataIsStale = false
67+
self.updateCGMStalenessTimer(expiration: sample.startDate.addingTimeInterval(LoopAlgorithm.inputDataRecencyInterval + CGMStalenessMonitor.cgmStalenessTimerTolerance))
68+
} else {
69+
self.cgmDataIsStale = true
8270
}
71+
} catch {
72+
self.log.error("Unable to get latest CGM clucose: %{public}@ ", String(describing: error))
73+
// Some kind of system error; check again in 5 minutes
74+
self.updateCGMStalenessTimer(expiration: Date(timeIntervalSinceNow: .minutes(5)))
8375
}
8476
}
8577
}

Loop/Managers/CriticalEventLogExportManager.swift

+2-16
Original file line numberDiff line numberDiff line change
@@ -199,16 +199,6 @@ public class CriticalEventLogExportManager {
199199
calendar.timeZone = TimeZone(identifier: "UTC")!
200200
return calendar
201201
}()
202-
203-
// MARK: - Background Tasks
204-
205-
func registerBackgroundTasks() {
206-
if Self.registerCriticalEventLogHistoricalExportBackgroundTask({ self.handleCriticalEventLogHistoricalExportBackgroundTask($0) }) {
207-
log.debug("Critical event log export background task registered")
208-
} else {
209-
log.error("Critical event log export background task not registered")
210-
}
211-
}
212202
}
213203

214204
// MARK: - CriticalEventLogBaseExporter
@@ -567,11 +557,7 @@ fileprivate extension FileManager {
567557
// MARK: - Critical Event Log Export
568558

569559
extension CriticalEventLogExportManager {
570-
private static var criticalEventLogHistoricalExportBackgroundTaskIdentifier: String { "com.loopkit.background-task.critical-event-log.historical-export" }
571-
572-
public static func registerCriticalEventLogHistoricalExportBackgroundTask(_ handler: @escaping (BGProcessingTask) -> Void) -> Bool {
573-
return BGTaskScheduler.shared.register(forTaskWithIdentifier: criticalEventLogHistoricalExportBackgroundTaskIdentifier, using: nil) { handler($0 as! BGProcessingTask) }
574-
}
560+
static var historicalExportBackgroundTaskIdentifier: String { "com.loopkit.background-task.critical-event-log.historical-export" }
575561

576562
public func handleCriticalEventLogHistoricalExportBackgroundTask(_ task: BGProcessingTask) {
577563
dispatchPrecondition(condition: .notOnQueue(.main))
@@ -602,7 +588,7 @@ extension CriticalEventLogExportManager {
602588
public func scheduleCriticalEventLogHistoricalExportBackgroundTask(isRetry: Bool = false) {
603589
do {
604590
let earliestBeginDate = isRetry ? retryExportHistoricalDate() : nextExportHistoricalDate()
605-
let request = BGProcessingTaskRequest(identifier: Self.criticalEventLogHistoricalExportBackgroundTaskIdentifier)
591+
let request = BGProcessingTaskRequest(identifier: Self.historicalExportBackgroundTaskIdentifier)
606592
request.earliestBeginDate = earliestBeginDate
607593
request.requiresExternalPower = true
608594

Loop/Managers/DeviceDataManager.swift

+33-26
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,11 @@ final class DeviceDataManager {
288288
glucoseStore.delegate = self
289289
cgmEventStore.delegate = self
290290
doseStore.insulinDeliveryStore.delegate = self
291-
291+
292+
Task {
293+
await cgmStalenessMonitor.checkCGMStaleness()
294+
}
295+
292296
setupPump()
293297
setupCGM()
294298

@@ -1179,28 +1183,25 @@ extension DeviceDataManager {
11791183
return
11801184
}
11811185

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

11851188
Task {
11861189
do {
11871190
try await doseStore.resetPumpData()
1188-
} catch {
1189-
completion?(error)
1190-
return
1191-
}
11921191

1193-
let insulinSharingDenied = self.healthStore.authorizationStatus(for: HealthKitSampleStore.insulinQuantityType) == .sharingDenied
1194-
guard !insulinSharingDenied else {
1195-
// only clear cache since access to health kit is denied
1196-
insulinDeliveryStore.purgeCachedInsulinDeliveryObjects() { error in
1197-
completion?(error)
1192+
let insulinSharingDenied = self.healthStore.authorizationStatus(for: HealthKitSampleStore.insulinQuantityType) == .sharingDenied
1193+
guard !insulinSharingDenied else {
1194+
// only clear cache since access to health kit is denied
1195+
await insulinDeliveryStore.purgeCachedInsulinDeliveryObjects()
1196+
completion?(nil)
1197+
return
11981198
}
1199-
return
1200-
}
1201-
1202-
insulinDeliveryStore.purgeAllDoseEntries(healthKitPredicate: devicePredicate) { error in
1199+
1200+
try await insulinDeliveryStore.purgeDoseEntriesForDevice(testingPumpManager.testingDevice)
1201+
completion?(nil)
1202+
} catch {
12031203
completion?(error)
1204+
return
12041205
}
12051206
}
12061207
}
@@ -1210,19 +1211,25 @@ extension DeviceDataManager {
12101211
completion?(nil)
12111212
return
12121213
}
1213-
1214-
let glucoseSharingDenied = self.healthStore.authorizationStatus(for: HealthKitSampleStore.glucoseType) == .sharingDenied
1215-
guard !glucoseSharingDenied else {
1216-
// only clear cache since access to health kit is denied
1217-
glucoseStore.purgeCachedGlucoseObjects() { error in
1218-
completion?(error)
1214+
1215+
Task {
1216+
let glucoseSharingDenied = self.healthStore.authorizationStatus(for: HealthKitSampleStore.glucoseType) == .sharingDenied
1217+
guard !glucoseSharingDenied else {
1218+
// only clear cache since access to health kit is denied
1219+
do {
1220+
try await glucoseStore.purgeCachedGlucoseObjects()
1221+
} catch {
1222+
completion?(error)
1223+
}
1224+
return
12191225
}
1220-
return
1221-
}
12221226

1223-
let predicate = HKQuery.predicateForObjects(from: [testingCGMManager.testingDevice])
1224-
glucoseStore.purgeAllGlucoseSamples(healthKitPredicate: predicate) { error in
1225-
completion?(error)
1227+
do {
1228+
try await glucoseStore.purgeAllGlucose(for: testingCGMManager.testingDevice)
1229+
completion?(nil)
1230+
} catch {
1231+
completion?(error)
1232+
}
12261233
}
12271234
}
12281235
}

Loop/Managers/LoopAppManager.swift

+23-12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import UIKit
1010
import Intents
11+
import BackgroundTasks
1112
import Combine
1213
import LoopKit
1314
import LoopKitUI
@@ -133,9 +134,27 @@ class LoopAppManager: NSObject {
133134
self.state = state.next
134135
}
135136

137+
func registerBackgroundTasks() {
138+
let taskIdentifier = CriticalEventLogExportManager.historicalExportBackgroundTaskIdentifier
139+
let registered = BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil) { task in
140+
guard let criticalEventLogExportManager = self.criticalEventLogExportManager else {
141+
self.log.error("Critical event log export launch handler called before initialization complete!")
142+
return
143+
}
144+
criticalEventLogExportManager.handleCriticalEventLogHistoricalExportBackgroundTask(task as! BGProcessingTask)
145+
}
146+
if registered {
147+
log.debug("Critical event log export background task registered")
148+
} else {
149+
log.error("Critical event log export background task not registered")
150+
}
151+
}
152+
136153
func launch() {
137154
precondition(isLaunchPending)
138155

156+
registerBackgroundTasks()
157+
139158
Task {
140159
await resumeLaunch()
141160
}
@@ -248,7 +267,7 @@ class LoopAppManager: NSObject {
248267
observationStart: Date().addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval)
249268
)
250269

251-
self.doseStore = DoseStore(
270+
self.doseStore = await DoseStore(
252271
healthKitSampleStore: insulinHealthStore,
253272
cacheStore: cacheStore,
254273
cacheLength: localCacheDuration,
@@ -263,7 +282,7 @@ class LoopAppManager: NSObject {
263282
observationStart: Date().addingTimeInterval(-.hours(24))
264283
)
265284

266-
self.glucoseStore = GlucoseStore(
285+
self.glucoseStore = await GlucoseStore(
267286
healthKitSampleStore: glucoseHealthStore,
268287
cacheStore: cacheStore,
269288
cacheLength: localCacheDuration,
@@ -390,9 +409,6 @@ class LoopAppManager: NSObject {
390409
directory: FileManager.default.exportsDirectoryURL,
391410
historicalDuration: localCacheDuration)
392411

393-
criticalEventLogExportManager.registerBackgroundTasks()
394-
395-
396412
statusExtensionManager = ExtensionDataManager(
397413
deviceDataManager: deviceDataManager,
398414
loopDataManager: loopDataManager,
@@ -1045,6 +1061,7 @@ extension LoopAppManager: SimulatedData {
10451061
Task { @MainActor in
10461062
do {
10471063
try await self.doseStore.purgeHistoricalPumpEvents()
1064+
try await self.glucoseStore.purgeHistoricalGlucoseObjects()
10481065
} catch {
10491066
completion(error)
10501067
return
@@ -1059,13 +1076,7 @@ extension LoopAppManager: SimulatedData {
10591076
completion(error)
10601077
return
10611078
}
1062-
self.glucoseStore.purgeHistoricalGlucoseObjects() { error in
1063-
guard error == nil else {
1064-
completion(error)
1065-
return
1066-
}
1067-
self.settingsManager.purgeHistoricalSettingsObjects(completion: completion)
1068-
}
1079+
self.settingsManager.purgeHistoricalSettingsObjects(completion: completion)
10691080
}
10701081
}
10711082
}

Loop/Managers/LoopDataManager.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,8 @@ final class LoopDataManager: ObservableObject {
435435
carbAbsorptionModel: carbAbsorptionModel,
436436
recommendationInsulinModel: insulinModel(for: deliveryDelegate?.pumpInsulinType ?? .novolog),
437437
recommendationType: .manualBolus,
438-
automaticBolusApplicationFactor: effectiveBolusApplicationFactor)
438+
automaticBolusApplicationFactor: effectiveBolusApplicationFactor,
439+
useMidAbsorptionISF: false)
439440
}
440441

441442
func loopingReEnabled() async {

0 commit comments

Comments
 (0)