TaskScheduler.swift 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. // LoopFollow
  2. // TaskScheduler.swift
  3. // Created by Jonas Björkert on 2025-01-13.
  4. import Foundation
  5. import UIKit
  6. enum TaskID: CaseIterable {
  7. case profile
  8. case deviceStatus
  9. case treatments
  10. case fetchBG
  11. case minAgoUpdate
  12. case calendarWrite
  13. case alarmCheck
  14. }
  15. struct ScheduledTask {
  16. var nextRun: Date
  17. var action: () -> Void
  18. }
  19. class TaskScheduler {
  20. static let shared = TaskScheduler()
  21. private let queue = DispatchQueue(label: "com.LoopFollow.TaskSchedulerQueue")
  22. private var tasks: [TaskID: ScheduledTask] = [:]
  23. private var currentTimer: DispatchSourceTimer?
  24. private init() {}
  25. // MARK: - Public API
  26. func scheduleTask(id: TaskID, nextRun: Date, action: @escaping () -> Void) {
  27. queue.async {
  28. let timeString = self.formatTime(nextRun)
  29. LogManager.shared.log(category: .taskScheduler, message: "scheduleTask(\(id)): next run = \(timeString)", isDebug: true)
  30. self.tasks[id] = ScheduledTask(nextRun: nextRun, action: action)
  31. self.rescheduleTimer()
  32. }
  33. }
  34. func rescheduleTask(id: TaskID, to newRunDate: Date) {
  35. // let timeString = formatTime(newRunDate)
  36. // LogManager.shared.log(category: .taskScheduler, message: "Reschedule Task \(id): next run = \(timeString)", isDebug: true)
  37. queue.async {
  38. guard var existingTask = self.tasks[id] else { return }
  39. existingTask.nextRun = newRunDate
  40. self.tasks[id] = existingTask
  41. self.checkTasksNow()
  42. }
  43. }
  44. func checkTasksNow() {
  45. queue.async {
  46. self.fireOverdueTasks()
  47. self.rescheduleTimer()
  48. }
  49. }
  50. // MARK: - Private
  51. private func rescheduleTimer() {
  52. currentTimer?.cancel()
  53. currentTimer = nil
  54. guard let (_, earliestTask) = tasks.min(by: { $0.value.nextRun < $1.value.nextRun }) else {
  55. LogManager.shared.log(category: .taskScheduler, message: "No tasks, no timer scheduled.")
  56. return
  57. }
  58. let interval = earliestTask.nextRun.timeIntervalSinceNow
  59. let safeInterval = max(interval, 0)
  60. let timer = DispatchSource.makeTimerSource(queue: queue)
  61. timer.schedule(deadline: .now() + safeInterval)
  62. timer.setEventHandler { [weak self] in
  63. guard let self = self else { return }
  64. self.fireOverdueTasks()
  65. self.rescheduleTimer()
  66. }
  67. currentTimer = timer
  68. timer.resume()
  69. }
  70. private func fireOverdueTasks() {
  71. BackgroundAlertManager.shared.scheduleBackgroundAlert()
  72. let now = Date()
  73. let tasksToSkipAlarmCheck: Set<TaskID> = [.deviceStatus, .treatments, .fetchBG]
  74. for taskID in TaskID.allCases {
  75. guard let task = tasks[taskID], task.nextRun <= now else {
  76. continue
  77. }
  78. // Skip alarm checks if data-fetching tasks (deviceStatus, treatments, fetchBG) are currently due.
  79. // This ensures alarms are evaluated with the latest data, avoiding premature or incorrect triggers.
  80. // If skipped, reschedule alarmCheck 1 second later to retry after data updates.
  81. if taskID == .alarmCheck {
  82. let shouldSkip = tasksToSkipAlarmCheck.contains {
  83. guard let checkTask = tasks[$0] else { return false }
  84. return checkTask.nextRun <= now || checkTask.nextRun == .distantFuture
  85. }
  86. if shouldSkip {
  87. guard var existingTask = tasks[taskID] else { continue }
  88. existingTask.nextRun = Date().addingTimeInterval(1)
  89. tasks[taskID] = existingTask
  90. continue
  91. }
  92. }
  93. var updatedTask = task
  94. updatedTask.nextRun = .distantFuture
  95. tasks[taskID] = updatedTask
  96. // LogManager.shared.log(category: .taskScheduler, message: "Executing Task \(taskID)", isDebug: true)
  97. DispatchQueue.main.async {
  98. task.action()
  99. }
  100. }
  101. }
  102. private func formatTime(_ date: Date) -> String {
  103. let formatter = DateFormatter()
  104. formatter.dateStyle = .none
  105. formatter.timeStyle = .medium
  106. return formatter.string(from: date)
  107. }
  108. }