TaskScheduler.swift 3.3 KB

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