TaskScheduler.swift 4.0 KB

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