TaskScheduler.swift 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  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 {
  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. // Thread-safety: a serial queue so we don’t manipulate tasks from multiple threads at once
  26. private let queue = DispatchQueue(label: "com.LoopFollow.TaskSchedulerQueue")
  27. private var tasks: [TaskID: ScheduledTask] = [:]
  28. private var currentTimer: Timer?
  29. private init() {}
  30. // MARK: - Public API
  31. func scheduleTask(id: TaskID, nextRun: Date, action: @escaping () -> Void) {
  32. queue.async {
  33. let timeString = self.formatTime(nextRun)
  34. LogManager.shared.log(category: .taskScheduler, message: "scheduleTask(\(id)): nextRun = \(timeString)")
  35. self.tasks[id] = ScheduledTask(nextRun: nextRun, action: action)
  36. self.rescheduleTimer()
  37. }
  38. }
  39. func rescheduleTask(id: TaskID, to newRunDate: Date) {
  40. queue.async {
  41. guard var existingTask = self.tasks[id] else {
  42. return
  43. }
  44. //let timeString = self.formatTime(newRunDate)
  45. //LogManager.shared.log(category: .taskScheduler, message: "Reschedule Task \(id), nextRun = \(timeString)")
  46. existingTask.nextRun = newRunDate
  47. self.tasks[id] = existingTask
  48. self.rescheduleTimer()
  49. }
  50. }
  51. func checkTasksNow() {
  52. queue.async {
  53. LogManager.shared.log(category: .taskScheduler, message: "CheckTasksNow, Forcing immediate check.")
  54. self.fireOverdueTasks()
  55. self.rescheduleTimer()
  56. }
  57. }
  58. // MARK: - Private
  59. /// Updated signature to include info about who called us, and which task triggered it (if any).
  60. private func rescheduleTimer() {
  61. // Invalidate any existing timer
  62. currentTimer?.invalidate()
  63. currentTimer = nil
  64. guard let (_, earliestTask) = tasks.min(by: { $0.value.nextRun < $1.value.nextRun }) else {
  65. LogManager.shared.log(
  66. category: .taskScheduler,
  67. message: "No tasks, no timer scheduled."
  68. )
  69. return
  70. }
  71. let interval = earliestTask.nextRun.timeIntervalSinceNow
  72. let safeInterval = max(interval, 0)
  73. DispatchQueue.main.async {
  74. self.currentTimer = Timer.scheduledTimer(withTimeInterval: safeInterval, repeats: false) { [weak self] _ in
  75. guard let self = self else { return }
  76. self.queue.async {
  77. self.fireOverdueTasks()
  78. self.rescheduleTimer()
  79. }
  80. }
  81. }
  82. }
  83. private func fireOverdueTasks() {
  84. let now = Date()
  85. for (id, task) in tasks {
  86. if task.nextRun <= now {
  87. // let scheduledTimeString = formatTime(task.nextRun)
  88. // let diffSeconds = Int(now.timeIntervalSince(task.nextRun))
  89. // let diffInfo = diffSeconds > 0 ? "\(diffSeconds)s late" : "\(abs(diffSeconds))s early"
  90. // LogManager.shared.log(category: .taskScheduler, message: "Executing \(id) scheduled for \(scheduledTimeString) (\(diffInfo))")
  91. var updatedTask = task
  92. updatedTask.nextRun = .distantFuture
  93. tasks[id] = updatedTask
  94. DispatchQueue.main.async {
  95. task.action()
  96. }
  97. }
  98. }
  99. }
  100. private func formatTime(_ date: Date) -> String {
  101. let formatter = DateFormatter()
  102. formatter.dateStyle = .none
  103. formatter.timeStyle = .medium
  104. return formatter.string(from: date)
  105. }
  106. }
  107. private extension TaskID {
  108. var description: String {
  109. switch self {
  110. case .profile: return "profile"
  111. case .deviceStatus: return "deviceStatus"
  112. case .fetchBG: return "fetchBG"
  113. case .treatments: return "treatments"
  114. case .calendarWrite: return "calendarWrite"
  115. case .minAgoUpdate: return "minAgoUpdate"
  116. case .alarmCheck: return "alarmCheck"
  117. }
  118. }
  119. }