Skip to content

Commit e78ec9c

Browse files
convert task scheduling to service
1 parent 6044e05 commit e78ec9c

File tree

4 files changed

+66
-76
lines changed

4 files changed

+66
-76
lines changed

Alchemy/Queue/QueueWorker.swift

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import AsyncAlgorithms
22

3-
struct QueueWorker: Service, @unchecked Sendable {
3+
actor QueueWorker: Service {
44
let queue: Queue
5-
var channels: [String] = [Queue.defaultChannel]
6-
var pollRate: Duration = .seconds(1)
7-
var untilEmpty: Bool = false
5+
var channels: [String]
6+
var pollRate: Duration
7+
var untilEmpty: Bool
8+
9+
init(queue: Queue, channels: [String], pollRate: Duration, untilEmpty: Bool) {
10+
self.queue = queue
11+
self.channels = channels
12+
self.pollRate = pollRate
13+
self.untilEmpty = untilEmpty
14+
}
815

916
private var timer: some AsyncSequence {
1017
AsyncTimerSequence(interval: pollRate, clock: ContinuousClock())

Alchemy/Scheduler/Frequency.swift

+23-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Cron
22
import Foundation
33

44
/// Used to help build schedule frequencies for scheduled tasks.
5-
public final class Frequency {
5+
public final class Frequency: AsyncSequence {
66
/// A day of the week.
77
public enum DayOfWeek: Int, ExpressibleByIntegerLiteral {
88
/// Sunday
@@ -71,7 +71,7 @@ public final class Frequency {
7171
var cron = try! DatePattern("* * * * * * *")
7272

7373
/// The time amount until the next interval, if there is one.
74-
func timeUntilNext() -> TimeAmount? {
74+
func timeUntilNext() -> Duration? {
7575
guard let next = cron.next(), let nextDate = next.date else {
7676
return nil
7777
}
@@ -170,4 +170,25 @@ public final class Frequency {
170170
preconditionFailure("Error parsing cron expression '\(expression)': \(error).")
171171
}
172172
}
173+
174+
// MARK: AsyncSequence
175+
176+
public typealias Element = Foundation.Date
177+
178+
public func makeAsyncIterator() -> Iterator {
179+
Iterator(frequency: self)
180+
}
181+
182+
public struct Iterator: AsyncIteratorProtocol {
183+
let frequency: Frequency
184+
185+
public mutating func next() async throws -> Foundation.Date? {
186+
guard let delay = frequency.timeUntilNext() else {
187+
return nil
188+
}
189+
190+
try await Task.sleep(for: delay)
191+
return Date()
192+
}
193+
}
173194
}

Alchemy/Scheduler/Plugins/Schedules.swift

-4
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,4 @@ struct Schedules: Plugin {
55
app.container.register(scheduler).singleton()
66
app.registerCommand(ScheduleCommand.self)
77
}
8-
9-
func shutdown(app: Application) async throws {
10-
try await scheduler.shutdown()
11-
}
128
}

Alchemy/Scheduler/Scheduler.swift

+32-66
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,41 @@
1+
import AsyncAlgorithms
12
import NIOCore
23
import NIOConcurrencyHelpers
4+
import Foundation
5+
import ServiceLifecycle
36

47
/// A service for scheduling recurring work, in lieu of a separate cron task
58
/// running apart from your server.
69
public final class Scheduler {
7-
private struct ScheduledTask {
10+
private struct Task: Service, @unchecked Sendable {
811
let name: String
912
let frequency: Frequency
10-
let work: () async throws -> Void
11-
}
12-
13-
public private(set) var isStarted: Bool = false
14-
private var tasks: [ScheduledTask] = []
15-
private var scheduled: [Scheduled<Void>] = []
16-
private let lock = NIOLock()
17-
18-
/// Start scheduling with the given loop.
19-
///
20-
/// - Parameter scheduleLoop: A loop to run all tasks on. Defaults to the
21-
/// next available `EventLoop`.
22-
public func start(on scheduleLoop: EventLoop = LoopGroup.next()) {
23-
guard lock.withLock({
24-
guard !isStarted else { return false }
25-
isStarted = true
26-
return true
27-
}) else {
28-
Log.warning("This scheduler has already been started.")
29-
return
30-
}
13+
let task: () async throws -> Void
3114

32-
Log.info("Scheduling \(tasks.count) tasks.")
33-
for task in tasks {
34-
schedule(task: task, on: scheduleLoop)
35-
}
36-
}
15+
func run() async throws {
16+
for try await _ in frequency.cancelOnGracefulShutdown() {
17+
do {
18+
Log.info("Scheduling \(name) (\(frequency.cron.string))")
19+
try await task()
20+
} catch {
21+
// log an error but don't throw - we don't want to stop all
22+
// scheduling if a single instance of a task results in
23+
// an error.
24+
Log.error("Error scheduling \(name): \(error)")
25+
}
26+
}
3727

38-
public func shutdown() async throws {
39-
lock.withLock {
40-
isStarted = false
41-
scheduled.forEach { $0.cancel() }
42-
scheduled = []
28+
Log.info("Scheduling \(name) complete; there are no future times in the frequency.")
4329
}
4430
}
4531

46-
private func schedule(task: ScheduledTask, on loop: EventLoop) {
47-
guard let delay = task.frequency.timeUntilNext() else {
48-
Log.info("Scheduling \(task.name) complete; there are no future times in the frequency.")
49-
return
50-
}
51-
52-
lock.withLock {
53-
guard isStarted else {
54-
Log.debug("Not scheduling task \(task.name), this Scheduler is not started.")
55-
return
56-
}
32+
private var tasks: [Task] = []
5733

58-
let scheduledTask = loop.flatScheduleTask(in: delay) {
59-
loop.asyncSubmit {
60-
// Schedule next and run
61-
self.schedule(task: task, on: loop)
62-
63-
try await task.work()
64-
}
65-
}
66-
67-
scheduled.append(scheduledTask)
34+
/// Start scheduling.
35+
public func start() {
36+
Log.info("Scheduling \(tasks.count) tasks.")
37+
for task in tasks {
38+
Life.addService(task)
6839
}
6940
}
7041

@@ -78,18 +49,11 @@ public final class Scheduler {
7849
/// - Returns: A builder for customizing the scheduling frequency.
7950
public func task(_ name: String? = nil, _ task: @escaping () async throws -> Void) -> Frequency {
8051
let frequency = Frequency()
81-
let name = name ?? "task_\(tasks.count)"
82-
let task = ScheduledTask(name: name, frequency: frequency) {
83-
do {
84-
Log.info("Scheduling \(name) (\(frequency.cron.string))")
85-
try await task()
86-
} catch {
87-
Log.error("Error scheduling \(name): \(error)")
88-
throw error
89-
}
90-
}
91-
92-
tasks.append(task)
52+
tasks.append(
53+
Task(name: name ?? "task_\(tasks.count)",
54+
frequency: frequency,
55+
task: task)
56+
)
9357
return frequency
9458
}
9559

@@ -100,7 +64,9 @@ public final class Scheduler {
10064
/// - queue: The queue to schedule it on.
10165
/// - channel: The queue channel to schedule it on.
10266
/// - Returns: A builder for customizing the scheduling frequency.
103-
public func job<J: Job>(_ job: @escaping @autoclosure () -> J, queue: Queue = Q, channel: String = Queue.defaultChannel) -> Frequency {
67+
public func job<J: Job>(_ job: @escaping @autoclosure () -> J,
68+
queue: Queue = Q,
69+
channel: String = Queue.defaultChannel) -> Frequency {
10470

10571
// Register the job, just in case the user forgot.
10672
Jobs.register(J.self)

0 commit comments

Comments
 (0)