1
+ import AsyncAlgorithms
1
2
import NIOCore
2
3
import NIOConcurrencyHelpers
4
+ import Foundation
5
+ import ServiceLifecycle
3
6
4
7
/// A service for scheduling recurring work, in lieu of a separate cron task
5
8
/// running apart from your server.
6
9
public final class Scheduler {
7
- private struct ScheduledTask {
10
+ private struct Task : Service , @ unchecked Sendable {
8
11
let name : String
9
12
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
31
14
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
+ }
37
27
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. " )
43
29
}
44
30
}
45
31
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 ] = [ ]
57
33
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)
68
39
}
69
40
}
70
41
@@ -78,18 +49,11 @@ public final class Scheduler {
78
49
/// - Returns: A builder for customizing the scheduling frequency.
79
50
public func task( _ name: String ? = nil , _ task: @escaping ( ) async throws -> Void ) -> Frequency {
80
51
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
+ )
93
57
return frequency
94
58
}
95
59
@@ -100,7 +64,9 @@ public final class Scheduler {
100
64
/// - queue: The queue to schedule it on.
101
65
/// - channel: The queue channel to schedule it on.
102
66
/// - 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 {
104
70
105
71
// Register the job, just in case the user forgot.
106
72
Jobs . register ( J . self)
0 commit comments