From 6999d96ee5fd228cc10e091c72bbff7583dc500e Mon Sep 17 00:00:00 2001 From: Tanner Date: Thu, 5 Dec 2019 15:23:07 -0500 Subject: [PATCH] beta 2 updates (#48) * beta 2 * beta 2 updates * update schedule builder * regen linuxmain * fix ci --- .github/workflows/test.yml | 12 +- Package.swift | 2 +- Sources/Jobs/Exports.swift | 17 + Sources/Jobs/Job.swift | 76 +-- Sources/Jobs/JobContext.swift | 22 +- Sources/Jobs/JobData.swift | 32 ++ Sources/Jobs/JobIdentifier.swift | 13 + Sources/Jobs/JobStorage.swift | 53 -- Sources/Jobs/JobsCommand.swift | 183 +++--- Sources/Jobs/JobsConfiguration.swift | 46 +- Sources/Jobs/JobsDriver.swift | 58 +- Sources/Jobs/JobsProvider.swift | 103 ++-- Sources/Jobs/JobsQueue.swift | 82 ++- Sources/Jobs/JobsQueueName.swift | 23 + Sources/Jobs/JobsService.swift | 94 --- Sources/Jobs/JobsWorker.swift | 152 ++--- .../{Scheduler => }/ScheduleBuilder.swift | 108 ++-- Sources/Jobs/ScheduledJob.swift | 47 ++ Sources/Jobs/ScheduledJobsWorker.swift | 97 ---- Sources/Jobs/Scheduler/DateExtensions.swift | 486 ---------------- Sources/Jobs/Scheduler/RecurrenceRule.swift | 411 ------------- .../Scheduler/RecurrenceRuleConstraint.swift | 285 --------- .../SpecificRecurrenceRuleConstraints.swift | 540 ------------------ Tests/JobsTests/JobStorageTests.swift | 34 -- Tests/JobsTests/JobsConfigTests.swift | 76 --- Tests/JobsTests/JobsTests.swift | 190 +++--- Tests/JobsTests/JobsWorkerTests.swift | 77 --- Tests/JobsTests/Mocks/JobMock.swift | 22 - Tests/JobsTests/QueueNameTests.swift | 16 - Tests/JobsTests/ScheduleBuilderTests.swift | 188 ++++++ .../DateComponentRetrievalTests.swift | 172 ------ .../RecurrenceRuleConstraintTests.swift | 188 ------ .../Scheduler/RecurrenceRuleTests.swift | 258 --------- .../JobsTests/Scheduler/SchedulerTests.swift | 208 ------- Tests/JobsTests/XCTestManifests.swift | 129 ----- Tests/LinuxMain.swift | 8 - 36 files changed, 824 insertions(+), 3684 deletions(-) create mode 100644 Sources/Jobs/JobData.swift create mode 100644 Sources/Jobs/JobIdentifier.swift delete mode 100644 Sources/Jobs/JobStorage.swift create mode 100644 Sources/Jobs/JobsQueueName.swift delete mode 100644 Sources/Jobs/JobsService.swift rename Sources/Jobs/{Scheduler => }/ScheduleBuilder.swift (80%) create mode 100644 Sources/Jobs/ScheduledJob.swift delete mode 100644 Sources/Jobs/ScheduledJobsWorker.swift delete mode 100755 Sources/Jobs/Scheduler/DateExtensions.swift delete mode 100755 Sources/Jobs/Scheduler/RecurrenceRule.swift delete mode 100755 Sources/Jobs/Scheduler/RecurrenceRuleConstraint.swift delete mode 100755 Sources/Jobs/Scheduler/SpecificRecurrenceRuleConstraints.swift delete mode 100644 Tests/JobsTests/JobStorageTests.swift delete mode 100644 Tests/JobsTests/JobsConfigTests.swift delete mode 100644 Tests/JobsTests/JobsWorkerTests.swift delete mode 100644 Tests/JobsTests/Mocks/JobMock.swift delete mode 100644 Tests/JobsTests/QueueNameTests.swift create mode 100644 Tests/JobsTests/ScheduleBuilderTests.swift delete mode 100755 Tests/JobsTests/Scheduler/DateComponentRetrievalTests.swift delete mode 100755 Tests/JobsTests/Scheduler/RecurrenceRuleConstraintTests.swift delete mode 100755 Tests/JobsTests/Scheduler/RecurrenceRuleTests.swift delete mode 100644 Tests/JobsTests/Scheduler/SchedulerTests.swift delete mode 100644 Tests/JobsTests/XCTestManifests.swift delete mode 100644 Tests/LinuxMain.swift diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 62561b0..92ef07e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,19 +7,19 @@ jobs: image: vapor/swift:5.1-xenial runs-on: ubuntu-latest steps: - - uses: actions/checkout@master - - run: swift test + - uses: actions/checkout@v1 + - run: swift test --enable-test-discovery bionic: container: image: vapor/swift:5.1-bionic runs-on: ubuntu-latest steps: - - uses: actions/checkout@master - - run: swift test + - uses: actions/checkout@v1 + - run: swift test --enable-test-discovery thread: container: image: vapor/swift:5.1-bionic runs-on: ubuntu-latest steps: - - uses: actions/checkout@master - - run: swift test --sanitize=thread + - uses: actions/checkout@v1 + - run: swift test --enable-test-discovery --sanitize=thread diff --git a/Package.swift b/Package.swift index 124b1e6..0b00a08 100644 --- a/Package.swift +++ b/Package.swift @@ -10,7 +10,7 @@ let package = Package( .library(name: "Jobs", targets: ["Jobs"]), ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0-beta.1") + .package(url: "https://github.com/vapor/vapor.git", .branch("master")) ], targets: [ .target(name: "Jobs", dependencies: ["Vapor"]), diff --git a/Sources/Jobs/Exports.swift b/Sources/Jobs/Exports.swift index 8786bc2..6eb6acc 100644 --- a/Sources/Jobs/Exports.swift +++ b/Sources/Jobs/Exports.swift @@ -1,3 +1,20 @@ +@_exported import struct Foundation.Date +@_exported import struct Logging.Logger @_exported import class NIO.EventLoopFuture @_exported import struct NIO.EventLoopPromise @_exported import protocol NIO.EventLoop +@_exported import struct NIO.TimeAmount + +import class NIO.RepeatedTask + +extension RepeatedTask { + func syncCancel(on eventLoop: EventLoop) { + do { + let promise = eventLoop.makePromise(of: Void.self) + self.cancel(promise: promise) + try promise.futureResult.wait() + } catch { + print("failed cancelling repeated task \(error)") + } + } +} diff --git a/Sources/Jobs/Job.swift b/Sources/Jobs/Job.swift index 0b347f2..d63c29e 100644 --- a/Sources/Jobs/Job.swift +++ b/Sources/Jobs/Job.swift @@ -5,7 +5,7 @@ import Vapor /// A task that can be queued for future execution. public protocol Job: AnyJob { /// The data associated with a job - associatedtype Data: Codable + associatedtype Payload /// Called when it's this Job's turn to be dequeued. /// @@ -13,7 +13,10 @@ public protocol Job: AnyJob { /// - context: The JobContext. Can be used to store and retrieve services /// - data: The data for this handler /// - Returns: A future `Void` value used to signify completion - func dequeue(_ context: JobContext, _ data: Data) -> EventLoopFuture + func dequeue( + _ context: JobContext, + _ payload: Payload + ) -> EventLoopFuture /// Called when there is an error at any stage of the Job's execution. @@ -22,34 +25,51 @@ public protocol Job: AnyJob { /// - context: The JobContext. Can be used to store and retrieve services /// - error: The error returned by the job. /// - Returns: A future `Void` value used to signify completion - func error(_ context: JobContext, _ error: Error, _ data: Data) -> EventLoopFuture + func error( + _ context: JobContext, + _ error: Error, + _ payload: Payload + ) -> EventLoopFuture + + static func serializePayload(_ payload: Payload) throws -> [UInt8] + static func parsePayload(_ bytes: [UInt8]) throws -> Payload } -public extension Job { +extension Job where Payload: Codable { + public static func serializePayload(_ payload: Payload) throws -> [UInt8] { + try .init(JSONEncoder().encode(payload)) + } + + public static func parsePayload(_ bytes: [UInt8]) throws -> Payload { + try JSONDecoder().decode(Payload.self, from: .init(bytes)) + } +} + +extension Job { /// The jobName of the Job - static var jobName: String { + public static var name: String { return String(describing: Self.self) } - /// See `Job.error` - func error(_ context: JobContext, _ error: Error, _ data: Data) -> EventLoopFuture { - return context.eventLoop.makeSucceededFuture(()) + public func error( + _ context: JobContext, + _ error: Error, + _ payload: Payload + ) -> EventLoopFuture { + context.eventLoop.makeSucceededFuture(()) } - func error(_ context: JobContext, _ error: Error, _ storage: JobStorage) -> EventLoopFuture { + public func _error(_ context: JobContext, _ error: Error, payload: [UInt8]) -> EventLoopFuture { do { - let data = try JSONDecoder().decode(Data.self, from: storage.data) - return self.error(context, error, data) + return try self.error(context, error, Self.parsePayload(payload)) } catch { return context.eventLoop.makeFailedFuture(error) } } - /// See `AnyJob.anyDequeue` - func anyDequeue(_ context: JobContext, _ storage: JobStorage) -> EventLoopFuture { + public func _dequeue(_ context: JobContext, payload: [UInt8]) -> EventLoopFuture { do { - let data = try JSONDecoder().decode(Data.self, from: storage.data) - return self.dequeue(context, data) + return try self.dequeue(context, Self.parsePayload(payload)) } catch { return context.eventLoop.makeFailedFuture(error) } @@ -59,7 +79,7 @@ public extension Job { /// A type-erased version of `Job` public protocol AnyJob { /// The name of the `Job` - static var jobName: String { get } + static var name: String { get } /// Dequeues the `Job` /// @@ -67,7 +87,7 @@ public protocol AnyJob { /// - context: The context for the job /// - storage: The `JobStorage` metadata object /// - Returns: A future void, signifying completion - func anyDequeue(_ context: JobContext, _ storage: JobStorage) -> EventLoopFuture + func _dequeue(_ context: JobContext, payload: [UInt8]) -> EventLoopFuture /// Handles errors thrown from `anyDequeue` /// @@ -76,25 +96,5 @@ public protocol AnyJob { /// - error: The error thrown /// - storage: The JobStorage /// - Returns: A future void, signifying completion - func error(_ context: JobContext, _ error: Error, _ storage: JobStorage) -> EventLoopFuture -} - -// MARK: Scheduled - -/// Describes a job that can be scheduled and repeated -public protocol ScheduledJob { - - /// The method called when the job is run - /// - Parameter context: A `JobContext` that can be used - func run(context: JobContext) -> EventLoopFuture -} - -class AnyScheduledJob { - let job: ScheduledJob - let scheduler: ScheduleBuilder - - init(job: ScheduledJob, scheduler: ScheduleBuilder) { - self.job = job - self.scheduler = scheduler - } + func _error(_ context: JobContext, _ error: Error, payload: [UInt8]) -> EventLoopFuture } diff --git a/Sources/Jobs/JobContext.swift b/Sources/Jobs/JobContext.swift index 30f75ef..0dbbf42 100644 --- a/Sources/Jobs/JobContext.swift +++ b/Sources/Jobs/JobContext.swift @@ -1,16 +1,18 @@ -import Foundation -import Vapor - -/// A simple wrapper to hold job context and services. public struct JobContext { - /// Storage for the wrapper. - public var userInfo: [AnyHashable: Any] - + public let queueName: JobsQueueName + public let configuration: JobsConfiguration + public let logger: Logger public let eventLoop: EventLoop - /// Creates an empty `JobContext` - public init(userInfo: [AnyHashable: Any] = [:], on eventLoop: EventLoop) { + public init( + queueName: JobsQueueName, + configuration: JobsConfiguration, + logger: Logger, + on eventLoop: EventLoop + ) { + self.queueName = queueName + self.configuration = configuration + self.logger = logger self.eventLoop = eventLoop - self.userInfo = [:] } } diff --git a/Sources/Jobs/JobData.swift b/Sources/Jobs/JobData.swift new file mode 100644 index 0000000..ca553ce --- /dev/null +++ b/Sources/Jobs/JobData.swift @@ -0,0 +1,32 @@ +/// Holds information about the Job that is to be encoded to the persistence store. +public struct JobData: Codable { + /// The job data to be encoded. + public let payload: [UInt8] + + /// The maxRetryCount for the `Job`. + public let maxRetryCount: Int + + /// A date to execute this job after + public let delayUntil: Date? + + /// The date this job was queued + public let queuedAt: Date + + /// The name of the `Job` + public let jobName: String + + /// Creates a new `JobStorage` holding object + public init( + payload: [UInt8], + maxRetryCount: Int, + jobName: String, + delayUntil: Date?, + queuedAt: Date + ) { + self.payload = payload + self.maxRetryCount = maxRetryCount + self.jobName = jobName + self.delayUntil = delayUntil + self.queuedAt = queuedAt + } +} diff --git a/Sources/Jobs/JobIdentifier.swift b/Sources/Jobs/JobIdentifier.swift new file mode 100644 index 0000000..3121026 --- /dev/null +++ b/Sources/Jobs/JobIdentifier.swift @@ -0,0 +1,13 @@ +import struct Foundation.UUID + +public struct JobIdentifier: Hashable, Equatable { + public let string: String + + public init(string: String) { + self.string = string + } + + public init() { + self.init(string: UUID().uuidString) + } +} diff --git a/Sources/Jobs/JobStorage.swift b/Sources/Jobs/JobStorage.swift deleted file mode 100644 index 84a0e80..0000000 --- a/Sources/Jobs/JobStorage.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Foundation - -/// Holds information about the Job that is to be encoded to the persistence store. -public struct JobStorage: Codable { - /// The persistence key for the backing store. - var key: String - - /// The job data to be encoded. - var data: Data - - /// The maxRetryCount for the `Job`. - var maxRetryCount: Int - - /// A date to execute this job after - var delayUntil: Date? - - /// The date this job was queued - var queuedAt: Date - - /// A unique ID for the job - public internal(set) var id: String - - /// The name of the `Job` - var jobName: String - - /// Creates a new `JobStorage` holding object - public init( - key: String, - data: Data, - maxRetryCount: Int, - id: String, - jobName: String, - delayUntil: Date?, - queuedAt: Date - ) { - self.key = key - self.data = data - self.maxRetryCount = maxRetryCount - self.id = id - self.jobName = jobName - self.delayUntil = delayUntil - self.queuedAt = queuedAt - } - - /// Returns a string representation of the JobStorage object - /// - /// - Returns: The string representation - public func stringValue() -> String? { - guard let jobStorageData = try? JSONEncoder().encode(self) else { return nil } - guard let jobString = String(data: jobStorageData, encoding: .utf8) else { return nil } - return jobString - } -} diff --git a/Sources/Jobs/JobsCommand.swift b/Sources/Jobs/JobsCommand.swift index f4c3146..db40957 100644 --- a/Sources/Jobs/JobsCommand.swift +++ b/Sources/Jobs/JobsCommand.swift @@ -1,5 +1,6 @@ -import Foundation import Vapor +import class NIOConcurrencyHelpers.Atomic +import class NIO.RepeatedTask /// The command to start the Queue job public final class JobsCommand: Command { @@ -10,11 +11,11 @@ public final class JobsCommand: Command { public struct Signature: CommandSignature { public init() { } - @Option(name: "queue", short: "Q", help: "Specifies a single queue to run") + @Option(name: "queue", help: "Specifies a single queue to run") var queue: String? - @Option(name: "scheduled", short: "S", help: "Runs the scheduled jobs") - var scheduledJobs: Bool? + @Flag(name: "scheduled", help: "Runs the scheduled jobs") + var scheduled: Bool } /// See `Command.help` @@ -23,107 +24,129 @@ public final class JobsCommand: Command { } private let application: Application - private var workers: [JobsWorker]? - private var scheduledWorkers: [ScheduledJobsWorker]? - private let scheduled: Bool - private let eventLoopGroup: EventLoopGroup + private var jobTasks: [RepeatedTask] + private var scheduledTasks: [String: AnyScheduledJob.Task] + private var lock: Lock + private var signalSources: [DispatchSourceSignal] + private var didShutdown: Bool + + + let isShuttingDown: Atomic + + private var eventLoopGroup: EventLoopGroup { + self.application.eventLoopGroup + } /// Create a new `JobsCommand` init(application: Application, scheduled: Bool = false) { self.application = application - self.scheduled = scheduled - self.eventLoopGroup = self.application.make() + self.jobTasks = [] + self.scheduledTasks = [:] + self.isShuttingDown = .init(value: false) + self.signalSources = [] + self.didShutdown = false + self.lock = .init() } public func run(using context: CommandContext, signature: JobsCommand.Signature) throws { - let signalQueue = DispatchQueue(label: "vapor.jobs.command.SignalHandlingQueue") - // shutdown future - let runningPromise = self.application.make(EventLoopGroup.self).next().makePromise(of: Void.self) - self.application.running = .start(using: runningPromise) - - //SIGTERM - let termSignalSource = DispatchSource.makeSignalSource(signal: SIGTERM, queue: signalQueue) - termSignalSource.setEventHandler { - print("Shutting down remaining jobs.") - runningPromise.succeed(()) - termSignalSource.cancel() - } - signal(SIGTERM, SIG_IGN) - termSignalSource.resume() + let promise = self.application.eventLoopGroup.next().makePromise(of: Void.self) + self.application.running = .start(using: promise) - //SIGINT - let intSignalSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: signalQueue) - intSignalSource.setEventHandler { - print("Shutting down remaining jobs.") - runningPromise.succeed(()) - intSignalSource.cancel() + // setup signal sources for shutdown + let signalQueue = DispatchQueue(label: "codes.vapor.jobs.command") + func makeSignalSource(_ code: Int32) { + let source = DispatchSource.makeSignalSource(signal: code, queue: signalQueue) + source.setEventHandler { + print() // clear ^C + promise.succeed(()) + } + source.resume() + self.signalSources.append(source) + signal(code, SIG_IGN) } - signal(SIGINT, SIG_IGN) - intSignalSource.resume() - - let isScheduledFromCli = signature.scheduledJobs ?? false + makeSignalSource(SIGTERM) + makeSignalSource(SIGINT) - if isScheduledFromCli || self.scheduled { - context.console.info("Starting scheduled jobs worker") - try self.startScheduledWorker() + if signature.scheduled { + self.application.logger.info("Starting scheduled jobs worker") + try self.startScheduledJobs() } else { - let queue: JobsQueue = signature.queue - .flatMap { .init(name: $0) } ?? .default - context.console.info("Starting jobs worker") - try self.startJobsWorker(on: queue) + let queue: JobsQueueName = signature.queue + .flatMap { .init(string: $0) } ?? .default + self.application.logger.info("Starting jobs worker (queue: \(queue.string))") + try self.startJobs(on: queue) } } - private func startJobsWorker(on queue: JobsQueue) throws { - var workers: [JobsWorker] = [] + private func startJobs(on queueName: JobsQueueName) throws { for eventLoop in eventLoopGroup.makeIterator() { - let worker = JobsWorker( - configuration: self.application.make(), - driver: self.application.make(), - logger: self.application.make(), - on: eventLoop - ) - worker.start(on: queue) - workers.append(worker) + let worker = self.application.jobs.queue(queueName).worker + let task = eventLoop.scheduleRepeatedAsyncTask( + initialDelay: .seconds(0), + delay: worker.queue.configuration.refreshInterval + ) { task in + // run task + return worker.run().map { + //Check if shutting down + if self.isShuttingDown.load() { + task.cancel() + } + }.recover { error in + worker.queue.logger.error("Job run failed: \(error)") + } + } + self.jobTasks.append(task) } - - self.workers = workers } - private func startScheduledWorker() throws { - var scheduledWorkers: [ScheduledJobsWorker] = [] - for eventLoop in eventLoopGroup.makeIterator() { - let worker = ScheduledJobsWorker( - configuration: self.application.make(), - logger: self.application.make(), - on: eventLoop - ) - try worker.start() - scheduledWorkers.append(worker) + private func startScheduledJobs() throws { + self.application.jobs.configuration.scheduledJobs + .forEach { self.schedule($0) } + } + + private func schedule(_ job: AnyScheduledJob) { + if self.isShuttingDown.load() { + return + } + + let context = JobContext( + queueName: JobsQueueName(string: "scheduled"), + configuration: self.application.jobs.configuration, + logger: self.application.logger, + on: self.eventLoopGroup.next() + ) + if let task = job.schedule(context: context) { + self.lock.withLock { + self.scheduledTasks[job.job.name] = task + } + task.done.whenComplete { _ in + self.schedule(job) + } } - - self.scheduledWorkers = scheduledWorkers } public func shutdown() { - var futures: [EventLoopFuture] = [] + self.didShutdown = true - if let workers = workers { - workers.forEach { worker in - worker.shutdown() - } - futures += workers.map { $0.onShutdown } - } + // stop running in case shutting downf rom signal + self.application.running?.stop() - if let scheduledWorkers = scheduledWorkers { - scheduledWorkers.forEach { worker in - worker.shutdown() - } - futures += scheduledWorkers.map { $0.onShutdown } - } + // clear signal sources + self.signalSources.forEach { $0.cancel() } // clear refs + self.signalSources = [] - try! EventLoopFuture - .andAllComplete(futures, on: self.eventLoopGroup.next()).wait() + // stop all job queue workers + self.jobTasks.forEach { + $0.syncCancel(on: self.eventLoopGroup.next()) + } + // stop all scheduled jobs + self.scheduledTasks.values.forEach { + $0.task.syncCancel(on: self.eventLoopGroup.next()) + } + } + + deinit { + assert(self.didShutdown, "JobsCommand did not shutdown before deinit") } } diff --git a/Sources/Jobs/JobsConfiguration.swift b/Sources/Jobs/JobsConfiguration.swift index 3d51a2c..85c6f1c 100644 --- a/Sources/Jobs/JobsConfiguration.swift +++ b/Sources/Jobs/JobsConfiguration.swift @@ -1,31 +1,26 @@ -import Foundation -import Vapor -import NIO - /// A `Service` to configure `Job`s public struct JobsConfiguration { - /// Type storage - internal var storage: [String: AnyJob] - - /// Scheduled Job Storage - internal var scheduledStorage: [AnyScheduledJob] - - /// A Logger object - internal let logger: Logger - /// The number of seconds to wait before checking for the next job. Defaults to `1` public var refreshInterval: TimeAmount /// The key that stores the data about a job. Defaults to `vapor_jobs` public var persistenceKey: String + public let logger: Logger public var userInfo: [AnyHashable: Any] + var jobs: [String: AnyJob] + var scheduledJobs: [AnyScheduledJob] + /// Creates an empty `JobsConfig` - public init(refreshInterval: TimeAmount = .seconds(1), persistenceKey: String = "vapor_jobs") { - self.storage = [:] - self.scheduledStorage = [] - self.logger = Logger(label: "vapor.codes.jobs") + public init( + refreshInterval: TimeAmount = .seconds(1), + persistenceKey: String = "vapor_jobs", + logger: Logger = .init(label: "codes.vapor.jobs") + ) { + self.jobs = [:] + self.scheduledJobs = [] + self.logger = logger self.refreshInterval = refreshInterval self.persistenceKey = persistenceKey self.userInfo = [:] @@ -38,11 +33,10 @@ public struct JobsConfiguration { mutating public func add(_ job: J) where J: Job { - let key = J.jobName - if let existing = storage[key] { - self.logger.warning("A job is already registered with key \(key): \(existing)") + if let existing = self.jobs[J.name] { + self.logger.warning("A job is already registered with key \(J.name): \(existing)") } - self.storage[key] = job + self.jobs[J.name] = job } @@ -59,15 +53,7 @@ public struct JobsConfiguration { where J: ScheduledJob { let storage = AnyScheduledJob(job: job, scheduler: builder) - self.scheduledStorage.append(storage) + self.scheduledJobs.append(storage) return builder } - - /// Returns the `AnyJob` for the string it was registered under - /// - /// - Parameter key: The key of the job - /// - Returns: The `AnyJob` - func make(for key: String) -> AnyJob? { - return storage[key] - } } diff --git a/Sources/Jobs/JobsDriver.swift b/Sources/Jobs/JobsDriver.swift index 4d7b534..5a1a208 100644 --- a/Sources/Jobs/JobsDriver.swift +++ b/Sources/Jobs/JobsDriver.swift @@ -1,58 +1,4 @@ -import Foundation -import NIO -import Vapor - -/// A type that can store and retrieve jobs from a persistence layer public protocol JobsDriver { - /// The event loop to be run on - var eventLoopGroup: EventLoopGroup { get } - - /// Returns a storage wrapper for a specified key. - /// - /// - Parameters: - /// - key: The key that the data is stored under. - /// - Returns: The retrieved `JobStorage`, if it exists. - func get( - key: String, - eventLoop: JobsEventLoopPreference - ) -> EventLoopFuture - - /// Handles adding a `Job` to the persistence layer for future processing. - /// - /// - Parameters: - /// - key: The key to add the `Job` under. - /// - jobStorage: The `JobStorage` object to persist. - /// - Returns: A future `Void` value used to signify completion - func set( - key: String, - job: JobStorage, - eventLoop: JobsEventLoopPreference - ) -> EventLoopFuture - - /// Called upon completion of the `Job`. Should be used for cleanup. - /// - /// - Parameters: - /// - key: The key that the `Job` was stored under - /// - jobStorage: The jobStorage holding the `Job` that was completed - /// - Returns: A future `Void` value used to signify completion - func completed( - key: String, - job: JobStorage, - eventLoop: JobsEventLoopPreference - ) -> EventLoopFuture - - /// Returns the processing version of the key - /// - /// - Parameter key: The base key - /// - Returns: The processing key - func processingKey(key: String) -> String - - /// Requeues a job due to a delay - /// - Parameter key: The key of the job - /// - Parameter jobStorage: The jobStorage holding the `Job` to be requeued - func requeue( - key: String, - job: JobStorage, - eventLoop: JobsEventLoopPreference - ) -> EventLoopFuture + func makeQueue(with context: JobContext) -> JobsQueue + func shutdown() } diff --git a/Sources/Jobs/JobsProvider.swift b/Sources/Jobs/JobsProvider.swift index e8f6307..63d6708 100644 --- a/Sources/Jobs/JobsProvider.swift +++ b/Sources/Jobs/JobsProvider.swift @@ -2,73 +2,90 @@ import Foundation import Vapor import NIO -/// A provider used to setup the `Jobs` package -public struct JobsProvider: Provider { - /// The key to use for calling the command. Defaults to `jobs` - public var commandKey: String +extension Request { + public var jobs: JobsQueue { + self.jobs(.default) + } - /// Initializes the `Jobs` package - public init(commandKey: String = "jobs") { - self.commandKey = commandKey + public func jobs(_ queue: JobsQueueName) -> JobsQueue { + self.application.jobs.queue( + queue, + logger: self.logger, + on: self.eventLoop + ) } +} - /// See `Provider`.`register(_ app:)` - public func register(_ app: Application) { - app.register(JobsService.self) { app in - return ApplicationJobsService( - configuration: app.make(), - driver: app.make(), - logger: app.make(), - eventLoopPreference: .indifferent - ) - } +public struct JobsDriverFactory { + let factory: (Jobs) -> JobsDriver + public init(_ factory: @escaping (Jobs) -> JobsDriver) { + self.factory = factory + } +} - app.register(JobsConfiguration.self) { _ in - return JobsConfiguration() - } +public final class Jobs: Provider { + public var application: Application + + public var configuration: JobsConfiguration + let command: JobsCommand + var driver: JobsDriver? - app.register(singleton: JobsCommand.self, boot: { app in - return .init(application: app) - }, shutdown: { jobs in - jobs.shutdown() - }) - - app.register(extension: CommandConfiguration.self) { configuration, a in - configuration.use(a.make(JobsCommand.self), as: self.commandKey) + public var queue: JobsQueue { + self.queue(.default) + } + + public func queue(_ name: JobsQueueName, logger: Logger? = nil, on eventLoop: EventLoop? = nil) -> JobsQueue { + guard let driver = self.driver else { + fatalError("No Jobs driver configured.") } + return driver.makeQueue( + with: .init( + queueName: name, + configuration: self.configuration, + logger: logger ?? self.application.logger, + on: eventLoop ?? self.application.eventLoopGroup.next() + ) + ) } -} - -public struct ApplicationJobs { - private let application: Application - public init(for application: Application) { + public init(_ application: Application) { self.application = application + self.configuration = .init(logger: application.logger) + self.command = .init(application: application) + self.application.commands.use(self.command, as: "jobs") } + public func add(_ job: J) where J: Job { - application.register(extension: JobsConfiguration.self) { jobs, app in - jobs.add(job) - } + self.configuration.add(job) + } + + public func use(_ driver: JobsDriverFactory) { + self.driver = driver.factory(self) } - public func driver(_ driver: JobsDriver) { - application.register(instance: driver) + public func use(custom driver: JobsDriver) { + self.driver = driver } public func schedule(_ job: J) -> ScheduleBuilder where J: ScheduledJob { let builder = ScheduleBuilder() - application.register(extension: JobsConfiguration.self) { jobs, app in - _ = jobs.schedule(job, builder: builder) - } + _ = self.configuration.schedule(job, builder: builder) return builder } + + public func shutdown() { + self.command.shutdown() + if let driver = self.driver { + driver.shutdown() + } + } } extension Application { - public var jobs: ApplicationJobs { - return ApplicationJobs(for: self) + public var jobs: Jobs { + self.providers.require(Jobs.self) } } diff --git a/Sources/Jobs/JobsQueue.swift b/Sources/Jobs/JobsQueue.swift index 20e56bc..5ce8fa4 100644 --- a/Sources/Jobs/JobsQueue.swift +++ b/Sources/Jobs/JobsQueue.swift @@ -1,27 +1,65 @@ -import Foundation -import Vapor -import NIO +/// A type that can store and retrieve jobs from a persistence layer +public protocol JobsQueue { + var context: JobContext { get } + + func get(_ id: JobIdentifier) -> EventLoopFuture + func set(_ id: JobIdentifier, to data: JobData) -> EventLoopFuture + func clear(_ id: JobIdentifier) -> EventLoopFuture -/// A specific queue that jobs are run on. -public struct JobsQueue { - /// The default queue that jobs are run on - public static let `default` = JobsQueue(name: "default") - - /// The name of the queue - public let name: String + func pop() -> EventLoopFuture + func push(_ id: JobIdentifier) -> EventLoopFuture +} - /// Creates a new `QueueType` - /// - /// - Parameter name: The name of the `QueueType` - public init(name: String) { - self.name = name +extension JobsQueue { + public var eventLoop: EventLoop { + self.context.eventLoop } - - /// Makes the name of the queue - /// - /// - Parameter persistanceKey: The base persistence key - /// - Returns: A string of the queue's fully qualified name - public func makeKey(with persistanceKey: String) -> String { - return persistanceKey + "[\(self.name)]" + + public var logger: Logger { + self.context.logger + } + + public var configuration: JobsConfiguration { + self.context.configuration + } + + public var queueName: JobsQueueName { + self.context.queueName + } + + public var key: String { + self.queueName.makeKey(with: self.configuration.persistenceKey) + } + + public func dispatch( + _ job: J.Type, + _ payload: J.Payload, + maxRetryCount: Int = 0, + delayUntil: Date? = nil + ) -> EventLoopFuture + where J: Job + { + let bytes: [UInt8] + do { + bytes = try J.serializePayload(payload) + } catch { + return self.eventLoop.makeFailedFuture(error) + } + let id = JobIdentifier() + let storage = JobData( + payload: bytes, + maxRetryCount: maxRetryCount, + jobName: J.name, + delayUntil: delayUntil, + queuedAt: Date() + ) + return self.set(id, to: storage).flatMap { + self.push(id) + }.map { _ in + self.logger.info("Dispatched queue job", metadata: [ + "job_id": .string("\(id)"), + "queue": .string(self.queueName.string) + ]) + } } } diff --git a/Sources/Jobs/JobsQueueName.swift b/Sources/Jobs/JobsQueueName.swift new file mode 100644 index 0000000..da085bc --- /dev/null +++ b/Sources/Jobs/JobsQueueName.swift @@ -0,0 +1,23 @@ +/// A specific queue that jobs are run on. +public struct JobsQueueName { + /// The default queue that jobs are run on + public static let `default` = JobsQueueName(string: "default") + + /// The name of the queue + public let string: String + + /// Creates a new `QueueType` + /// + /// - Parameter name: The name of the `QueueType` + public init(string: String) { + self.string = string + } + + /// Makes the name of the queue + /// + /// - Parameter persistanceKey: The base persistence key + /// - Returns: A string of the queue's fully qualified name + public func makeKey(with persistanceKey: String) -> String { + return persistanceKey + "[\(self.string)]" + } +} diff --git a/Sources/Jobs/JobsService.swift b/Sources/Jobs/JobsService.swift deleted file mode 100644 index 38d0924..0000000 --- a/Sources/Jobs/JobsService.swift +++ /dev/null @@ -1,94 +0,0 @@ -import Foundation -import Logging -import Vapor - -public protocol JobsService { - var logger: Logger { get } - var driver: JobsDriver { get } - var eventLoopPreference: JobsEventLoopPreference { get } - var configuration: JobsConfiguration { get } -} - -extension JobsService { - public func dispatch( - _ job: Job.Type, - _ jobData: Job.Data, - maxRetryCount: Int = 0, - queue: JobsQueue = .default, - delayUntil: Date? = nil - ) -> EventLoopFuture - where Job: Jobs.Job - { - let data: Data - do { - data = try JSONEncoder().encode(jobData) - } catch { - return self.eventLoopPreference.delegate( - for: self.driver.eventLoopGroup - ).makeFailedFuture(error) - } - let jobID = UUID().uuidString - let jobStorage = JobStorage( - key: self.configuration.persistenceKey, - data: data, - maxRetryCount: maxRetryCount, - id: jobID, - jobName: Job.jobName, - delayUntil: delayUntil, - queuedAt: Date() - ) - return self.driver.set( - key: queue.makeKey(with: self.configuration.persistenceKey), - job: jobStorage, - eventLoop: self.eventLoopPreference - ).map { _ in - self.logger.info("Dispatched queue job", metadata: [ - "job_id": .string("\(jobID)"), - "queue": .string(queue.name) - ]) - } - } - - public func with(_ request: Request) -> JobsService { - return RequestSpecificJobsService(request: request, service: self, configuration: self.configuration) - } -} - -struct ApplicationJobsService: JobsService { - let configuration: JobsConfiguration - let driver: JobsDriver - let logger: Logger - let eventLoopPreference: JobsEventLoopPreference -} - -extension Request { - public var jobs: JobsService { - return self.application.make(JobsService.self).with(self) - } -} - - -private struct RequestSpecificJobsService: JobsService { - public let request: Request - public let service: JobsService - - var driver: JobsDriver { - return self.service.driver - } - - var logger: Logger { - return self.request.logger - } - - var eventLoopPreference: JobsEventLoopPreference { - return .delegate(on: self.request.eventLoop) - } - - public var configuration: JobsConfiguration - - init(request: Request, service: JobsService, configuration: JobsConfiguration) { - self.request = request - self.configuration = configuration - self.service = service - } -} diff --git a/Sources/Jobs/JobsWorker.swift b/Sources/Jobs/JobsWorker.swift index 11d31bd..74d42e4 100644 --- a/Sources/Jobs/JobsWorker.swift +++ b/Sources/Jobs/JobsWorker.swift @@ -1,126 +1,70 @@ -import Foundation -import NIO -import Vapor -import NIOConcurrencyHelpers - -final class JobsWorker { - let configuration: JobsConfiguration - let driver: JobsDriver - var context: JobContext { - return .init( - userInfo: self.configuration.userInfo, - on: self.eventLoop - ) - } - let logger: Logger - let eventLoop: EventLoop - - var onShutdown: EventLoopFuture { - return self.shutdownPromise.futureResult - } - - private let shutdownPromise: EventLoopPromise - private var isShuttingDown: Atomic - private var repeatedTask: RepeatedTask? - - init( - configuration: JobsConfiguration, - driver: JobsDriver, - logger: Logger, - on eventLoop: EventLoop - ) { - self.configuration = configuration - self.eventLoop = eventLoop - self.driver = driver - self.logger = logger - self.shutdownPromise = self.eventLoop.makePromise() - self.isShuttingDown = .init(value: false) +extension JobsQueue { + public var worker: JobsQueueWorker { + .init(queue: self) } +} - func start(on queue: JobsQueue) { - // Schedule the repeating task - self.repeatedTask = eventLoop.scheduleRepeatedAsyncTask( - initialDelay: .seconds(0), - delay: self.configuration.refreshInterval - ) { task in - // run task - return self.run(on: queue).map { - //Check if shutting down - if self.isShuttingDown.load() { - task.cancel() - self.shutdownPromise.succeed(()) - } - }.recover { error in - self.logger.error("Job run failed: \(error)") - } - } - } +public struct JobsQueueWorker { + let queue: JobsQueue - func shutdown() { - self.isShuttingDown.store(true) + init(queue: JobsQueue) { + self.queue = queue } - private func run(on queue: JobsQueue) -> EventLoopFuture { - let key = queue.makeKey(with: self.configuration.persistenceKey) - // self.logger.debug("Jobs worker running", metadata: ["key": .string(key)]) - - return self.driver.get(key: key, eventLoop: .delegate(on: self.eventLoop)).flatMap { jobStorage in + public func run() -> EventLoopFuture { + self.queue.pop().flatMap { id in //No job found, go to the next iteration - guard let jobStorage = jobStorage else { - return self.eventLoop.makeSucceededFuture(()) - } - - // If the job has a delay, we must check to make sure we can execute. If the delay has not passed yet, requeue the job - if let delay = jobStorage.delayUntil, delay >= Date() { - return self.driver.requeue( - key: key, - job: jobStorage, - eventLoop: .delegate(on: self.eventLoop) - ) + guard let id = id else { + return self.queue.eventLoop.makeSucceededFuture(()) } + return self.queue.get(id).flatMap { data in + // If the job has a delay, we must check to make sure we can execute. + // If the delay has not passed yet, requeue the job + if let delay = data.delayUntil, delay >= Date() { + return self.queue.push(id) + } - guard let job = self.configuration.make(for: jobStorage.jobName) else { - let error = Abort(.internalServerError) - self.logger.error("No job named \(jobStorage.jobName) is registered") - return self.eventLoop.makeFailedFuture(error) - } + guard let job = self.queue.configuration.jobs[data.jobName] else { + self.queue.logger.error("No job named \(data.jobName) is registered") + return self.queue.eventLoop.makeSucceededFuture(()) + } - self.logger.info("Dequeing Job", metadata: ["job_id": .string(jobStorage.id)]) - let jobRunPromise = self.eventLoop.makePromise(of: Void.self) - self.firstJobToSucceed( - job: job, - jobContext: self.context, - jobStorage: jobStorage, - tries: jobStorage.maxRetryCount) - .flatMapError { error in - self.logger.error("Error: \(error)", metadata: ["job_id": .string(jobStorage.id)]) - return job.error(self.context, error, jobStorage) - }.whenComplete { _ in - self.driver.completed( - key: key, - job: jobStorage, - eventLoop: .delegate(on: self.eventLoop) - ).cascade(to: jobRunPromise) + self.queue.logger.info("Dequeing Job", metadata: ["job_id": .string(id.string)]) + var logger = self.queue.logger + logger[metadataKey: "job_id"] = .string(id.string) + return self.run( + job: job, + payload: data.payload, + logger: logger, + remainingTries: data.maxRetryCount + ).flatMap { + self.queue.clear(id) + } } - - return jobRunPromise.futureResult } } - private func firstJobToSucceed( + private func run( job: AnyJob, - jobContext: JobContext, - jobStorage: JobStorage, - tries: Int + payload: [UInt8], + logger: Logger, + remainingTries: Int ) -> EventLoopFuture { - let futureJob = job.anyDequeue(jobContext, jobStorage) + let futureJob = job._dequeue(self.queue.context, payload: payload) return futureJob.map { complete in return complete }.flatMapError { error in - if tries == 0 { - return self.eventLoop.makeFailedFuture(error) + if remainingTries == 0 { + logger.error("Job failed: \(error)") + return job._error(self.queue.context, error, payload: payload) } else { - return self.firstJobToSucceed(job: job, jobContext: jobContext, jobStorage: jobStorage, tries: tries - 1) + logger.error("Retrying job: \(error)") + return self.run( + job: job, + payload: payload, + logger: logger, + remainingTries: remainingTries - 1 + ) } } } diff --git a/Sources/Jobs/Scheduler/ScheduleBuilder.swift b/Sources/Jobs/ScheduleBuilder.swift similarity index 80% rename from Sources/Jobs/Scheduler/ScheduleBuilder.swift rename to Sources/Jobs/ScheduleBuilder.swift index 0265b60..a4ec747 100644 --- a/Sources/Jobs/Scheduler/ScheduleBuilder.swift +++ b/Sources/Jobs/ScheduleBuilder.swift @@ -1,9 +1,8 @@ -import Foundation +import struct Foundation.DateComponents +import struct Foundation.Calendar /// An object that can be used to build a scheduled job public final class ScheduleBuilder { - // MARK: Data Structures - /// Months of the year public enum Month: Int { case january = 1 @@ -32,7 +31,7 @@ public final class ScheduleBuilder { } /// Describes a day of the week - public enum DayOfWeek: Int { + public enum Weekday: Int { case sunday = 1 case monday = 2 case tuesday = 3 @@ -283,8 +282,8 @@ public final class ScheduleBuilder { /// The day of week to run the job on /// - Parameter dayOfWeek: A `DayOfWeek` to run the job on - public func on(_ dayOfWeek: DayOfWeek) -> Daily { - self.builder.dayOfWeek = dayOfWeek + public func on(_ weekday: Weekday) -> Daily { + self.builder.weekday = weekday return self.builder.daily() } } @@ -327,7 +326,7 @@ public final class ScheduleBuilder { } /// An object to build a `EveryMinute` scheduled job - public struct EveryMinute { + public struct Minutely { let builder: ScheduleBuilder /// The second to run the job at @@ -336,74 +335,57 @@ public final class ScheduleBuilder { self.builder.second = second } } - - /// returns the next date that satisfies the schedule - internal func resolveNextDateThatSatisifiesSchedule(date: Date) throws -> Date { - if let oneTimeDate = self.date { - return oneTimeDate + + public func nextDate(current: Date = .init()) -> Date? { + if let date = self.date, date > current { + return date } - var monthConstraint: MonthRecurrenceRuleConstraint? - if let monthValue = month?.rawValue { - monthConstraint = try MonthRecurrenceRuleConstraint.atMonth(monthValue) + var components = DateComponents() + if let second = self.second { + components.second = second.number } - - var dayOfMonthConstraint: DayOfMonthRecurrenceRuleConstraint? - if let dayValue = day { - switch dayValue { - case .first: - dayOfMonthConstraint = try DayOfMonthRecurrenceRuleConstraint.atDayOfMonth(1) - case .last: - dayOfMonthConstraint = try DayOfMonthRecurrenceRuleConstraint.atLastDayOfMonth() - case .exact(let exactValue): - dayOfMonthConstraint = try DayOfMonthRecurrenceRuleConstraint.atDayOfMonth(exactValue) - } + if let minute = self.minute { + components.minute = minute.number } - - var dayOfWeekConstraint: DayOfWeekRecurrenceRuleConstraint? - if let dayOfWeek = dayOfWeek { - dayOfWeekConstraint = try DayOfWeekRecurrenceRuleConstraint.atDayOfWeek(dayOfWeek.rawValue) + if let time = self.time { + components.minute = time.minute.number + components.hour = time.hour.number } - - var hourConstraint: HourRecurrenceRuleConstraint? - if let hourValue = time?.hour.number { - hourConstraint = try HourRecurrenceRuleConstraint.atHour(hourValue) + if let weekday = self.weekday { + components.weekday = weekday.rawValue } - - var minuteConstraint: MinuteRecurrenceRuleConstraint? - if let timeMinuteValue = time?.minute.number { - minuteConstraint = try MinuteRecurrenceRuleConstraint.atMinute(timeMinuteValue) + if let day = self.day { + switch day { + case .first: + components.day = 1 + case .last: + fatalError("Last day of the month is not yet supported.") + case .exact(let exact): + components.day = exact + } } - - if let minuteValue = minute?.number { - minuteConstraint = try MinuteRecurrenceRuleConstraint.atMinute(minuteValue) + if let month = self.month { + components.month = month.rawValue } - - let secondConstraint = try SecondRecurrenceRuleConstraint.atSecond(second.number) - let recurrenceRule = try RecurrenceRule(yearConstraint: nil, - monthConstraint: monthConstraint, - dayOfMonthConstraint: dayOfMonthConstraint, - dayOfWeekConstraint: dayOfWeekConstraint, - hourConstraint: hourConstraint, - minuteConstraint: minuteConstraint, - secondConstraint: secondConstraint) - - return try recurrenceRule.resolveNextDateThatSatisfiesRule(currentDate: date) + return Calendar.current.nextDate( + after: current, + matching: components, + matchingPolicy: .strict + ) } - - // MARK: Properties /// Date to perform task (one-off job) var date: Date? - var month: Month? var day: Day? - var dayOfWeek: DayOfWeek? + var weekday: Weekday? var time: Time? var minute: Minute? - var second: Second = Second(0) + var second: Second? + var millisecond: Int? - init() { } + public init() { } // MARK: Helpers @@ -437,7 +419,13 @@ public final class ScheduleBuilder { return Hourly(builder: self) } - public func everyMinute() -> EveryMinute { - return EveryMinute(builder: self) + @discardableResult + public func minutely() -> Minutely { + return Minutely(builder: self) + } + + public func everySecond() { + self.millisecond = 0 } } + diff --git a/Sources/Jobs/ScheduledJob.swift b/Sources/Jobs/ScheduledJob.swift new file mode 100644 index 0000000..16771b4 --- /dev/null +++ b/Sources/Jobs/ScheduledJob.swift @@ -0,0 +1,47 @@ +import class NIO.RepeatedTask + +/// Describes a job that can be scheduled and repeated +public protocol ScheduledJob { + var name: String { get } + /// The method called when the job is run + /// - Parameter context: A `JobContext` that can be used + func run(context: JobContext) -> EventLoopFuture +} + +extension ScheduledJob { + public var name: String { "\(Self.self)" } +} + +class AnyScheduledJob { + let job: ScheduledJob + let scheduler: ScheduleBuilder + + init(job: ScheduledJob, scheduler: ScheduleBuilder) { + self.job = job + self.scheduler = scheduler + } +} +extension AnyScheduledJob { + struct Task { + let task: RepeatedTask + let done: EventLoopFuture + } + + func schedule(context: JobContext) -> Task? { + guard let date = self.scheduler.nextDate() else { + context.logger.debug("No date scheduled for \(self.job.name)") + return nil + } + context.logger.debug("Scheduling \(self.job.name) to run at \(date)") + let promise = context.eventLoop.makePromise(of: Void.self) + let task = context.eventLoop.scheduleRepeatedTask( + initialDelay: .microseconds(Int64(date.timeIntervalSinceNow * 1_000_000)), + delay: .seconds(0) + ) { task in + // always cancel + task.cancel() + self.job.run(context: context).cascade(to: promise) + } + return .init(task: task, done: promise.futureResult) + } +} diff --git a/Sources/Jobs/ScheduledJobsWorker.swift b/Sources/Jobs/ScheduledJobsWorker.swift deleted file mode 100644 index 4fa5caf..0000000 --- a/Sources/Jobs/ScheduledJobsWorker.swift +++ /dev/null @@ -1,97 +0,0 @@ -import Foundation -import NIO -import Vapor -import NIOConcurrencyHelpers - -final class ScheduledJobsWorker { - let configuration: JobsConfiguration - let logger: Logger - let eventLoop: EventLoop - var context: JobContext { - return .init( - userInfo: self.configuration.userInfo, - on: self.eventLoop - ) - } - - var onShutdown: EventLoopFuture { - return self.shutdownPromise.futureResult - } - - private let shutdownPromise: EventLoopPromise - private var isShuttingDown: Atomic - internal var scheduledJobs: [(AnyScheduledJob, Date)] - - init( - configuration: JobsConfiguration, - logger: Logger, - on eventLoop: EventLoop - ) { - self.configuration = configuration - self.eventLoop = eventLoop - self.logger = logger - self.shutdownPromise = self.eventLoop.makePromise() - self.isShuttingDown = .init(value: false) - self.scheduledJobs = [] - } - - func start() throws { - let scheduledJobsStartDates = configuration - .scheduledStorage - .map { - return ($0, try? $0.scheduler.resolveNextDateThatSatisifiesSchedule(date: Date())) - } - - var counter = 0 - for job in scheduledJobsStartDates { - if let date = job.1 { - // This means that it was successful in calculating the next applicable date - counter += 1 - scheduledJobs.append((job.0, date)) - self.run(job: job.0, date: date) - } - } - - // Shut down the promise immediately if there were no jobs scheduled - if counter == 0 { - self.shutdownPromise.succeed(()) - } - } - - private func run(job: AnyScheduledJob, date: Date) { - let initialDelay = TimeAmount.seconds(Int64(abs(date.timeIntervalSinceNow))) - eventLoop.scheduleRepeatedAsyncTask( - initialDelay: initialDelay, - delay: .seconds(0) - ) { task -> EventLoopFuture in - // Cancel no matter what - task.cancel() - - if self.isShuttingDown.load() { - self.shutdownPromise.succeed(()) - } - - return job.job.run(context: self.context).always { _ in - if job.scheduler.date != nil { - guard let index = self.scheduledJobs.firstIndex(where: { $0.0 === job }) else { return } - self.scheduledJobs.remove(at: index) - if self.scheduledJobs.first(where: { $0.0.scheduler.date != nil }) == nil { - // We do not have any scheduled jobs, check for one-off jobs - if self.scheduledJobs.filter({ $0.0.scheduler.date != nil }).count == 0 { - self.shutdownPromise.succeed(()) - } - } - } else { - if let nextDate = try? job.scheduler.resolveNextDateThatSatisifiesSchedule(date: date) { - self.scheduledJobs.append((job, nextDate)) - self.run(job: job, date: nextDate) - } - } - }.transform(to: ()) - } - } - - func shutdown() { - self.isShuttingDown.store(true) - } -} diff --git a/Sources/Jobs/Scheduler/DateExtensions.swift b/Sources/Jobs/Scheduler/DateExtensions.swift deleted file mode 100755 index b45cd60..0000000 --- a/Sources/Jobs/Scheduler/DateExtensions.swift +++ /dev/null @@ -1,486 +0,0 @@ -import Foundation - -internal extension Calendar { - enum CalendarExtensionError: Error { - case counldNotFindLowerBoundForRecurrenceRuleTimeUnit - case counldNotFindUpperBoundForRecurrenceRuleTimeUnit - } - - /// validates a value is within the bounds - /// - /// - Parameters: - /// - ruleTimeUnit: The TimeUnit to check value against - /// - value: the value to check - /// - Returns: true if value is valid, false if value is invalid - func validate(ruleTimeUnit: RecurrenceRule.TimeUnit, value: Int) throws -> Bool { - if let validLowerBound = try self.lowerBound(for: ruleTimeUnit) { - if value < validLowerBound { - return false - } - } - - if let validUpperBound = try Calendar.current.upperBound(for: ruleTimeUnit) { - if value > validUpperBound { - return false - } - } - - return true - } - - /// Finds the range amount (validUpperBound - validLowerBound) given a RecurrenceRule.TimeUnit - /// - /// - Parameter ruleTimeUnit: The RecurrenceRule.TimeUnit to reference - /// - Returns: The range (validUpperBound - validLowerBound) - /// - Throws: will throw if no lowerBound or upperBound is available for a given RecurrenceRule.TimeUnit - func rangeOfValidBounds(_ ruleTimeUnit: RecurrenceRule.TimeUnit) throws -> Int? { - guard let validLowerBound = try lowerBound(for: ruleTimeUnit) else { - return nil - } - - guard let validUpperBound = try upperBound(for: ruleTimeUnit) else { - return nil - } - - return validUpperBound - validLowerBound - } - - /// Resolves The lower bound (inclusive) of a given RecurrenceRule.TimeUnit - /// - /// - Parameter ruleTimeUnit: The referenced RecurrenceRule.TimeUnit - /// - Returns: The lower bound(inclusive) - func lowerBound(for ruleTimeUnit: RecurrenceRule.TimeUnit) throws -> Int? { - switch self.identifier { - case .gregorian, .iso8601: - return Calendar.gregorianLowerBound(for: ruleTimeUnit) - default: - throw CalendarExtensionError.counldNotFindLowerBoundForRecurrenceRuleTimeUnit - } - } - - /// Resolves The upper bound (inclusive) of a given RecurrenceRule.TimeUnit - /// - /// - Parameter ruleTimeUnit: The referenced RecurrenceRule.TimeUnit - /// - Returns: The upper bound(inclusive) - func upperBound(for ruleTimeUnit: RecurrenceRule.TimeUnit) throws -> Int? { - switch self.identifier { - case .gregorian, .iso8601: - return Calendar.gregorianUpperBound(for: ruleTimeUnit) - default: - throw CalendarExtensionError.counldNotFindUpperBoundForRecurrenceRuleTimeUnit - } - } - - static func gregorianLowerBound(for ruleTimeUnit: RecurrenceRule.TimeUnit) -> Int? { - switch ruleTimeUnit { - case .second: - return 0 - case .minute: - return 0 - case .hour: - return 0 - case .dayOfWeek: - return 1 - case .dayOfMonth: - return 1 - case .month: - return 1 - case .quarter: - return 1 - case .year: - return 1970 - } - } - - static func gregorianUpperBound(for ruleTimeUnit: RecurrenceRule.TimeUnit) -> Int? { - switch ruleTimeUnit { - case .second: - return 59 - case .minute: - return 59 - case .hour: - return 23 - case .dayOfWeek: - return 7 - case .dayOfMonth: - return 31 - case .month: - return 12 - case .quarter: - return 4 - case .year: - return nil - } - } -} - -// Date extensions that assist with evaluating recurrence rules -extension Date { - enum DateExtensionError: Error { - case couldNotFindNextDate - case couldNotIncrementDateByTimeUnit - case couldNotValidateDateComponentValue - case couldNotSetDateComponentToDefaultValue - } - - /// returns the second component (Calendar.Component.second) of the date - /// - /// - Parameter timeZone: The TimeZone to base the date off of (defaults to current timeZone) - /// - Returns: The second component of the date (0-59) - internal func second(atTimeZone timeZone: TimeZone? = nil) -> Int? { - return calendar(atTimeZone: timeZone).component(.second, from: self) - } - - /// returns the minute component (Calendar.Component.minute) of the date - /// - /// - Parameter timeZone: The TimeZone to base the date off of (defaults to current timeZone) - /// - Returns: The minute component of the date (0-59) - internal func minute(atTimeZone timeZone: TimeZone? = nil) -> Int? { - return calendar(atTimeZone: timeZone).component(.minute, from: self) - } - - /// returns the hour component (Calendar.Component.hour) of the date - /// - /// - Parameter timeZone: The TimeZone to base the date off of (defaults to current timeZone) - /// - Returns: The hour component of the date (0-23) - internal func hour(atTimeZone timeZone: TimeZone? = nil) -> Int? { - return calendar(atTimeZone: timeZone).component(.hour, from: self) - } - - /// returns the dayOfWeek (Calendar.Component.weekday) component of the date - /// - /// - Parameter timeZone: The TimeZone to base the date off of (defaults to current timeZone) - /// - Returns: The dayOfWeek of the date (1 Sunday, 7 Saturday) - internal func dayOfWeek(atTimeZone timeZone: TimeZone? = nil) -> Int? { - return calendar(atTimeZone: timeZone).component(.weekday, from: self) - } - - /// returns the dayOfMonth (Calendar.Component.day) component of the date - /// - /// - Parameter timeZone: The TimeZone to base the date off of (defaults to current timeZone) - /// - Returns: The dayOfMonth of the date (1-31) - internal func dayOfMonth(atTimeZone timeZone: TimeZone? = nil) -> Int? { - return calendar(atTimeZone: timeZone).component(.day, from: self) - } - - /// returns the weekOfMonth (Calendar.Component.weekOfMonth) component of the date - /// - /// - Parameter timeZone: The TimeZone to base the date off of (defaults to current timeZone) - /// - Returns: The weekOfMonth of the date (1-31) - internal func weekOfMonth(atTimeZone timeZone: TimeZone? = nil) -> Int? { - return calendar(atTimeZone: timeZone).component(.weekOfMonth, from: self) - } - - /// returns the weekOfYear (Calendar.Component.weekOfYear) component of the date - /// - /// - Parameter timeZone: The TimeZone to base the date off of (defaults to current timeZone) - /// - Returns: The weekOfYear of the date (1-52) - internal func weekOfYear(atTimeZone timeZone: TimeZone? = nil) -> Int? { - return calendar(atTimeZone: timeZone).component(.weekOfYear, from: self) - } - - /// returns the month (Calendar.Component.month) component of the date - /// - /// - Parameter timeZone: The TimeZone to base the date off of (defaults to current timeZone) - /// - Returns: The month of the date (1 (january) - 12 (December)) - internal func month(atTimeZone timeZone: TimeZone? = nil) -> Int? { - return calendar(atTimeZone: timeZone).component(.month, from: self) - } - - /// returns the quarter (Calendar.Component.quarter) component of the date - /// - /// - Parameter timeZone: The TimeZone to base the date off of (defaults to current timeZone) - /// - Returns: The quarter of the date (1-4) - internal func quarter(atTimeZone timeZone: TimeZone? = nil) -> Int? { - // Bug: doesn't work in ios 12 and macOS Mojave 10.14 - // return Calendar.current.component(.quarter.component(.month, from: self), from: self) - - // workaround - let formatter = DateFormatter() - formatter.dateFormat = "Q" - guard let quarter = Int(formatter.string(from: self)) else { - return nil - } - - // quarter must be between 1 and 4 (inclusive) - if quarter < 1 || quarter > 4 { - return nil - } - - return quarter - } - - /// returns the year (Calendar.Component.year) component of the date - /// - /// - Parameter timeZone: The TimeZone to base the date off of (defaults to current timeZone) - /// - Returns: The year of the date - internal func year(atTimeZone timeZone: TimeZone? = nil) -> Int? { - return calendar(atTimeZone: timeZone).component(.year, from: self) - } - - /// returns the yearForWeekOfYear (Calendar.Component.yearForWeekOfYear) component of the date - /// - /// - Parameter timeZone: The TimeZone to base the date off of (defaults to current timeZone) - /// - Returns: The yearForWeekOfYear of the datesss - internal func yearForWeekOfYear(atTimeZone timeZone: TimeZone? = nil) -> Int? { - return calendar(atTimeZone: timeZone).component(.yearForWeekOfYear, from: self) - } - - /// finds the number of weeks the year (52 or 53) - internal func weeksInYear() -> Int? { - func weeksInYearFormula(_ year: Int) -> Int { - return (year + year/4 - year/100 + year/400) % 7 - } - - guard let year = self.year() else { - return nil - } - - if weeksInYearFormula(year) == 4 || weeksInYearFormula(year-1) == 3 { - return 53 - } else { - return 52 - } - } - - /// Resolves the Calendar Component for a given RecurrenceRule.TimeUnit - /// - /// - Parameter ruleTimeUnit: The referenced RecurrenceRule.TimeUnit - /// - Returns: The associated Calendar.Component - private func resolveCalendarComponent(for ruleTimeUnit: RecurrenceRule.TimeUnit) -> Calendar.Component { - switch ruleTimeUnit { - case .second: - return .second - case .minute: - return .minute - case .hour: - return .hour - case .dayOfWeek: - return .weekday - case .dayOfMonth: - return .day - case .month: - return .month - case .quarter: - return .quarter - case .year: - return .year - } - } - - /// resolve the Calendar Component for a given RecurrenceRule.TimeUnit - /// - /// - Parameter ruleTimeUnit: The referenced RecurrenceRule.TimeUnits - /// - Returns: The associated Calendar Component - internal func dateComponentValue(for ruleTimeUnit: RecurrenceRule.TimeUnit, atTimeZone timeZone: TimeZone? = nil) -> Int? { - switch ruleTimeUnit { - case .second: - return self.second(atTimeZone: timeZone) - case .minute: - return self.minute(atTimeZone: timeZone) - case .hour: - return self.hour(atTimeZone: timeZone) - case .dayOfWeek: - return self.dayOfWeek(atTimeZone: timeZone) - case .dayOfMonth: - return self.dayOfMonth(atTimeZone: timeZone) - case .month: - return self.month(atTimeZone: timeZone) - case .quarter: - return self.quarter(atTimeZone: timeZone) - case .year: - return self.year(atTimeZone: timeZone) - } - } - - /// Returns the amount of years from another date - internal func years(from date: Date) -> Int? { - return Calendar.current.dateComponents([.year], from: date, to: self).year - } - - /// Returns the amount of quarters from another date - internal func quarters(from date: Date) -> Int? { - // bug in quarter on ios12 and macOS 10.14 mojave - //return Calendar.current.dateComponents([.quarter], from: date, to: self).quarter - let formatter = DateFormatter() - formatter.dateFormat = "Q" - return Int(formatter.string(from: date)) - } - - /// Returns the amount of months from another date - internal func months(from date: Date) -> Int? { - return Calendar.current.dateComponents([.month], from: date, to: self).month - } - - /// Returns the amount of weeks from another date - internal func weeks(from date: Date) -> Int? { - return Calendar.current.dateComponents([.weekOfMonth], from: date, to: self).weekOfMonth - } - - /// Returns the amount of days from another date - internal func days(from date: Date) -> Int? { - return Calendar.current.dateComponents([.day], from: date, to: self).day - } - - /// Returns the amount of hours from another date - internal func hours(from date: Date) -> Int? { - return Calendar.current.dateComponents([.hour], from: date, to: self).hour - } - - /// Returns the amount of minutes from another date - internal func minutes(from date: Date) -> Int? { - return Calendar.current.dateComponents([.minute], from: date, to: self).minute - } - - /// Returns the amount of seconds from another date - internal func seconds(from date: Date) -> Int? { - return Calendar.current.dateComponents([.second], from: date, to: self).second - } - - /// Produces a date by advancing a date component by a given value - /// - /// - Parameter calendarComponent: The compoent of the date to increment - /// - Returns: a date with the specified component incremented - internal func dateByIncrementing(calendarComponent: Calendar.Component) -> Date? { - return Calendar.current.date(byAdding: calendarComponent, value: 1, to: self) - } - - /// Produces a date by incrementing the date component associated with the RecurrenceRule.TimeUnit - /// - /// - Parameter ruleTimeUnit: The time unit to increment - /// - Returns: a date the referenced component incrementeds - internal func dateByIncrementing(_ ruleTimeUnit: RecurrenceRule.TimeUnit, atTimeZone timeZone: TimeZone? = nil) throws -> Date { - let calendarComponent = resolveCalendarComponent(for: ruleTimeUnit) - - /// sets the date component values lower than ruleTimeUnit to 0 - let dateWithDefaultValues = try setDateComponentValuesToDefault(lowerThan: ruleTimeUnit, date: self, atTimeZone: timeZone) - - guard let incrementedDate = dateWithDefaultValues.dateByIncrementing(calendarComponent: calendarComponent) else { - throw DateExtensionError.couldNotIncrementDateByTimeUnit - } - - return incrementedDate - } - - /// Finds the next possible date where the date component value associated with the `RecurrenceRule.TimeUnit` is - /// equal to given value - /// - /// For example: If ruleTimeUnit is .hour, nextValue is 4, and the self is `2019-02-16T14:42:20` then - /// the date returned will be `2019-02-17T04:00:00` - internal func nextDate(where ruleTimeUnit: RecurrenceRule.TimeUnit, is nextValue: Int, atTimeZone timeZone: TimeZone? = nil) throws -> Date { - guard let currentValue = self.dateComponentValue(for: ruleTimeUnit) else { - throw DateExtensionError.couldNotFindNextDate - } - let dateComponent = resolveCalendarComponent(for: ruleTimeUnit) - - /// sets the date component values lower than ruleTimeUnit to 0 - let dateWithDefaultValues = try setDateComponentValuesToDefault(lowerThan: ruleTimeUnit, date: self, atTimeZone: timeZone) - - /// Finds how many units to add to the date component to get to the next value - let unitsToAdd = try resolveUnitsToAdd(ruleTimeUnit: ruleTimeUnit, currentValue: currentValue, nextValue: nextValue) - - /// Advances the date component by the given units - guard let nextDate = Calendar.current.date(byAdding: dateComponent, value: unitsToAdd, to: dateWithDefaultValues) else { - throw DateExtensionError.couldNotFindNextDate - } - return nextDate - } - - /// Sets lower date components to their default values - /// - /// For Example: If ruleTimeUnit is .month then the date component value associated with .dayOfMonth will be is set to 1 and - /// the date components associated with .hour, .minute, and .second value will be set to 0 - private func setDateComponentValuesToDefault(lowerThan ruleTimeUnit: RecurrenceRule.TimeUnit, date: Date, atTimeZone timeZone: TimeZone? = nil) throws -> Date { - var lowerCadenceLevelUnits = [RecurrenceRule.TimeUnit]() - - switch ruleTimeUnit { - case .second: - lowerCadenceLevelUnits = [] - case .minute: - lowerCadenceLevelUnits = [.second] - case .hour: - lowerCadenceLevelUnits = [.second, .minute] - case .dayOfMonth, .dayOfWeek: - lowerCadenceLevelUnits = [.second, .minute, .hour] - case .month: - lowerCadenceLevelUnits = [.second, .minute, .hour, .dayOfMonth] - case .quarter, .year: - lowerCadenceLevelUnits = [.second, .minute, .hour, .dayOfMonth, .month] - } - - var newDate = date - for unit in lowerCadenceLevelUnits { - newDate = try setDateComponentValueToDefault(unit, date: newDate, atTimeZone: timeZone) - } - - return newDate - } - - /// Sets the a date component to its default value (i.e second to 0, hour to 0, dayOfMonth to 1, month to 1) - private func setDateComponentValueToDefault(_ ruleTimeUnit: RecurrenceRule.TimeUnit, date: Date, atTimeZone timeZone: TimeZone? = nil) throws -> Date { - guard let defaultValue = try Calendar.current.lowerBound(for: ruleTimeUnit) else { - return date - } - guard let currentValue = self.dateComponentValue(for: ruleTimeUnit, atTimeZone: timeZone) else { - throw DateExtensionError.couldNotValidateDateComponentValue - } - - let dateComponent = resolveCalendarComponent(for: ruleTimeUnit) - let unitsToSubtract = currentValue - defaultValue - - guard let dateWithDefaultComponentValue = Calendar.current.date(byAdding: dateComponent, value: -unitsToSubtract, to: date) else { - throw DateExtensionError.couldNotSetDateComponentToDefaultValue - } - return dateWithDefaultComponentValue - } - - /// Finds how many units to advance the value of the date component by to to get to the next value - /// - /// For Example: If ruleTimeUnit is .seconds, currentValue is 55, and nextValue is 5 then 9 would be returned - private func resolveUnitsToAdd(ruleTimeUnit: RecurrenceRule.TimeUnit, currentValue: Int, nextValue: Int) throws -> Int { - let isCurrentValueValid = try Calendar.current.validate(ruleTimeUnit: ruleTimeUnit, value: currentValue) - let isNextValueValid = try Calendar.current.validate(ruleTimeUnit: ruleTimeUnit, value: nextValue) - if isCurrentValueValid == false || isNextValueValid == false { - throw DateExtensionError.couldNotValidateDateComponentValue - } - - var unitsToAdd = nextValue - currentValue - if unitsToAdd <= 0 { - if let rangeOfValidBounds = try Calendar.current.rangeOfValidBounds(ruleTimeUnit) { - unitsToAdd = unitsToAdd + rangeOfValidBounds - } - } - - if unitsToAdd == 0 { - unitsToAdd = 1 - } - - return unitsToAdd - } - - /// Checks if the date lies on the last day of the month - internal func isLastDayOfMonth() throws -> Bool { - let tomorrow = try self.dateByIncrementing(.dayOfMonth) - if tomorrow.dayOfMonth() == 1 { - return true - } - - return false - } - - internal func numberOfDaysInMonth() -> Int? { - guard let range = Calendar.current.range(of: .day, in: .month, for: self) else { - return nil - } - return range.count - } - - private func calendar(atTimeZone timeZone: TimeZone? = nil) -> Calendar { - var calendar = Calendar.current - if let timeZone = timeZone { - calendar.timeZone = timeZone - } - return calendar - } - -} diff --git a/Sources/Jobs/Scheduler/RecurrenceRule.swift b/Sources/Jobs/Scheduler/RecurrenceRule.swift deleted file mode 100755 index 47834f6..0000000 --- a/Sources/Jobs/Scheduler/RecurrenceRule.swift +++ /dev/null @@ -1,411 +0,0 @@ -import Foundation - -enum RecurrenceRuleError: Error { - case atLeastOneRecurrenceRuleConstraintRequiredToIntialize - case lowerBoundGreaterThanUpperBound - case noSetConstraintForRecurrenceRuleTimeUnit - case couldNotResolveDateComponentValueFromRecurrenceRuleTimeUnit - case noConstraintsSetForRecurrenceRule - case coundNotResolveNextInstanceWithin1000Years - case couldNotResolveYearComponentValueFromDate - case couldNotResloveNextValueFromConstraint - case ruleInsatiable - case couldNotParseHourAndMinuteFromString - case startHourAndMinuteGreaterThanEndHourAndMinute -} - -/// Defines the rule for when to run a job based on the given constraints -/// -/// - warning: RecurrenceRule only supports the Gregorian calendar (i.e. Calendar.identifier.gregorian or Calendar.identifier.iso8601) -/// -/// - Note: RecurrenceRule uses the local TimeZone as default -internal struct RecurrenceRule { - internal enum TimeUnit: CaseIterable { - case second - case minute - case hour - case dayOfWeek - case dayOfMonth - case month - case quarter - case year - } - - var timeZone: TimeZone - - private(set) var yearConstraint: YearRecurrenceRuleConstraint? - private(set) var quarterConstraint: QuarterRecurrenceRuleConstraint? - private(set) var monthConstraint: MonthRecurrenceRuleConstraint? - private(set) var dayOfMonthConstraint: DayOfMonthRecurrenceRuleConstraint? - private(set) var dayOfWeekConstraint: DayOfWeekRecurrenceRuleConstraint? - private(set) var hourConstraint: HourRecurrenceRuleConstraint? - private(set) var minuteConstraint: MinuteRecurrenceRuleConstraint? - private(set) var secondConstraint: SecondRecurrenceRuleConstraint? - - private let timeUnitOrder: [RecurrenceRule.TimeUnit] = [ - .year, - .quarter, - .month, - .dayOfMonth, - .dayOfWeek, - .hour, - .minute, - .second - ] - - init(timeZone: TimeZone = TimeZone.current) { - self.timeZone = timeZone - } - - init(yearConstraint: YearRecurrenceRuleConstraint? = nil, - monthConstraint: MonthRecurrenceRuleConstraint? = nil, - dayOfMonthConstraint: DayOfMonthRecurrenceRuleConstraint? = nil, - dayOfWeekConstraint: DayOfWeekRecurrenceRuleConstraint? = nil, - hourConstraint: HourRecurrenceRuleConstraint? = nil, - minuteConstraint: MinuteRecurrenceRuleConstraint? = nil, - secondConstraint: SecondRecurrenceRuleConstraint? = nil, - timeZone: TimeZone = TimeZone.current) throws { - self.timeZone = timeZone - self.yearConstraint = yearConstraint - self.monthConstraint = monthConstraint - self.dayOfMonthConstraint = dayOfMonthConstraint - self.dayOfWeekConstraint = dayOfWeekConstraint - self.hourConstraint = hourConstraint - self.minuteConstraint = minuteConstraint - self.secondConstraint = secondConstraint - } - - internal mutating func setYearConstraint(_ yearConstraint: YearRecurrenceRuleConstraint) { - self.yearConstraint = yearConstraint - } - - internal mutating func setQuarterConstraint(_ quarterConstraint: QuarterRecurrenceRuleConstraint) { - self.quarterConstraint = quarterConstraint - } - - internal mutating func setMonthConstraint(_ monthConstraint: MonthRecurrenceRuleConstraint) { - self.monthConstraint = monthConstraint - } - - internal mutating func setDayOfMonthConstraint(_ dayOfMonthConstraint: DayOfMonthRecurrenceRuleConstraint) { - self.dayOfMonthConstraint = dayOfMonthConstraint - } - - internal mutating func setDayOfWeekConstraint(_ dayOfWeekConstraint: DayOfWeekRecurrenceRuleConstraint) { - self.dayOfWeekConstraint = dayOfWeekConstraint - } - - internal mutating func setHourConstraint(_ hourConstraint: HourRecurrenceRuleConstraint) { - self.hourConstraint = hourConstraint - } - - internal mutating func setMinuteConstraint(_ minuteConstraint: MinuteRecurrenceRuleConstraint) { - self.minuteConstraint = minuteConstraint - } - - internal mutating func setSecondConstraint(_ secondConstraint: SecondRecurrenceRuleConstraint) { - self.secondConstraint = secondConstraint - } - - /// Sets the timeZone used by rule constraintss - /// - /// - Parameter timeZone: The TimeZone constraints reference against - internal mutating func usingTimeZone(_ timeZone: TimeZone) { - self.timeZone = timeZone - } -} - -/// Extension for the evaluation of a `RecurrenceRule`s -extension RecurrenceRule { - internal struct EvaluationResult { - /// Set to true if the date satisfies the `RecurrenceRule` - let isValid: Bool - - /// The `RecurrenceRule.TimeUnit` associated with the highest failing constraint - let timeUnitFailedOn: RecurrenceRule.TimeUnit? - - init(isValid: Bool, timeUnitFailedOn: RecurrenceRule.TimeUnit?) { - self.isValid = isValid - self.timeUnitFailedOn = timeUnitFailedOn - } - } - - /// Evaluates if the constraints are satified at a given date - /// - /// - Parameter date: The date to test the constraints against - /// - Returns: returns true if all constraints are satisfied for the given date - public func evaluate(date: Date) throws -> Bool { - return try evaluate(date: date).isValid - } - - /// Evaluate the `RecurrenceRule`'s constraints to deterine it they are satisfied - /// - /// - NOTE: Upon an evaluation result of false the `timeUnitFailedOn` property of `RecurrenceRule.EvaluationResult` - /// is always set to `RecurrenceRule.TimeUnit` of the highest failing constraint so that it's associated date component value can - /// be advanced first. This impoves the efficiency when finding the next date that satisfies the `RecurrenceRule` - private func evaluate(date: Date) throws -> RecurrenceRule.EvaluationResult { - var ruleEvaluationState = EvaluationState.noComparisonAttempted - var ruleTimeUnitFailedOn: RecurrenceRule.TimeUnit? - - for ruleTimeUnit in timeUnitOrder { - guard let dateComponentValue = date.dateComponentValue(for: ruleTimeUnit, atTimeZone: timeZone) else { - throw RecurrenceRuleError.couldNotResolveDateComponentValueFromRecurrenceRuleTimeUnit - } - - if let specificConstraint = resolveSpecificConstraint(ruleTimeUnit) { - // evaluates the constraint - let constraintEvalutionState = specificConstraint.evaluate(dateComponentValue) - - if constraintEvalutionState != .noComparisonAttempted { - ruleEvaluationState = constraintEvalutionState - } - } else { - // constraint not set - let lowestCadence = resolveLowestCadence() - let lowestCadenceLevel = resolveCadenceLevel(lowestCadence) - let currentConstraintCadenceLevel = resolveCadenceLevel(ruleTimeUnit) - - /// If second, minute, hour, dayOfMonth or month constraints are not set - /// they must be at their default values to avoid the rule passing on every second - if (currentConstraintCadenceLevel <= lowestCadenceLevel) { - if ruleTimeUnit == .second && dateComponentValue != 0 { - ruleEvaluationState = .failed - } else if ruleTimeUnit == .minute && dateComponentValue != 0 { - ruleEvaluationState = .failed - } else if ruleTimeUnit == .hour && dateComponentValue != 0 { - ruleEvaluationState = .failed - } else if ruleTimeUnit == .dayOfMonth && dateComponentValue != 1 { - ruleEvaluationState = .failed - } else if ruleTimeUnit == .month && dateComponentValue != 1 { - ruleEvaluationState = .failed - } - } - } - - if ruleEvaluationState == .failed { - // break iteraton - ruleTimeUnitFailedOn = ruleTimeUnit - break - } - } - - if ruleEvaluationState == .passing { - return EvaluationResult(isValid: true, timeUnitFailedOn: nil) - } else { - return EvaluationResult(isValid: false, timeUnitFailedOn: ruleTimeUnitFailedOn) - } - } - - /// Finds the next date from the starting date that satisfies the rule - /// - /// - Warning: The search is exhausted after the year 3000 - /// - /// - Parameter currentDate: The starting date, a date after this will be searched for - /// - Returns: The next date that satisfies the rule - internal func resolveNextDateThatSatisfiesRule(currentDate: Date) throws -> Date { - guard let timeUnitOfLowestActiveConstraint = resolveTimeUnitOfActiveConstraintWithLowestCadenceLevel() else { - throw RecurrenceRuleError.noConstraintsSetForRecurrenceRule - } - - /// Defines a year when to stop searching - guard let currentYear = currentDate.year() else { - throw RecurrenceRuleError.couldNotResolveYearComponentValueFromDate - } - let exhaustedYear = currentYear + 1000 - - /// Throws an error if rule contains constraints that can never be satisfied - try checkForInsatiableConstraints() - - /// Increments the `RecurrenceRule.TimeUnit` of the lowest active constraint so that - /// the currentDate will not be returned (The result must be some date after the currentDate parameter) - var dateToTest = try currentDate.dateByIncrementing(timeUnitOfLowestActiveConstraint, atTimeZone: timeZone) - - var isNextInstanceFound = false - var isSearchExhausted = false - var numOfChecksMade = 0 - while isNextInstanceFound == false && isSearchExhausted == false { - /// Evaluates if the dateToTest satisifes the` RecurrenceRule`. If the dateToTest does not satisfy the `RecurrenceRule` - /// the associated date component value of the `SpecificRecurrenceRuleConstraint`that failed is advanced. - /// For example if ruleTimeUnitFailedOn equals `.hour`, the hour component of dateToTest is advanced to the next valid value. - if let ruleTimeUnitFailedOn = try self.evaluate(date: dateToTest).timeUnitFailedOn { - - /// Advances the associated date component value of the failed `RecurrenceRuel.TimeUnit` to the next valid value - /// as defined by the SpecificRecurrenceRuleConstraint - let nextValidValue = try resolveNextValidValue(for: ruleTimeUnitFailedOn, date: dateToTest) - dateToTest = try dateToTest.nextDate(where: ruleTimeUnitFailedOn, is: nextValidValue, atTimeZone: timeZone) - - /// Search is exhausted if the year component of dateToTest is greater than the exhaustedYear - if let year = dateToTest.year() { - if year > exhaustedYear { - isSearchExhausted = true - } - } else { - throw RecurrenceRuleError.couldNotResolveYearComponentValueFromDate - } - numOfChecksMade += 1 - } else { - isNextInstanceFound = true - } - } - - if isNextInstanceFound { - return dateToTest - } else { - throw RecurrenceRuleError.coundNotResolveNextInstanceWithin1000Years - } - } - - /// Throws error if rule contains constraints that can never be satisfiedss - private func checkForInsatiableConstraints() throws { - // January, March, May, July, August, October, December - let monthsWithExactly31Days = [1, 3, 5, 7, 8, 10, 12] - // April, June, September, November - let monthsWithExactly30Days = [4, 6, 9, 11] - // februrary has 28 or 29 days - - guard let dayOfMonthLowestPossibleValue = dayOfMonthConstraint?.lowestPossibleValue else { - return - } - - if dayOfMonthLowestPossibleValue > 30 { - var hasAtLeastOneMonthWith31Days = false - if let monthConstraint = monthConstraint { - for month in monthsWithExactly31Days { - if monthConstraint.evaluate(month) == .passing { - hasAtLeastOneMonthWith31Days = true - } - } - } - - if hasAtLeastOneMonthWith31Days == false { - throw RecurrenceRuleError.ruleInsatiable - } - } else if dayOfMonthLowestPossibleValue > 29 { - var hasOneMonthWithAtLeast30Days = false - - if let monthConstraint = monthConstraint { - for month in monthsWithExactly31Days { - if monthConstraint.evaluate(month) == .passing { - hasOneMonthWithAtLeast30Days = true - } - } - for month in monthsWithExactly30Days { - if monthConstraint.evaluate(month) == .passing { - hasOneMonthWithAtLeast30Days = true - } - } - } - - if hasOneMonthWithAtLeast30Days == false { - throw RecurrenceRuleError.ruleInsatiable - } - } - } - - private func resolveTimeUnitOfActiveConstraintWithLowestCadenceLevel() -> RecurrenceRule.TimeUnit? { - var activeConstraintTimeUnitWithLowestCadenceLevel: RecurrenceRule.TimeUnit? - - for ruleTimeUnit in timeUnitOrder { - let constraint = resolveSpecificConstraint(ruleTimeUnit) - if constraint != nil { - activeConstraintTimeUnitWithLowestCadenceLevel = ruleTimeUnit - } - } - - return activeConstraintTimeUnitWithLowestCadenceLevel - } - - /// Finds the the next valid value for the constraint given the current date - private func resolveNextValidValue(for ruleTimeUnit: RecurrenceRule.TimeUnit, date: Date) throws -> Int { - guard let currentValue = date.dateComponentValue(for: ruleTimeUnit, atTimeZone: timeZone) else { - throw RecurrenceRuleError.couldNotResolveDateComponentValueFromRecurrenceRuleTimeUnit - } - - if let specificConstraint = resolveSpecificConstraint(ruleTimeUnit) { - - var nextValidValue: Int? - // if dayOfMonth constraint and dayOfMonth constarint is in effect, return value for last dayOfMonth - if isLastDayOfMonthConstraint(specificConstraint) { - if try date.isLastDayOfMonth() { - return 0 - } else { - nextValidValue = date.numberOfDaysInMonth() - } - } else { - nextValidValue = specificConstraint.nextValidValue(currentValue: currentValue) - } - - guard let nextValue = nextValidValue else { - throw RecurrenceRuleError.couldNotResloveNextValueFromConstraint - } - - return nextValue - } else { - throw RecurrenceRuleError.couldNotResloveNextValueFromConstraint - } - } - - private func isLastDayOfMonthConstraint(_ specificConstraint: SpecificRecurrenceRuleConstraint) -> Bool { - if specificConstraint._constraint.timeUnit == .dayOfMonth { - if let dayOfMonthSpecificConstraint = specificConstraint as? DayOfMonthRecurrenceRuleConstraint { - return dayOfMonthSpecificConstraint.isLimitedToLastDayOfMonth - } - } - return false - } - - // get a specificConstraint by its RecurrenceRule.TimeUnit - private func resolveSpecificConstraint(_ ruleTimeUnit: RecurrenceRule.TimeUnit) -> SpecificRecurrenceRuleConstraint? { - switch ruleTimeUnit { - case .second: - return self.secondConstraint - case .minute: - return self.minuteConstraint - case .hour: - return self.hourConstraint - case .dayOfWeek: - return self.dayOfWeekConstraint - case .dayOfMonth: - return self.dayOfMonthConstraint - case .month: - return self.monthConstraint - case .quarter: - return self.quarterConstraint - case .year: - return self.yearConstraint - } - } - - private func resolveLowestCadence() -> RecurrenceRule.TimeUnit { - if secondConstraint != nil { - return .second - } else if minuteConstraint != nil { - return .minute - } else if hourConstraint != nil { - return .hour - } else if dayOfMonthConstraint != nil { - return .dayOfMonth - } else if monthConstraint != nil { - return .month - } else { - return .year - } - } - - private func resolveCadenceLevel(_ ruleTimeUnit: RecurrenceRule.TimeUnit) -> Int { - switch ruleTimeUnit { - case .second: - return 0 - case .minute: - return 1 - case .hour: - return 2 - case .dayOfMonth: - return 3 - case .month: - return 4 - default: - return 5 - } - } -} diff --git a/Sources/Jobs/Scheduler/RecurrenceRuleConstraint.swift b/Sources/Jobs/Scheduler/RecurrenceRuleConstraint.swift deleted file mode 100755 index 0eb8929..0000000 --- a/Sources/Jobs/Scheduler/RecurrenceRuleConstraint.swift +++ /dev/null @@ -1,285 +0,0 @@ -import Foundation - -enum RecurrenceRuleConstraintError: Error { - case constraintAmountLessThanLowerBound - case constraintAmountGreaterThanUpperBound -} - -internal enum EvaluationState { - case noComparisonAttempted - case failed - case passing -} - -internal enum RecurrenceRuleConstraintType { - case set - case range - case step -} - -internal protocol RecurrenceRuleConstraintEvaluateable { - var lowestPossibleValue: Int? { get } - var highestPossibleValue: Int? { get } - - func evaluate(_ evaluationAmount: Int) -> EvaluationState - func nextValidValue(currentValue: Int) -> Int? -} - -internal protocol RecurrenceRuleConstraint: RecurrenceRuleConstraintEvaluateable { - var timeUnit: RecurrenceRule.TimeUnit { get } - var type: RecurrenceRuleConstraintType { get } - var validLowerBound: Int? { get } - var validUpperBound: Int? { get } - - static func validate(value: Int, validLowerBound: Int?, validUpperBound: Int?) throws -} - -/// default implementation -extension RecurrenceRuleConstraint { - /// validates a constraint value is within the constraint bounds - static func validate(value: Int, validLowerBound: Int?, validUpperBound: Int?) throws { - if let lowerBound = validLowerBound { - if value < lowerBound { - throw RecurrenceRuleConstraintError.constraintAmountLessThanLowerBound - } - } - - if let upperBound = validUpperBound { - if value > upperBound { - throw RecurrenceRuleConstraintError.constraintAmountGreaterThanUpperBound - } - } - } -} - -/// A `RecurrenceRuleConstraint` that limits valid values to a given set -/// Equivalent in cron to a single value or list (i.e 14 or 8,3,6,21) -internal struct RecurrenceRuleSetConstraint: RecurrenceRuleConstraint, Equatable { - - let timeUnit: RecurrenceRule.TimeUnit - let type = RecurrenceRuleConstraintType.set - let validLowerBound: Int? - let validUpperBound: Int? - let setConstraint: Set - - var lowestPossibleValue: Int? { - setConstraint.min() - } - - var highestPossibleValue: Int? { - setConstraint.max() - } - - init (timeUnit: RecurrenceRule.TimeUnit, setConstraint: Set = Set()) throws { - self.timeUnit = timeUnit - self.validLowerBound = Calendar.gregorianLowerBound(for: timeUnit) - self.validUpperBound = Calendar.gregorianUpperBound(for: timeUnit) - - for amount in setConstraint { - try Self.validate(value: amount, validLowerBound: validLowerBound, validUpperBound: validUpperBound) - } - self.setConstraint = setConstraint - } - - /// Evaluates if a given amount satisfies the constraint - /// - /// - Parameter evaluationAmount: The amount to test - /// - Returns: passing, failed, or noComparisonAttempted - internal func evaluate(_ evaluationAmount: Int) -> EvaluationState { - if setConstraint.contains(evaluationAmount) { - return EvaluationState.passing - } else { - return EvaluationState.failed - } - } - - /// Finds the the next value that satisfies the constraint - /// - /// - Parameter currentValue: The current value the date component - /// - Returns: The next value that satisfies the constraint - internal func nextValidValue(currentValue: Int) -> Int? { - var lowestValueGreaterThanCurrentValue: Int? - - for value in setConstraint { - if value >= currentValue { - if let low = lowestValueGreaterThanCurrentValue { - if value < low { - lowestValueGreaterThanCurrentValue = value - } - } else { - lowestValueGreaterThanCurrentValue = value - } - } - } - - if lowestValueGreaterThanCurrentValue != nil { - return lowestValueGreaterThanCurrentValue - } else { - return lowestPossibleValue - } - } -} - -/// A `RecurrenceRuleConstraint` that limits valid values to a given range -/// Equivalent in cron to a range of values (i.e 12-6) -internal struct RecurrenceRuleRangeConstraint: RecurrenceRuleConstraint, Equatable { - let timeUnit: RecurrenceRule.TimeUnit - let type = RecurrenceRuleConstraintType.range - let validLowerBound: Int? - let validUpperBound: Int? - let rangeConstraint: ClosedRange - - var lowestPossibleValue: Int? { - return rangeConstraint.min() - } - - var highestPossibleValue: Int? { - return rangeConstraint.max() - } - - init(timeUnit: RecurrenceRule.TimeUnit, rangeConstraint: ClosedRange) throws { - self.timeUnit = timeUnit - self.validLowerBound = Calendar.gregorianLowerBound(for: timeUnit) - self.validUpperBound = Calendar.gregorianUpperBound(for: timeUnit) - - try Self.validate(value: rangeConstraint.lowerBound, validLowerBound: validLowerBound, validUpperBound: validUpperBound) - try Self.validate(value: rangeConstraint.upperBound, validLowerBound: validLowerBound, validUpperBound: validUpperBound) - self.rangeConstraint = rangeConstraint - } - - /// Evaluates if a given amount satisfies the constraint - /// - /// - Parameter evaluationAmount: The amount to test - /// - Returns: passing, failed, or noComparisonAttempted - internal func evaluate(_ evaluationAmount: Int) -> EvaluationState { - if rangeConstraint.contains(evaluationAmount) { - return .passing - } else { - return .failed - } - } - - /// Finds the the next value that satisfies the constraint - /// - /// - Parameter currentValue: The current value the date component - /// - Returns: The next value that satisfies the constraint - internal func nextValidValue(currentValue: Int) -> Int? { - var lowestValueGreaterThanCurrentValue: Int? - - if (currentValue + 1) <= rangeConstraint.upperBound { - if let low = lowestValueGreaterThanCurrentValue { - if low >= (currentValue + 1) { - lowestValueGreaterThanCurrentValue = (currentValue + 1) - } - } else { - lowestValueGreaterThanCurrentValue = (currentValue + 1) - } - } - - if lowestValueGreaterThanCurrentValue != nil { - return lowestValueGreaterThanCurrentValue - } else { - return lowestPossibleValue - } - } -} - -/// A `RecurrenceRuleConstraint` that limits valid values to a given set -/// Equivalent in cron to a step value (i.e */2) -internal struct RecurrenceRuleStepConstraint: RecurrenceRuleConstraint, Equatable { - let timeUnit: RecurrenceRule.TimeUnit - let type = RecurrenceRuleConstraintType.step - let validLowerBound: Int? - let validUpperBound: Int? - let stepConstraint: Int - - var lowestPossibleValue: Int? { - return 0 - } - - var highestPossibleValue: Int? { - return nil - } - - init(timeUnit: RecurrenceRule.TimeUnit, stepConstraint: Int) throws { - self.timeUnit = timeUnit - self.validLowerBound = Calendar.gregorianLowerBound(for: timeUnit) - self.validUpperBound = Calendar.gregorianUpperBound(for: timeUnit) - - if stepConstraint < 1 { - throw RecurrenceRuleConstraintError.constraintAmountLessThanLowerBound - } - if let validUpperBound = validUpperBound { - if stepConstraint < 1 { - throw RecurrenceRuleConstraintError.constraintAmountLessThanLowerBound - } - if stepConstraint > validUpperBound { - throw RecurrenceRuleConstraintError.constraintAmountGreaterThanUpperBound - } - } - - self.stepConstraint = stepConstraint - } - - /// Evaluates if a given amount satisfies the constraint - /// - /// - Parameter evaluationAmount: The amount to test - /// - Returns: passing, failed, or noComparisonAttempted - internal func evaluate(_ evaluationAmount: Int) -> EvaluationState { - // pass if evaluationAmount is divisiable of stepConstriant - if evaluationAmount % stepConstraint == 0 { - return EvaluationState.passing - } else { - return EvaluationState.failed - } - } - - /// Finds the the next value that satisfies the constraint - /// - /// - Parameter currentValue: The current value the date component - /// - Returns: The next value that satisfies the constraint - internal func nextValidValue(currentValue: Int) -> Int? { - var lowestValueGreaterThanCurrentValue: Int? - - // step - var multiple = 0 - var shouldStopLooking = false - - if let validUpperBound = validUpperBound { - // others - var shouldStopLooking = false - while multiple <= validUpperBound && shouldStopLooking == false { - if multiple >= currentValue { - if let low = lowestValueGreaterThanCurrentValue { - if multiple < low { - lowestValueGreaterThanCurrentValue = multiple - } - } else { - lowestValueGreaterThanCurrentValue = multiple - } - shouldStopLooking = true - } - multiple = multiple + stepConstraint - } - } else { - // year - while shouldStopLooking == false { - if multiple >= currentValue { - if let low = lowestValueGreaterThanCurrentValue { - if multiple < low { - lowestValueGreaterThanCurrentValue = multiple - } - } else { - lowestValueGreaterThanCurrentValue = multiple - } - shouldStopLooking = true - } - - multiple = multiple + stepConstraint - } - } - - return lowestValueGreaterThanCurrentValue ?? lowestPossibleValue - } - -} diff --git a/Sources/Jobs/Scheduler/SpecificRecurrenceRuleConstraints.swift b/Sources/Jobs/Scheduler/SpecificRecurrenceRuleConstraints.swift deleted file mode 100755 index 59f4aff..0000000 --- a/Sources/Jobs/Scheduler/SpecificRecurrenceRuleConstraints.swift +++ /dev/null @@ -1,540 +0,0 @@ -import Foundation - -enum SpecificRecurrenceRuleConstraintError: Error { - case incompatibleConstriantTimeUnit -} - -/// `SpecificRecurrenceRuleConstraint`s are limited to a single `RecurrenceRule.TimeUnit` and have static methods for convenient initialization -protocol SpecificRecurrenceRuleConstraint: RecurrenceRuleConstraintEvaluateable { - static var timeUnit: RecurrenceRule.TimeUnit { get } - static var validLowerBound: Int? { get } - static var validUpperBound: Int? { get } - var _constraint: RecurrenceRuleConstraint { get } - - init(constraint: RecurrenceRuleConstraint) throws -} - -/// default implementations -extension SpecificRecurrenceRuleConstraint { - /// The lower bound of the constraint's `TimeUnit` - static var validLowerBound: Int? { - return Calendar.gregorianLowerBound(for: timeUnit) - } - - /// The upper bound of the constraint's `TimeUnit` - static var validUpperBound: Int? { - return Calendar.gregorianUpperBound(for: timeUnit) - } - - /// The lowest value that satisfies the constraint - internal var lowestPossibleValue: Int? { - return _constraint.lowestPossibleValue - } - - /// The highest value that satisfies the constraint - internal var highestPossibleValue: Int? { - return _constraint.highestPossibleValue - } - - /// Evaluates if a given amount satisfies the constraint - internal func evaluate(_ evaluationAmount: Int) -> EvaluationState { - return _constraint.evaluate(evaluationAmount) - } - - /// Finds the the next value that satisfies the constraint - internal func nextValidValue(currentValue: Int) -> Int? { - return _constraint.nextValidValue(currentValue: currentValue) - } -} - -/// A constraint that limits the year value of a`RecurrenceRule` to given a set, range, or step -internal struct YearRecurrenceRuleConstraint: SpecificRecurrenceRuleConstraint { - static let timeUnit = RecurrenceRule.TimeUnit.year - let _constraint: RecurrenceRuleConstraint - - internal init(constraint: RecurrenceRuleConstraint) throws { - if constraint.timeUnit != YearRecurrenceRuleConstraint.timeUnit { - throw SpecificRecurrenceRuleConstraintError.incompatibleConstriantTimeUnit - } - _constraint = constraint - } - - /// The year the job will run pending all other constraints are met - /// - /// - Parameter year: Lower bound: 1970, Upper bound: 3000 - internal static func atYear(_ year: Int) throws -> YearRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleSetConstraint.init(timeUnit: timeUnit, setConstraint: [year])) - } - - /// The years the job will run pending all other constraints are met - /// - /// - Parameter year: Lower bound: 1970, Upper bound: 3000 - internal static func atYears(_ years: Set) throws -> YearRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleSetConstraint.init(timeUnit: timeUnit, setConstraint: years)) - } - - /// The range of the years (inclusive) the job will run pending all other constraints are met - /// - Parameter lowerBound: must be at least 1 - /// - Parameter upperBound: must not greater than 3000 - internal static func atYearsInRange(lowerBound: Int, upperBound: Int) throws -> YearRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleRangeConstraint.init(timeUnit: timeUnit, rangeConstraint: lowerBound...upperBound)) - } - - /// Defines the step value of a constraint - /// - /// - Note: inputed values are step values. (ex a step value in cron could be: */22) - /// For example: minuteStep(22) will be satisfied at every minute that is divisible by 22 including 0: - /// ..., 3:44, 04:00, 04:22, 04:44, 05:00, 05:22, 05:44, 06:00 etc - /// - /// - Parameter yearStep: the step value to be scheduled - internal static func yearStep(_ stepValue: Int) throws -> YearRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleStepConstraint.init(timeUnit: timeUnit, stepConstraint: stepValue)) - } -} - -/// A constraint that limits the quarter value of a`RecurrenceRule` to given a set, range, or step -internal struct QuarterRecurrenceRuleConstraint: SpecificRecurrenceRuleConstraint { - static let timeUnit = RecurrenceRule.TimeUnit.quarter - let _constraint: RecurrenceRuleConstraint - - internal init(constraint: RecurrenceRuleConstraint) throws { - if constraint.timeUnit != QuarterRecurrenceRuleConstraint.timeUnit { - throw SpecificRecurrenceRuleConstraintError.incompatibleConstriantTimeUnit - } - _constraint = constraint - } - - /// The quarter the job will run pending all other constraints are met - /// - /// - Parameter quarter: Lower bound: 1, Upper bound: 4 - internal static func atQuarter(_ quarter: Int) throws -> QuarterRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleSetConstraint.init(timeUnit: timeUnit, setConstraint: [quarter])) - } - - /// The quarters the job will run pending all other constraints are met - /// - /// - Parameter quarter: Lower bound: 1, Upper bound: 4 - internal static func atQuarters(_ quarters: Set) throws -> QuarterRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleSetConstraint.init(timeUnit: timeUnit, setConstraint: quarters)) - } - - /// The range of the quarters (inclusive) the job will run pending all other constraints are met - /// - Parameter lowerBound: must be at least 1 - /// - Parameter upperBound: must not greater than 4 - internal static func atQuartersInRange(lowerBound: Int, upperBound: Int) throws -> QuarterRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleRangeConstraint.init(timeUnit: timeUnit, rangeConstraint: lowerBound...upperBound)) - } - - /// Defines the step value of a constraint - /// - /// - Note: inputed values are step values. (ex a step value in cron could be: */22) - /// For example: minuteStep(22) will be satisfied at every minute that is divisible by 22 including 0: - /// ..., 3:44, 04:00, 04:22, 04:44, 05:00, 05:22, 05:44, 06:00 etc - /// - /// - Parameter quarterStep: the step value to be scheduled - internal static func quarterStep(_ stepValue: Int) throws -> QuarterRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleStepConstraint.init(timeUnit: timeUnit, stepConstraint: stepValue)) - } -} - -/// A constraint that limits the month value of a`RecurrenceRule` to given a set, range, or step -internal struct MonthRecurrenceRuleConstraint: SpecificRecurrenceRuleConstraint { - static let timeUnit = RecurrenceRule.TimeUnit.month - let _constraint: RecurrenceRuleConstraint - - internal init(constraint: RecurrenceRuleConstraint) throws { - if constraint.timeUnit != MonthRecurrenceRuleConstraint.timeUnit { - throw SpecificRecurrenceRuleConstraintError.incompatibleConstriantTimeUnit - } - _constraint = constraint - } - - /// The month the job will run pending all other constraints are met - /// - /// - Note: 1 is January, 12 is December - /// - Parameter month: Lower bound: 1, Upper bound: 12 - internal static func atMonth(_ month: Int) throws -> MonthRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleSetConstraint.init(timeUnit: timeUnit, setConstraint: [month])) - } - - /// The months the job will run pending all other constraints are met - /// - /// - Note: 1 is January, 12 is December - /// - Parameter month: Lower bound: 1, Upper bound: 12 - internal static func atMonths(_ months: Set) throws -> MonthRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleSetConstraint.init(timeUnit: timeUnit, setConstraint: months)) - } - - /// The range of the months (inclusive) the job will run pending all other constraints are met - /// - Note: 1 is January, 12 is December - /// - Parameter lowerBound: must be at least 1 - /// - Parameter upperBound: must not greater than 12 - internal static func atMonthsInRange(lowerBound: Int, upperBound: Int) throws -> MonthRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleRangeConstraint.init(timeUnit: timeUnit, rangeConstraint: lowerBound...upperBound)) - } - - /// Defines the step value of a constraint - /// - /// - Note: inputed values are step values. (ex a step value in cron could be: */22) - /// For example: minuteStep(22) will be satisfied at every minute that is divisible by 22 including 0: - /// ..., 3:44, 04:00, 04:22, 04:44, 05:00, 05:22, 05:44, 06:00 etc - /// - /// - Parameter monthStep: the step value to be scheduled - internal static func monthStep(_ stepValue: Int) throws -> MonthRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleStepConstraint.init(timeUnit: timeUnit, stepConstraint: stepValue)) - } -} - -/// A constraint that limits the dayOfMonth value of a`RecurrenceRule` to given a set, range, or step -internal struct DayOfMonthRecurrenceRuleConstraint: SpecificRecurrenceRuleConstraint { - static let timeUnit = RecurrenceRule.TimeUnit.dayOfMonth - let _constraint: RecurrenceRuleConstraint - internal let isLimitedToLastDayOfMonth: Bool - - internal init(constraint: RecurrenceRuleConstraint) throws { - if constraint.timeUnit != DayOfMonthRecurrenceRuleConstraint.timeUnit { - throw SpecificRecurrenceRuleConstraintError.incompatibleConstriantTimeUnit - } - self.isLimitedToLastDayOfMonth = false - _constraint = constraint - } - - internal init(constraint: RecurrenceRuleConstraint, isLimitedToLastDayOfMonth: Bool = false) throws { - if constraint.timeUnit != DayOfMonthRecurrenceRuleConstraint.timeUnit { - throw SpecificRecurrenceRuleConstraintError.incompatibleConstriantTimeUnit - } - self.isLimitedToLastDayOfMonth = isLimitedToLastDayOfMonth - _constraint = constraint - } - - internal func evaluate(_ evaluationAmount: Int) -> EvaluationState { - return _constraint.evaluate(evaluationAmount) - } - - internal func nextValidValue(currentValue: Int) -> Int? { - return _constraint.nextValidValue(currentValue: currentValue) - } - - /// The dayOfMonth the job will run pending all other constraints are met - /// - /// - Parameter dayOfMonth: Lower bound: 1, Upper bound: 31 - internal static func atDayOfMonth(_ dayOfMonth: Int) throws -> DayOfMonthRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleSetConstraint.init(timeUnit: timeUnit, setConstraint: [dayOfMonth])) - } - - /// The dayOfMonth the job will run pending all other constraints are met - /// - /// - Parameter dayOfMonth: Lower bound: 1, Upper bound: 31 - internal static func atDaysOfMonth(_ daysOfMonth: Set) throws -> DayOfMonthRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleSetConstraint.init(timeUnit: timeUnit, setConstraint: daysOfMonth)) - } - - /// The range of the days of the month (inclusive) the job will run pending all other constraints are met - /// - Parameter lowerBound: must be at least 1 - /// - Parameter upperBound: must not greater than 31 - internal static func atDaysOfMonthInRange(lowerBound: Int, upperBound: Int) throws -> DayOfMonthRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleRangeConstraint.init(timeUnit: timeUnit, rangeConstraint: lowerBound...upperBound)) - } - - /// Defines the step value of a constraint - /// - /// - Note: inputed values are step values. (ex a step value in cron could be: */22) - /// For example: minuteStep(22) will be satisfied at every minute that is divisible by 22 (including 0): - /// ..., 3:44, 04:00, 04:22, 04:44, 05:00, 05:22, 05:44, 06:00 etc - /// - /// - Parameter dayOfMonthStep: the step value to be scheduled - internal static func dayOfMonthStep(_ stepValue: Int) throws -> DayOfMonthRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleStepConstraint.init(timeUnit: timeUnit, stepConstraint: stepValue)) - } - - /// Limits the job to run only on the last day of the month - internal static func atLastDayOfMonth() throws -> DayOfMonthRecurrenceRuleConstraint { - let isLimitedToLastDayOfMonth = true - return try .init(constraint: RecurrenceRuleSetConstraint.init(timeUnit: timeUnit, setConstraint: [28, 29, 30, 31]), isLimitedToLastDayOfMonth: isLimitedToLastDayOfMonth) - } -} - -/// A constraint that limits the dayOfWeek value of a`RecurrenceRule` to given a set, range, or step -internal struct DayOfWeekRecurrenceRuleConstraint: SpecificRecurrenceRuleConstraint { - static let timeUnit = RecurrenceRule.TimeUnit.dayOfWeek - let _constraint: RecurrenceRuleConstraint - - internal init(constraint: RecurrenceRuleConstraint) throws { - if constraint.timeUnit != DayOfWeekRecurrenceRuleConstraint.timeUnit { - throw SpecificRecurrenceRuleConstraintError.incompatibleConstriantTimeUnit - } - _constraint = constraint - } - - /// The dayOfWeek the job will run pending all other constraints are met - /// - /// - Note: 1 is Sunday, 7 is Saturday - /// - Parameter dayOfWeek: Lower bound: 1, Upper bound: 7 - internal static func atDayOfWeek(_ dayOfWeek: Int) throws -> DayOfWeekRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleSetConstraint.init(timeUnit: timeUnit, setConstraint: [dayOfWeek])) - } - - /// The dayOfWeek the job will run pending all other constraints are met - /// - /// - Note: 1 is Sunday, 7 is Saturday - /// - Parameter dayOfWeek: Lower bound: 1, Upper bound: 7 - internal static func atDaysOfWeek(_ daysOfWeek: Set) throws -> DayOfWeekRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleSetConstraint.init(timeUnit: timeUnit, setConstraint: daysOfWeek)) - } - - /// The range of the days of the week (inclusive) the job will run pending all other constraints are met - /// - Note: 1 is Sunday, 7 is Saturday - /// - Parameter lowerBound: must be at least 1 - /// - Parameter upperBound: must not greater than 7 - internal static func atDaysOfWeekInRange(lowerBound: Int, upperBound: Int) throws -> DayOfWeekRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleRangeConstraint.init(timeUnit: timeUnit, rangeConstraint: lowerBound...upperBound)) - } - - /// Defines the step value of a constraint - /// - /// - Note: inputed values are step values. (ex a step value in cron could be: */22) - /// For example: minuteStep(22) will be satisfied at every minute that is divisible by 22 (including 0): - /// ..., 3:44, 04:00, 04:22, 04:44, 05:00, 05:22, 05:44, 06:00 etc - /// - /// - Parameter dayOfWeekStep: the step value to be scheduled - internal static func dayOfWeekStep(_ stepValue: Int) throws -> DayOfWeekRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleStepConstraint.init(timeUnit: timeUnit, stepConstraint: stepValue)) - } - - // convenience - - /// Limits the Job to run on Sundays - internal static func sundays() throws -> DayOfWeekRecurrenceRuleConstraint { - return try atDayOfWeek(1) - } - - /// Limits the Job to run on Mondays - internal static func mondays() throws -> DayOfWeekRecurrenceRuleConstraint { - return try atDayOfWeek(2) - } - - /// Limits the Job to run on Tuesdays - internal static func tuesdays() throws -> DayOfWeekRecurrenceRuleConstraint { - return try atDayOfWeek(3) - } - - /// Limits the Job to run on Wednesdays - internal static func wednesdays() throws -> DayOfWeekRecurrenceRuleConstraint { - return try atDayOfWeek(4) - } - - /// Limits the Job to run on Thursdays - internal static func thursdays() throws -> DayOfWeekRecurrenceRuleConstraint { - return try atDayOfWeek(5) - } - - /// Limits the Job to run on Fridays - internal static func fridays() throws -> DayOfWeekRecurrenceRuleConstraint { - return try atDayOfWeek(6) - } - - /// Limits the Job to run on Saturdays - internal static func saturdays() throws -> DayOfWeekRecurrenceRuleConstraint { - return try atDayOfWeek(7) - } - - /// Limits the Job to run on Weekdays (Mondays, Tuesdays, Wednesdays, Thursdays, Fridays) - internal static func weekdays() throws -> DayOfWeekRecurrenceRuleConstraint { - return try atDaysOfWeek([2, 3, 4, 5, 6]) - } - - /// Limits the Job to run on Weekends (Saturdays, Sundays) - internal static func weekends() throws -> DayOfWeekRecurrenceRuleConstraint { - return try atDaysOfWeek([1, 7]) - } -} - -/// A constraint that limits the hour value of a`RecurrenceRule` to given a set, range, or step -internal struct HourRecurrenceRuleConstraint: SpecificRecurrenceRuleConstraint { - static let timeUnit = RecurrenceRule.TimeUnit.hour - let _constraint: RecurrenceRuleConstraint - - internal init(constraint: RecurrenceRuleConstraint) throws { - if constraint.timeUnit != HourRecurrenceRuleConstraint.timeUnit { - throw SpecificRecurrenceRuleConstraintError.incompatibleConstriantTimeUnit - } - _constraint = constraint - } - - /// The hour the job will run pending all other constraints are met - /// - /// - Parameter hour: Lower bound: 0, Upper bound: 23 - internal static func atHour(_ hour: Int) throws -> HourRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleSetConstraint.init(timeUnit: timeUnit, setConstraint: [hour])) - } - - /// The hour the job will run pending all other constraints are met - /// - /// - Note: Uses the 24 hour clock - /// - Parameter hour: Lower bound: 0, Upper bound: 23 - internal static func atHours(_ hours: Set) throws -> HourRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleSetConstraint.init(timeUnit: timeUnit, setConstraint: hours)) - } - - /// The range of hours (inclusive) the job will run pending all other constraints are met - /// - Parameter lowerBound: must be at least 0 - /// - Parameter upperBound: must not greater than 23 - internal static func atHoursInRange(lowerBound: Int, upperBound: Int) throws -> HourRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleRangeConstraint.init(timeUnit: timeUnit, rangeConstraint: lowerBound...upperBound)) - } - - /// Defines the step value of a constraint - /// - /// - Note: inputed values are step values. (ex a step value in cron could be: */22) - /// For example: minuteStep(22) will be satisfied at every minute that is divisible by 22 (including 0) in the hour: - /// ..., 3:44, 04:00, 04:22, 04:44, 05:00, 05:22, 05:44, 06:00 etc - /// - /// - Parameter hourStep: the step value to be scheduled - internal static func hourStep(_ stepValue: Int) throws -> HourRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleStepConstraint.init(timeUnit: timeUnit, stepConstraint: stepValue)) - } -} - -/// A constraint that limits the minute value of a`RecurrenceRule` to given a set, range, or step -internal struct MinuteRecurrenceRuleConstraint: SpecificRecurrenceRuleConstraint { - static let timeUnit = RecurrenceRule.TimeUnit.minute - let _constraint: RecurrenceRuleConstraint - - internal init(constraint: RecurrenceRuleConstraint) throws { - if constraint.timeUnit != MinuteRecurrenceRuleConstraint.timeUnit { - throw SpecificRecurrenceRuleConstraintError.incompatibleConstriantTimeUnit - } - _constraint = constraint - } - - /// The minute the job will run pending all other constraints are met - /// - /// - Parameter minute: Lower bound: 0, Upper bound: 59 - internal static func atMinute(_ minute: Int) throws -> MinuteRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleSetConstraint.init(timeUnit: timeUnit, setConstraint: [minute])) - } - - /// The minute the job will run pending all other constraints are met - /// - /// - Parameter minute: Lower bound: 0, Upper bound: 59 - internal static func atMinutes(_ minutes: Set) throws -> MinuteRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleSetConstraint.init(timeUnit: timeUnit, setConstraint: minutes)) - } - - /// The range of minutes (inclusive) the job will run pending all other constraints are met - /// - Parameter lowerBound: must be at least 0 - /// - Parameter upperBound: must not greater than 59 - internal static func atMinutesInRange(lowerBound: Int, upperBound: Int) throws -> MinuteRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleRangeConstraint.init(timeUnit: timeUnit, rangeConstraint: lowerBound...upperBound)) - } - - /// Defines the step value of a constraint - /// - /// - Note: inputed values are step values. (ex a step value in cron could be: */22) - /// For example: minuteStep(22) will be satisfied at every minute that is divisible by 22 (including 0): - /// ..., 3:44, 04:00, 04:22, 04:44, 05:00, 05:22, 05:44, 06:00 etc - /// - /// - Parameter minuteStep: the step value to be scheduled - internal static func minuteStep(_ stepValue: Int) throws -> MinuteRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleStepConstraint.init(timeUnit: timeUnit, stepConstraint: stepValue)) - } - - // conveince - - /// Runs the job every minute - internal static func everyMinute() throws -> MinuteRecurrenceRuleConstraint { - return try self.minuteStep(1) - } - - /// Runs the job every 5 minutes - internal static func everyFiveMinutes() throws -> MinuteRecurrenceRuleConstraint { - return try self.minuteStep(5) - } - - /// Runs the job every 10 minutes - internal static func everyTenMinutes() throws -> MinuteRecurrenceRuleConstraint { - return try self.minuteStep(10) - } - - /// Runs the job every 15 minutes - internal static func everyFifteenMinutes() throws -> MinuteRecurrenceRuleConstraint { - return try self.minuteStep(15) - } - - /// Runs the job every 30 minutes - internal static func everyThirtyMinutes() throws -> MinuteRecurrenceRuleConstraint { - return try self.minuteStep(30) - } - -} - -/// A constraint that limits the second value of a`RecurrenceRule` to given a set, range, or step -internal struct SecondRecurrenceRuleConstraint: SpecificRecurrenceRuleConstraint { - static let timeUnit = RecurrenceRule.TimeUnit.second - let _constraint: RecurrenceRuleConstraint - - internal init(constraint: RecurrenceRuleConstraint) throws { - if constraint.timeUnit != SecondRecurrenceRuleConstraint.timeUnit { - throw SpecificRecurrenceRuleConstraintError.incompatibleConstriantTimeUnit - } - _constraint = constraint - } - - /// The second the job will run pending all other constraints are met - /// - /// - Parameter second: Lower bound: 0, Upper bound: 59 - internal static func atSecond(_ second: Int) throws -> SecondRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleSetConstraint.init(timeUnit: timeUnit, setConstraint: [second])) - } - - /// The second the job will run pending all other constraints are met - /// - /// - Parameter second: Lower bound: 0, Upper bound: 59 - internal static func atSeconds(_ seconds: Set) throws -> SecondRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleSetConstraint.init(timeUnit: timeUnit, setConstraint: seconds)) - } - - /// The range of seconds (inclusive) the job will run pending all other constraints are met - /// - Parameter lowerBound: must be at least 0 - /// - Parameter upperBound: 59 - internal static func atSecondsInRange(lowerBound: Int, upperBound: Int) throws -> SecondRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleRangeConstraint.init(timeUnit: timeUnit, rangeConstraint: lowerBound...upperBound)) - } - - /// Defines the step value of a constraint - /// - /// - Note: inputed values are step values. (ex a step value in cron could be: */22) - /// For example: minuteStep(22) will be satisfied at every minute that is divisible by 22 (including 0): - /// ..., 3:44, 04:00, 04:22, 04:44, 05:00, 05:22, 05:44, 06:00 etc - /// - /// - Parameter secondStep: the step value to be scheduled - internal static func secondStep(_ stepValue: Int) throws -> SecondRecurrenceRuleConstraint { - return try .init(constraint: RecurrenceRuleStepConstraint.init(timeUnit: timeUnit, stepConstraint: stepValue)) - } - - // convenience - /// Runs the job every second - internal static func everySecond() throws -> SecondRecurrenceRuleConstraint { - return try self.secondStep(1) - } - - /// Runs the job every 5 second - internal static func everyFiveSeconds() throws -> SecondRecurrenceRuleConstraint { - return try self.secondStep(5) - } - - /// Runs the job every 10 second - internal static func everyTenSeconds() throws -> SecondRecurrenceRuleConstraint { - return try self.secondStep(10) - } - - /// Runs the job every 15 second - internal static func everyFifteenSeconds() throws -> SecondRecurrenceRuleConstraint { - return try self.secondStep(15) - } - - /// Runs the job every 30 second - internal static func everyThirtySeconds() throws -> SecondRecurrenceRuleConstraint { - return try self.secondStep(30) - } -} diff --git a/Tests/JobsTests/JobStorageTests.swift b/Tests/JobsTests/JobStorageTests.swift deleted file mode 100644 index 39124a0..0000000 --- a/Tests/JobsTests/JobStorageTests.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// JobStorageTests.swift -// Async -// -// Created by Raul Riera on 2019-03-16. -// - -import XCTest -@testable import Jobs - -final class JobStorageTests: XCTestCase { - func testStringRepresentationIsValidJSON() { - let jobStorage = JobStorage(key: "vapor", - data: Data(), - maxRetryCount: 1, - id: "identifier", - jobName: "jobs", - delayUntil: nil, - queuedAt: Date()) - - let stringRepresentation = jobStorage.stringValue() - - if let data = stringRepresentation?.data(using: String.Encoding.utf8), let jobStorageRestored = try? JSONDecoder().decode(JobStorage.self, from: data) { - XCTAssertEqual(jobStorage.key, jobStorageRestored.key) - XCTAssertEqual(jobStorage.data, jobStorageRestored.data) - XCTAssertEqual(jobStorage.maxRetryCount, jobStorageRestored.maxRetryCount) - XCTAssertEqual(jobStorage.id, jobStorageRestored.id) - XCTAssertEqual(jobStorage.jobName, jobStorageRestored.jobName) - XCTAssertEqual(jobStorage.delayUntil, nil) - } else { - XCTFail("There was a problem restoring JobStorage") - } - } -} diff --git a/Tests/JobsTests/JobsConfigTests.swift b/Tests/JobsTests/JobsConfigTests.swift deleted file mode 100644 index 8ce3b8c..0000000 --- a/Tests/JobsTests/JobsConfigTests.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// JobsConfigTests.swift -// Async -// -// Created by Raul Riera on 2019-03-16. -// - -import XCTest -import NIO -@testable import Jobs - -struct DailyCleanup: ScheduledJob { - func run(context: JobContext) -> EventLoopFuture { - return context.eventLoop.makeSucceededFuture(()) - } -} - -final class JobsConfigTests: XCTestCase { - func testAddingJobs() { - var config = JobsConfiguration() - config.add(JobMock()) - - XCTAssertEqual(config.storage.count, 1) - XCTAssertEqual(config.storage.first?.key, "JobMock") - } - - func testAddingAlreadyRegistratedJobsAreIgnored() { - var config = JobsConfiguration() - config.add(JobMock()) - config.add(JobMock()) - - XCTAssertEqual(config.storage.count, 1) - XCTAssertNotNil(config.storage["JobMock"]) - - config.add(JobMock()) - - XCTAssertEqual(config.storage.count, 2) - XCTAssertNotNil(config.storage["JobMock"]) - } - - // https://github.com/vapor/jobs/issues/38 - func testAddingJobsWithTheSameDataType() { - struct JobOne: Job { - func dequeue(_ context: JobContext, _ data: [String : String]) -> EventLoopFuture { - fatalError() - } - - typealias Data = [String: String] - } - - struct JobTwo: Job { - func dequeue(_ context: JobContext, _ data: [String : String]) -> EventLoopFuture { - fatalError() - } - - typealias Data = [String: String] - } - - var config = JobsConfiguration() - config.add(JobOne()) - config.add(JobTwo()) - - XCTAssertEqual(config.storage.count, 2) - XCTAssertNotNil(config.storage["JobOne"]) - XCTAssertNotNil(config.storage["JobTwo"]) - } - - func testScheduledJob() throws { - var config = JobsConfiguration() - config.schedule(DailyCleanup()) - .daily() - .at("1:01am") - - XCTAssertEqual(config.scheduledStorage.count, 1) - } -} diff --git a/Tests/JobsTests/JobsTests.swift b/Tests/JobsTests/JobsTests.swift index 4567646..91a7585 100644 --- a/Tests/JobsTests/JobsTests.swift +++ b/Tests/JobsTests/JobsTests.swift @@ -1,59 +1,85 @@ import Jobs import Vapor -import XCTest +import XCTVapor @testable import Jobs final class JobsTests: XCTestCase { func testVaporIntegration() throws { - let server = try self.startServer() - defer { server.shutdown() } - let worker = try self.startWorker() - defer { worker.shutdown() } + let app = Application(.testing) + defer { app.shutdown() } + app.use(Jobs.self) + app.jobs.use(custom: TestDriver()) + + let promise = app.eventLoopGroup.next().makePromise(of: String.self) + app.jobs.add(Foo(promise: promise)) - FooJob.dequeuePromise = server.make(EventLoopGroup.self) - .next().makePromise(of: Void.self) + app.get("foo") { req in + req.jobs.dispatch(Foo.self, .init(foo: "bar")) + .map { "done" } + } - let task = server.make(EventLoopGroup.self).next().scheduleTask(in: .seconds(5)) { - return server.client.get("http://localhost:8080/foo") + try app.testable().test(.GET, "foo") { res in + XCTAssertEqual(res.status, .ok) + XCTAssertEqual(res.body.string, "done") } - let res = try task.futureResult.wait().wait() - XCTAssertEqual(res.body?.string, "done") - try FooJob.dequeuePromise!.futureResult.wait() + XCTAssertEqual(TestQueue.queue.count, 1) + XCTAssertEqual(TestQueue.jobs.count, 1) + let job = TestQueue.jobs[TestQueue.queue[0]]! + XCTAssertEqual(job.jobName, "Foo") + XCTAssertEqual(job.maxRetryCount, 0) + + try app.jobs.queue.worker.run().wait() + XCTAssertEqual(TestQueue.queue.count, 0) + XCTAssertEqual(TestQueue.jobs.count, 0) + + try XCTAssertEqual(promise.futureResult.wait(), "bar") } - - func testVaporScheduledJob() throws { - let app = try self.startServer() + + func testScheduleBuilderAPI() throws { + let app = Application(.testing) defer { app.shutdown() } - app.jobs.schedule(Cleanup()).hourly().at(30) - app.jobs.schedule(Cleanup()).at(Date() + 5) - - XCTAssertEqual(app.make(JobsConfiguration.self).scheduledStorage.count, 2) - } - - private func startServer() throws -> Application { - let app = self.setupApplication(.init(name: "worker", arguments: ["vapor", "serve"])) - try app.start() - return app - } - - private func startWorker() throws -> Application { - let app = self.setupApplication(.init(name: "worker", arguments: ["vapor", "jobs"])) - try app.start() - return app - } - - private func setupApplication(_ env: Environment) -> Application { - let app = Application(environment: env) - app.provider(JobsProvider()) - app.jobs.driver(TestDriver(on: app.make())) - app.jobs.add(FooJob()) - app.get("foo") { req in - return req.jobs.dispatch(FooJob.self, .init(foo: "bar")) - .map { "done" } - } - return app + app.use(Jobs.self) + + // yearly + app.jobs.schedule(Cleanup()) + .yearly() + .in(.may) + .on(23) + .at(.noon) + + // monthly + app.jobs.schedule(Cleanup()) + .monthly() + .on(15) + .at(.midnight) + + // weekly + app.jobs.schedule(Cleanup()) + .weekly() + .on(.monday) + .at("3:13am") + + // daily + app.jobs.schedule(Cleanup()) + .daily() + .at("5:23pm") + + // daily 2 + app.jobs.schedule(Cleanup()) + .daily() + .at(5, 23, .pm) + + // daily 3 + app.jobs.schedule(Cleanup()) + .daily() + .at(17, 23) + + // hourly + app.jobs.schedule(Cleanup()) + .hourly() + .at(30) } } @@ -63,67 +89,71 @@ extension ByteBuffer { } } -var storage: [String: JobStorage] = [:] -var lock = Lock() - -final class TestDriver: JobsDriver { - var eventLoopGroup: EventLoopGroup +struct TestDriver: JobsDriver { + func makeQueue(with context: JobContext) -> JobsQueue { + TestQueue(context: context) + } - init(on eventLoopGroup: EventLoopGroup) { - self.eventLoopGroup = eventLoopGroup + func shutdown() { + // nothing } +} + +struct TestQueue: JobsQueue { + static var queue: [JobIdentifier] = [] + static var jobs: [JobIdentifier: JobData] = [:] + static var lock: Lock = .init() - func get(key: String, eventLoop: JobsEventLoopPreference) -> EventLoopFuture { - lock.lock() - defer { lock.unlock() } - let job: JobStorage? - if let existing = storage[key] { - job = existing - storage[key] = nil - } else { - job = nil - } - return eventLoop.delegate(for: self.eventLoopGroup) - .makeSucceededFuture(job) + let context: JobContext + + func get(_ id: JobIdentifier) -> EventLoopFuture { + TestQueue.lock.lock() + defer { TestQueue.lock.unlock() } + return self.context.eventLoop.makeSucceededFuture(TestQueue.jobs[id]!) } - func set(key: String, job: JobStorage, eventLoop: JobsEventLoopPreference) -> EventLoopFuture { - lock.lock() - defer { lock.unlock() } - storage[key] = job - return eventLoop.delegate(for: self.eventLoopGroup) - .makeSucceededFuture(()) + func set(_ id: JobIdentifier, to data: JobData) -> EventLoopFuture { + TestQueue.lock.lock() + defer { TestQueue.lock.unlock() } + TestQueue.jobs[id] = data + return self.context.eventLoop.makeSucceededFuture(()) } - func completed(key: String, job: JobStorage, eventLoop: JobsEventLoopPreference) -> EventLoopFuture { - return eventLoop.delegate(for: self.eventLoopGroup) - .makeSucceededFuture(()) + func clear(_ id: JobIdentifier) -> EventLoopFuture { + TestQueue.lock.lock() + defer { TestQueue.lock.unlock() } + TestQueue.jobs[id] = nil + return self.context.eventLoop.makeSucceededFuture(()) } - func processingKey(key: String) -> String { - return key + func pop() -> EventLoopFuture { + TestQueue.lock.lock() + defer { TestQueue.lock.unlock() } + return self.context.eventLoop.makeSucceededFuture(TestQueue.queue.popLast()) } - func requeue(key: String, job: JobStorage, eventLoop: JobsEventLoopPreference) -> EventLoopFuture { - return eventLoop.delegate(for: self.eventLoopGroup) - .makeSucceededFuture(()) + func push(_ id: JobIdentifier) -> EventLoopFuture { + TestQueue.lock.lock() + defer { TestQueue.lock.unlock() } + TestQueue.queue.append(id) + return self.context.eventLoop.makeSucceededFuture(()) } } -struct FooJob: Job { - static var dequeuePromise: EventLoopPromise? +struct Foo: Job { + let promise: EventLoopPromise struct Data: Codable { var foo: String } func dequeue(_ context: JobContext, _ data: Data) -> EventLoopFuture { - Self.dequeuePromise!.succeed(()) + self.promise.succeed(data.foo) return context.eventLoop.makeSucceededFuture(()) } func error(_ context: JobContext, _ error: Error, _ data: Data) -> EventLoopFuture { - Self.dequeuePromise!.fail(error) + self.promise.fail(error) return context.eventLoop.makeSucceededFuture(()) } } diff --git a/Tests/JobsTests/JobsWorkerTests.swift b/Tests/JobsTests/JobsWorkerTests.swift deleted file mode 100644 index 8dc3bbf..0000000 --- a/Tests/JobsTests/JobsWorkerTests.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// JobsWorkerTests.swift -// Jobs -// -// Created by Jimmy McDermott on 2019-08-05. -// - -import XCTest -import NIO -import Logging -@testable import Jobs - -final class JobsWorkerTests: XCTestCase { - - func testScheduledJob() throws { - let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) - let expectation = XCTestExpectation(description: "Waits for scheduled job to be completed") - var config = JobsConfiguration() - - guard let second = Date().second() else { XCTFail("Can't get date second"); return } - - config.schedule(DailyCleanupScheduledJob(expectation: expectation)) - .everyMinute() - .at(.init(second + 2)) - - let logger = Logger(label: "com.vapor.codes.jobs.tests") - let worker = ScheduledJobsWorker( - configuration: config, - logger: logger, - on: elg.next() - ) - try worker.start() - - XCTAssertEqual(worker.scheduledJobs.count, 1) - wait(for: [expectation], timeout: 3) - - try elg.next().scheduleTask(in: .seconds(1)) { () -> Void in - // Test that job was rescheduled - XCTAssertEqual(worker.scheduledJobs.count, 2) - worker.shutdown() - }.futureResult.wait() - } - - func testScheduledJobAt() throws { - let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) - let expectation = XCTestExpectation(description: "Waits for scheduled job to be completed") - var config = JobsConfiguration() - - config.schedule(DailyCleanupScheduledJob(expectation: expectation)).at(Date().addingTimeInterval(5)) - - let logger = Logger(label: "com.vapor.codes.jobs.tests") - let worker = ScheduledJobsWorker( - configuration: config, - logger: logger, - on: elg.next() - ) - try worker.start() - - XCTAssertEqual(worker.scheduledJobs.count, 1) - wait(for: [expectation], timeout: 6) - - try elg.next().scheduleTask(in: .seconds(1)) { () -> Void in - // Test that job was not rescheduled - XCTAssertEqual(worker.scheduledJobs.count, 0) - worker.shutdown() - }.futureResult.wait() - } -} - -struct DailyCleanupScheduledJob: ScheduledJob { - let expectation: XCTestExpectation - - func run(context: JobContext) -> EventLoopFuture { - expectation.fulfill() - return context.eventLoop.makeSucceededFuture(()) - } -} diff --git a/Tests/JobsTests/Mocks/JobMock.swift b/Tests/JobsTests/Mocks/JobMock.swift deleted file mode 100644 index 8d70cae..0000000 --- a/Tests/JobsTests/Mocks/JobMock.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// JobMock.swift -// Async -// -// Created by Raul Riera on 2019-03-16. -// - -import Jobs -import NIO - -struct JobDataMock: Codable {} -struct JobDataOtherMock: Codable {} - -struct JobMock: Job { - func dequeue(_ context: JobContext, _ data: T) -> EventLoopFuture { - return context.eventLoop.makeSucceededFuture(()) - } - - func error(_ context: JobContext, _ error: Error, _ data: T) -> EventLoopFuture { - return context.eventLoop.makeSucceededFuture(()) - } -} diff --git a/Tests/JobsTests/QueueNameTests.swift b/Tests/JobsTests/QueueNameTests.swift deleted file mode 100644 index 8d62900..0000000 --- a/Tests/JobsTests/QueueNameTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// QueueNameTests.swift -// Async -// -// Created by Raul Riera on 2019-03-16. -// - -import XCTest -@testable import Jobs - -final class QueueNameTests: XCTestCase { - func testKeyIsGeneratedCorrectly() { - let key = JobsQueue(name: "vapor").makeKey(with: "jobs") - XCTAssertEqual(key, "jobs[vapor]") - } -} diff --git a/Tests/JobsTests/ScheduleBuilderTests.swift b/Tests/JobsTests/ScheduleBuilderTests.swift new file mode 100644 index 0000000..e11b552 --- /dev/null +++ b/Tests/JobsTests/ScheduleBuilderTests.swift @@ -0,0 +1,188 @@ +import Jobs +import XCTest + +final class ScheduleBuilderTests: XCTestCase { + func testHourlyBuilder() throws { + let builder = ScheduleBuilder() + builder.hourly().at(30) + // same time + XCTAssertEqual( + builder.nextDate(current: Date(hour: 5, minute: 30)), + // plus one hour + Date(hour: 6, minute: 30) + ) + // just before + XCTAssertEqual( + builder.nextDate(current: Date(hour: 5, minute: 29)), + // plus one minute + Date(hour: 5, minute: 30) + ) + // just after + XCTAssertEqual( + builder.nextDate(current: Date(hour: 5, minute: 31)), + // plus one hour + Date(hour: 6, minute: 30) + ) + } + + func testDailyBuilder() throws { + let builder = ScheduleBuilder() + builder.daily().at("5:23am") + // same time + XCTAssertEqual( + builder.nextDate(current: Date(day: 1, hour: 5, minute: 23)), + // plus one day + Date(day: 2, hour: 5, minute: 23) + ) + // just before + XCTAssertEqual( + builder.nextDate(current: Date(day: 1, hour: 5, minute: 22)), + // plus one minute + Date(day: 1, hour: 5, minute: 23) + ) + // just after + XCTAssertEqual( + builder.nextDate(current: Date(day: 1, hour: 5, minute: 24)), + // plus one day + Date(day: 2, hour: 5, minute: 23) + ) + } + + func testWeeklyBuilder() throws { + let builder = ScheduleBuilder() + builder.weekly().on(.monday).at(.noon) + // sunday before + XCTAssertEqual( + builder.nextDate(current: Date(year: 2019, month: 1, day: 6, hour: 5, minute: 23)), + // next day at noon + Date(year: 2019, month: 1, day: 7, hour: 12, minute: 00) + ) + // monday at 1pm + XCTAssertEqual( + builder.nextDate(current: Date(year: 2019, month: 1, day: 7, hour: 13, minute: 00)), + // next monday at noon + Date(year: 2019, month: 1, day: 14, hour: 12, minute: 00) + ) + // monday at 11:30am + XCTAssertEqual( + builder.nextDate(current: Date(year: 2019, month: 1, day: 7, hour: 11, minute: 30)), + // same day at noon + Date(year: 2019, month: 1, day: 7, hour: 12, minute: 00) + ) + } + + func testMonthlyBuilderFirstDay() throws { + let builder = ScheduleBuilder() + builder.monthly().on(.first).at(.noon) + // middle of jan + XCTAssertEqual( + builder.nextDate(current: Date(year: 2019, month: 1, day: 15, hour: 5, minute: 23)), + // first of feb + Date(year: 2019, month: 2, day: 1, hour: 12, minute: 00) + ) + // just before + XCTAssertEqual( + builder.nextDate(current: Date(year: 2019, month: 2, day: 1, hour: 11, minute: 30)), + // first of feb + Date(year: 2019, month: 2, day: 1, hour: 12, minute: 00) + ) + // just after + XCTAssertEqual( + builder.nextDate(current: Date(year: 2019, month: 2, day: 1, hour: 12, minute: 30)), + // first of feb + Date(year: 2019, month: 3, day: 1, hour: 12, minute: 00) + ) + } + + func testMonthlyBuilder15th() throws { + let builder = ScheduleBuilder() + builder.monthly().on(15).at(.noon) + // just before + XCTAssertEqual( + builder.nextDate(current: Date(year: 2019, month: 2, day: 15, hour: 11, minute: 30)), + // first of feb + Date(year: 2019, month: 2, day: 15, hour: 12, minute: 00) + ) + // just after + XCTAssertEqual( + builder.nextDate(current: Date(year: 2019, month: 2, day: 15, hour: 12, minute: 30)), + // first of feb + Date(year: 2019, month: 3, day: 15, hour: 12, minute: 00) + ) + } + + func testYearlyBuilder() throws { + let builder = ScheduleBuilder() + builder.yearly().in(.may).on(23).at("2:58pm") + // early in the year + XCTAssertEqual( + builder.nextDate(current: Date(year: 2019, month: 1, day: 15, hour: 5, minute: 23)), + // 2019 + Date(year: 2019, month: 5, day: 23, hour: 14, minute: 58) + ) + // just before + XCTAssertEqual( + builder.nextDate(current: Date(year: 2019, month: 5, day: 23, hour: 14, minute: 57)), + // one minute later + Date(year: 2019, month: 5, day: 23, hour: 14, minute: 58) + ) + // just after + XCTAssertEqual( + builder.nextDate(current: Date(year: 2019, month: 5, day: 23, hour: 14, minute: 59)), + // one year later + Date(year: 2020, month: 5, day: 23, hour: 14, minute: 58) + ) + } +} + + + +final class Cleanup: ScheduledJob { + func run(context: JobContext) -> EventLoopFuture { + return context.eventLoop.makeSucceededFuture(()) + } +} + +extension Date { + var year: Int { + Calendar.current.component(.year, from: self) + } + + var month: Int { + Calendar.current.component(.month, from: self) + } + + var weekday: Int { + Calendar.current.component(.weekday, from: self) + } + + var day: Int { + Calendar.current.component(.day, from: self) + } + + var hour: Int { + Calendar.current.component(.hour, from: self) + } + + var minute: Int { + Calendar.current.component(.minute, from: self) + } + + var second: Int { + Calendar.current.component(.second, from: self) + } + + init( + year: Int = 2020, + month: Int = 1, + day: Int = 1, + hour: Int = 0, + minute: Int = 0, + second: Int = 0 + ) { + self = DateComponents( + calendar: .current, + year: year, month: month, day: day, hour: hour, minute: minute, second: second + ).date! + } +} diff --git a/Tests/JobsTests/Scheduler/DateComponentRetrievalTests.swift b/Tests/JobsTests/Scheduler/DateComponentRetrievalTests.swift deleted file mode 100755 index 79c527c..0000000 --- a/Tests/JobsTests/Scheduler/DateComponentRetrievalTests.swift +++ /dev/null @@ -1,172 +0,0 @@ -import XCTest -@testable import Jobs - -final class DateComponentRetrievalTests: XCTestCase { - - func testCalendarIdentifier() { - let gregorianCalendar = Calendar.init(identifier: .gregorian) - let iso8601Calendar = Calendar.init(identifier: .gregorian) - - XCTAssertEqual(gregorianCalendar.identifier, Calendar.current.identifier) - XCTAssertEqual(iso8601Calendar.identifier, Calendar.current.identifier) - } - - func testDateComponentRetrival() { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - // Sat, Feb 16, 2019 - let feb162019 = dateFormatter.date(from: "2019-02-16T14:42:20")! - XCTAssertEqual(2019, feb162019.year()) - XCTAssertEqual(2, feb162019.month()) - XCTAssertEqual(16, feb162019.dayOfMonth()) - XCTAssertEqual(14, feb162019.hour()) - XCTAssertEqual(42, feb162019.minute()) - XCTAssertEqual(20, feb162019.second()) - - // more advanced date components - XCTAssertEqual(1, feb162019.quarter()) - XCTAssertEqual(7, feb162019.weekOfYear()) - XCTAssertEqual(3, feb162019.weekOfMonth()) - XCTAssertEqual(7, feb162019.dayOfWeek()) - } - - func testDayOfWeek() { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - // Sat, Feb 16, 2019 - let feb162019 = dateFormatter.date(from: "2019-02-16T14:42:20")! - XCTAssertEqual(7, feb162019.dayOfWeek()) - - // Sun, Feb 17, 2019 - let feb172019 = dateFormatter.date(from: "2019-02-17T14:42:20")! - XCTAssertEqual(1, feb172019.dayOfWeek()) - } - - func testWeekOfMonth() { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - // Fri, Feb 1, 2019 - let feb012019 = dateFormatter.date(from: "2019-02-01T00:00:00")! - XCTAssertEqual(1, feb012019.weekOfMonth()) - - // Sun, Feb 3, 2019 - let feb032019 = dateFormatter.date(from: "2019-02-03T00:00:00")! - XCTAssertEqual(2, feb032019.weekOfMonth()) - - // Mon, Feb 4, 2019 - let feb042019 = dateFormatter.date(from: "2019-02-04T00:00:00")! - XCTAssertEqual(2, feb042019.weekOfMonth()) - - // Sat, Feb 23, 2019 - let feb232019 = dateFormatter.date(from: "2019-02-23T00:00:00")! - XCTAssertEqual(4, feb232019.weekOfMonth()) - - // Sun, Feb 24, 2019 - let feb242019 = dateFormatter.date(from: "2019-02-24T00:00:00")! - XCTAssertEqual(5, feb242019.weekOfMonth()) - - // Mon, Feb 25, 2019 - let feb252019 = dateFormatter.date(from: "2019-02-25T00:00:00")! - XCTAssertEqual(5, feb252019.weekOfMonth()) - } - - func testWeekOfYear() { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - // Tue, Jan 1, 2019 - let jan012019 = dateFormatter.date(from: "2019-01-01T00:00:00")! - XCTAssertEqual(1, jan012019.weekOfYear()) - - // Fri, Dec 27, 2019 - let dec272019 = dateFormatter.date(from: "2019-12-27T00:00:00")! - XCTAssertEqual(52, dec272019.weekOfYear()) - - /// Must be careful because even though it Dec 31, 2019 is in the last week of the year - /// this is because 2019 has 52 weeks + <7 days - /// weekOfYear returns 1 - /// year returns 2019 - /// yeyearForWeekOfYear returns 2020 - - // Tue, Dec 31, 2019 - let dec312019 = dateFormatter.date(from: "2019-12-31T00:00:00")! - XCTAssertEqual(1, dec312019.weekOfYear()) - XCTAssertEqual(2019, dec312019.year()) - XCTAssertEqual(2020, dec312019.yearForWeekOfYear()) - XCTAssertEqual(52, dec312019.weeksInYear()) - - // Fri, Dec 25, 2020 - let dec252020 = dateFormatter.date(from: "2020-12-25T00:00:00")! - XCTAssertEqual(52, dec252020.weekOfYear()) - - // Tue, Dec 31, 2020 - let dec312020 = dateFormatter.date(from: "2020-12-31T00:00:00")! - XCTAssertEqual(1, dec312020.weekOfYear()) - XCTAssertEqual(2020, dec312020.year()) - XCTAssertEqual(2021, dec312020.yearForWeekOfYear()) - XCTAssertEqual(53, dec312020.weeksInYear()) - } - - func testWeeksInYear() { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - let jan012019 = dateFormatter.date(from: "2019-02-01T00:00:00")! - XCTAssertEqual(52, jan012019.weeksInYear()) - - let jan012020 = dateFormatter.date(from: "2020-02-01T00:00:00")! - XCTAssertEqual(53, jan012020.weeksInYear()) - } - - func testQuarters() { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd" - - // jan 1 2019 - let jan012019 = dateFormatter.date(from: "2019-01-01")! - XCTAssertEqual(1, jan012019.quarter()) - - // apr 1 2019 - let apr012019 = dateFormatter.date(from: "2019-04-01")! - XCTAssertEqual(2, apr012019.quarter()) - - // jul 1 2019 - let jul012019 = dateFormatter.date(from: "2019-07-01")! - XCTAssertEqual(3, jul012019.quarter()) - - // oct 1 2019 - let oct012019 = dateFormatter.date(from: "2019-10-01")! - XCTAssertEqual(4, oct012019.quarter()) - } - - func testTimeZone() { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - guard let timeZoneEST = TimeZone.init(abbreviation: "EST") else { - XCTFail() - return - } - - guard let timeZoneUTC = TimeZone.init(abbreviation: "UTC") else { - XCTFail() - return - } - - // jan 20 2019 EST - dateFormatter.timeZone = timeZoneEST - let jan202019EST = dateFormatter.date(from: "2019-01-20T13:00:00")! - XCTAssertEqual(13, jan202019EST.hour(atTimeZone: timeZoneEST)) - XCTAssertEqual(18, jan202019EST.hour(atTimeZone: timeZoneUTC)) - - // jan 20 2019 UTC (5 hours ahead of EST) - dateFormatter.timeZone = timeZoneUTC - let jan202019UTC = dateFormatter.date(from: "2019-01-20T13:00:00")! - XCTAssertEqual(13, jan202019UTC.hour(atTimeZone: timeZoneUTC)) - XCTAssertEqual(8, jan202019UTC.hour(atTimeZone: timeZoneEST)) - } - -} diff --git a/Tests/JobsTests/Scheduler/RecurrenceRuleConstraintTests.swift b/Tests/JobsTests/Scheduler/RecurrenceRuleConstraintTests.swift deleted file mode 100755 index bf9e487..0000000 --- a/Tests/JobsTests/Scheduler/RecurrenceRuleConstraintTests.swift +++ /dev/null @@ -1,188 +0,0 @@ -import XCTest -import NIO -@testable import Jobs - -final class RecurrenceRuleConstraintTests: XCTestCase { - - func testRecurrenceRuleConstraintCreationSetSingleValue() throws { - // second (0-59) - XCTAssertThrowsError(try SecondRecurrenceRuleConstraint.atSecond(-1)) - XCTAssertNoThrow(try SecondRecurrenceRuleConstraint.atSecond(0)) - XCTAssertNoThrow(try SecondRecurrenceRuleConstraint.atSecond(59)) - XCTAssertThrowsError(try SecondRecurrenceRuleConstraint.atSecond(60)) - - // minute (0-59) - XCTAssertThrowsError(try MinuteRecurrenceRuleConstraint.atMinute(-1)) - XCTAssertNoThrow(try MinuteRecurrenceRuleConstraint.atMinute(0)) - XCTAssertNoThrow(try MinuteRecurrenceRuleConstraint.atMinute(59)) - XCTAssertThrowsError(try MinuteRecurrenceRuleConstraint.atMinute(60)) - - // hour (0-23) - XCTAssertThrowsError(try HourRecurrenceRuleConstraint.atHour(-1)) - XCTAssertNoThrow(try HourRecurrenceRuleConstraint.atHour(0)) - XCTAssertNoThrow(try HourRecurrenceRuleConstraint.atHour(23)) - XCTAssertThrowsError(try HourRecurrenceRuleConstraint.atHour(24)) - - // dayOfWeek (1-7) ex: 1 sunday, 7 saturday - XCTAssertThrowsError(try DayOfWeekRecurrenceRuleConstraint.atDayOfWeek(0)) - XCTAssertNoThrow(try DayOfWeekRecurrenceRuleConstraint.atDayOfWeek(1)) - XCTAssertNoThrow(try DayOfWeekRecurrenceRuleConstraint.atDayOfWeek(7)) - XCTAssertThrowsError(try DayOfWeekRecurrenceRuleConstraint.atDayOfWeek(8)) - - // dayOfMonth (1-31) ex: 1 is the 1st of month, 31 is the 31st of month - XCTAssertThrowsError(try DayOfMonthRecurrenceRuleConstraint.atDayOfMonth(0)) - XCTAssertNoThrow(try DayOfMonthRecurrenceRuleConstraint.atDayOfMonth(1)) - XCTAssertNoThrow(try DayOfMonthRecurrenceRuleConstraint.atDayOfMonth(31)) - XCTAssertThrowsError(try DayOfMonthRecurrenceRuleConstraint.atDayOfMonth(32)) - - // month (1-12) ex: 1 is January, 12 is December - XCTAssertThrowsError(try MonthRecurrenceRuleConstraint.atMonth(0)) - XCTAssertNoThrow(try MonthRecurrenceRuleConstraint.atMonth(1)) - XCTAssertNoThrow(try MonthRecurrenceRuleConstraint.atMonth(12)) - XCTAssertThrowsError(try MonthRecurrenceRuleConstraint.atMonth(13)) - - // quarter (1-4) - XCTAssertThrowsError(try QuarterRecurrenceRuleConstraint.atQuarter(0)) - XCTAssertNoThrow(try QuarterRecurrenceRuleConstraint.atQuarter(1)) - XCTAssertNoThrow(try QuarterRecurrenceRuleConstraint.atQuarter(4)) - XCTAssertThrowsError(try QuarterRecurrenceRuleConstraint.atQuarter(5)) - - // year (1970-3000) - // XCTAssertThrowsError(try reccurrenceRule.atYear(1969)) - XCTAssertNoThrow(try YearRecurrenceRuleConstraint.atYear(1970)) - XCTAssertNoThrow(try YearRecurrenceRuleConstraint.atYear(3000)) - // XCTAssertThrowsError(try reccurrenceRule.atYear(3001)) - } - - func testRecurrenceRuleConstraintCreationSetMultipleValues() throws { - // second (0-59) - XCTAssertThrowsError(try SecondRecurrenceRuleConstraint.atSeconds([0, 59, -1])) - XCTAssertNoThrow(try SecondRecurrenceRuleConstraint.atSeconds([0, 59])) - XCTAssertThrowsError(try SecondRecurrenceRuleConstraint.atSeconds([0, 59, 60])) - - // minute (0-59) - XCTAssertThrowsError(try MinuteRecurrenceRuleConstraint.atMinutes([0, 59, -1])) - XCTAssertNoThrow(try MinuteRecurrenceRuleConstraint.atMinutes([0, 59])) - XCTAssertThrowsError(try MinuteRecurrenceRuleConstraint.atMinutes([0, 59, 60])) - - // hour (0-23) - XCTAssertThrowsError(try HourRecurrenceRuleConstraint.atHours([0, 23, -1])) - XCTAssertNoThrow(try HourRecurrenceRuleConstraint.atHours([0, 23])) - XCTAssertThrowsError(try HourRecurrenceRuleConstraint.atHours([0, 23, 24])) - - // dayOfWeek (1-7) ex: 1 sunday, 7 saturday - XCTAssertThrowsError(try DayOfWeekRecurrenceRuleConstraint.atDaysOfWeek([1, 7, 0])) - XCTAssertNoThrow(try DayOfWeekRecurrenceRuleConstraint.atDaysOfWeek([1, 7])) - XCTAssertThrowsError(try DayOfWeekRecurrenceRuleConstraint.atDaysOfWeek([1, 7, 8])) - - // dayOfMonth (1-31) ex: 1 is the 1st of month, 31 is the 31st of month - XCTAssertThrowsError(try DayOfMonthRecurrenceRuleConstraint.atDaysOfMonth([1, 31, 0])) - XCTAssertNoThrow(try DayOfMonthRecurrenceRuleConstraint.atDaysOfMonth([1, 31])) - XCTAssertThrowsError(try DayOfMonthRecurrenceRuleConstraint.atDaysOfMonth([1, 31, 32])) - - // month (1-12) ex: 1 is January, 12 is December - XCTAssertThrowsError(try MonthRecurrenceRuleConstraint.atMonths([1, 12, 0])) - XCTAssertNoThrow(try MonthRecurrenceRuleConstraint.atMonths([1, 12])) - XCTAssertThrowsError(try MonthRecurrenceRuleConstraint.atMonths([1, 12, 13])) - - // quarter (1-4) - XCTAssertThrowsError(try QuarterRecurrenceRuleConstraint.atQuarters([1, 4, 0])) - XCTAssertNoThrow(try QuarterRecurrenceRuleConstraint.atQuarters([1, 4])) - XCTAssertThrowsError(try QuarterRecurrenceRuleConstraint.atQuarters([1, 4, 5])) - - // year (1970-3000) - XCTAssertNoThrow(try YearRecurrenceRuleConstraint.atYears([1970, 2019, 3000])) - } - - func testRecurrenceRuleConstraintCreationRange() throws { - // second (0-59) - XCTAssertThrowsError(try SecondRecurrenceRuleConstraint.atSecondsInRange(lowerBound: -1, upperBound: 59)) - XCTAssertNoThrow(try SecondRecurrenceRuleConstraint.atSecondsInRange(lowerBound: 0, upperBound: 59)) - XCTAssertThrowsError(try SecondRecurrenceRuleConstraint.atSecondsInRange(lowerBound: 0, upperBound: 60)) - - // minute (0-59) - XCTAssertThrowsError(try MinuteRecurrenceRuleConstraint.atMinutesInRange(lowerBound: -1, upperBound: 59)) - XCTAssertNoThrow(try MinuteRecurrenceRuleConstraint.atMinutesInRange(lowerBound: 0, upperBound: 59)) - XCTAssertThrowsError(try MinuteRecurrenceRuleConstraint.atMinutesInRange(lowerBound: 0, upperBound: 60)) - - // hour (0-23) - XCTAssertThrowsError(try HourRecurrenceRuleConstraint.atHoursInRange(lowerBound: -1, upperBound: 23)) - XCTAssertNoThrow(try HourRecurrenceRuleConstraint.atHoursInRange(lowerBound: 0, upperBound: 23)) - XCTAssertThrowsError(try HourRecurrenceRuleConstraint.atHoursInRange(lowerBound: 0, upperBound: 24)) - - // dayOfWeek (1-7) ex: 1 sunday, 7 saturday - XCTAssertThrowsError(try DayOfWeekRecurrenceRuleConstraint.atDaysOfWeekInRange(lowerBound: 0, upperBound: 7)) - XCTAssertNoThrow(try DayOfWeekRecurrenceRuleConstraint.atDaysOfWeekInRange(lowerBound: 1, upperBound: 7)) - XCTAssertThrowsError(try DayOfWeekRecurrenceRuleConstraint.atDaysOfWeekInRange(lowerBound: 1, upperBound: 8)) - - // dayOfMonth (1-31) ex: 1 is the 1st of month, 31 is the 31st of month - XCTAssertThrowsError(try DayOfMonthRecurrenceRuleConstraint.atDaysOfMonthInRange(lowerBound: 0, upperBound: 31)) - XCTAssertNoThrow(try DayOfMonthRecurrenceRuleConstraint.atDaysOfMonthInRange(lowerBound: 1, upperBound: 31)) - XCTAssertThrowsError(try DayOfMonthRecurrenceRuleConstraint.atDaysOfMonthInRange(lowerBound: 1, upperBound: 32)) - - // month (1-12) ex: 1 is January, 12 is December - XCTAssertThrowsError(try MonthRecurrenceRuleConstraint.atMonthsInRange(lowerBound: 0, upperBound: 12)) - XCTAssertNoThrow(try MonthRecurrenceRuleConstraint.atMonthsInRange(lowerBound: 1, upperBound: 12)) - XCTAssertThrowsError(try MonthRecurrenceRuleConstraint.atMonthsInRange(lowerBound: 1, upperBound: 13)) - - // quarter (1-4) - XCTAssertThrowsError(try QuarterRecurrenceRuleConstraint.atQuartersInRange(lowerBound: 0, upperBound: 4)) - XCTAssertNoThrow(try QuarterRecurrenceRuleConstraint.atQuartersInRange(lowerBound: 1, upperBound: 4)) - XCTAssertThrowsError(try QuarterRecurrenceRuleConstraint.atQuartersInRange(lowerBound: 1, upperBound: 5)) - - // year (1970-3000) - XCTAssertThrowsError(try YearRecurrenceRuleConstraint.atYearsInRange(lowerBound: 1969, upperBound: 3000)) - XCTAssertNoThrow(try YearRecurrenceRuleConstraint.atYearsInRange(lowerBound: 1970, upperBound: 3000)) - //XCTAssertThrowsError(try reccurrenceRule.atYearsInRange(lowerBound: 1970, upperBound: 3001)) - } - - func testRecurrenceRuleConstraintCreationStep() throws { - // second (0-59) - XCTAssertThrowsError(try SecondRecurrenceRuleConstraint.secondStep(0)) - XCTAssertNoThrow(try SecondRecurrenceRuleConstraint.secondStep(1)) - XCTAssertNoThrow(try SecondRecurrenceRuleConstraint.secondStep(59)) - XCTAssertThrowsError(try SecondRecurrenceRuleConstraint.secondStep(60)) - - // minute (0-59) - XCTAssertThrowsError(try MinuteRecurrenceRuleConstraint.minuteStep(0)) - XCTAssertNoThrow(try MinuteRecurrenceRuleConstraint.minuteStep(1)) - XCTAssertNoThrow(try MinuteRecurrenceRuleConstraint.minuteStep(59)) - XCTAssertThrowsError(try MinuteRecurrenceRuleConstraint.minuteStep(60)) - - // hour (0-23) - XCTAssertThrowsError(try HourRecurrenceRuleConstraint.hourStep(0)) - XCTAssertNoThrow(try HourRecurrenceRuleConstraint.hourStep(1)) - XCTAssertNoThrow(try HourRecurrenceRuleConstraint.hourStep(23)) - XCTAssertThrowsError(try HourRecurrenceRuleConstraint.hourStep(24)) - - // dayOfWeek (1-7) ex: 1 sunday, 7 saturday - XCTAssertThrowsError(try DayOfWeekRecurrenceRuleConstraint.dayOfWeekStep(0)) - XCTAssertNoThrow(try DayOfWeekRecurrenceRuleConstraint.dayOfWeekStep(1)) - XCTAssertNoThrow(try DayOfWeekRecurrenceRuleConstraint.dayOfWeekStep(7)) - XCTAssertThrowsError(try DayOfWeekRecurrenceRuleConstraint.dayOfWeekStep(8)) - - // dayOfMonth (1-31) ex: 1 is the 1st of month, 31 is the 31st of month - XCTAssertThrowsError(try DayOfMonthRecurrenceRuleConstraint.dayOfMonthStep(0)) - XCTAssertNoThrow(try DayOfMonthRecurrenceRuleConstraint.dayOfMonthStep(1)) - XCTAssertNoThrow(try DayOfMonthRecurrenceRuleConstraint.dayOfMonthStep(31)) - XCTAssertThrowsError(try DayOfMonthRecurrenceRuleConstraint.dayOfMonthStep(32)) - - // month (1-12) ex: 1 is January, 12 is December - XCTAssertThrowsError(try MonthRecurrenceRuleConstraint.monthStep(0)) - XCTAssertNoThrow(try MonthRecurrenceRuleConstraint.monthStep(1)) - XCTAssertNoThrow(try MonthRecurrenceRuleConstraint.monthStep(12)) - XCTAssertThrowsError(try MonthRecurrenceRuleConstraint.monthStep(53)) - - // quarter (1-4) - XCTAssertThrowsError(try QuarterRecurrenceRuleConstraint.quarterStep(0)) - XCTAssertNoThrow(try QuarterRecurrenceRuleConstraint.quarterStep(1)) - XCTAssertNoThrow(try QuarterRecurrenceRuleConstraint.quarterStep(4)) - XCTAssertThrowsError(try QuarterRecurrenceRuleConstraint.quarterStep(5)) - - // year (1970-3000) - XCTAssertThrowsError(try YearRecurrenceRuleConstraint.yearStep(0)) - XCTAssertNoThrow(try YearRecurrenceRuleConstraint.yearStep(2)) - XCTAssertNoThrow(try YearRecurrenceRuleConstraint.yearStep(1000)) - // XCTAssertThrowsError(try reccurrenceRule.atYear(3001)) - } -} diff --git a/Tests/JobsTests/Scheduler/RecurrenceRuleTests.swift b/Tests/JobsTests/Scheduler/RecurrenceRuleTests.swift deleted file mode 100755 index 2d2504f..0000000 --- a/Tests/JobsTests/Scheduler/RecurrenceRuleTests.swift +++ /dev/null @@ -1,258 +0,0 @@ -import XCTest -import NIO -@testable import Jobs - -final class RecurrenceRuleTests: XCTestCase { - - func testReccurrenceRuleEvaluationSimple() throws { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - var reccurrenceRule = RecurrenceRule() - reccurrenceRule.setMonthConstraint(try .atMonth(2)) - reccurrenceRule.setHourConstraint(try .atHour(3)) - - // Fri, Feb 1, 2019 03:00:00 - let date1 = dateFormatter.date(from: "2019-02-01T03:00:00")! - XCTAssertEqual(true, try reccurrenceRule.evaluate(date: date1)) - - // Fri, Feb 1, 2019 03:00:01 - let date2 = dateFormatter.date(from: "2019-02-01T03:00:01")! - XCTAssertEqual(false, try reccurrenceRule.evaluate(date: date2)) - - // Fri, Feb 1, 2019 04:00:00 - let date3 = dateFormatter.date(from: "2019-02-01T04:00:00")! - XCTAssertEqual(false, try reccurrenceRule.evaluate(date: date3)) - } - - func testReccurrenceRuleEvaluationStepSimple() throws { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - var reccurrenceRule = RecurrenceRule() - reccurrenceRule.setMonthConstraint(try .atMonth(2)) - reccurrenceRule.setMinuteConstraint(try .minuteStep(15)) - - // Fri, Feb 1, 2019 03:00:00 - let date1 = dateFormatter.date(from: "2019-02-01T03:00:00")! - XCTAssertEqual(true, try reccurrenceRule.evaluate(date: date1)) - - // Fri, Feb 1, 2019 03:15:00 - let date2 = dateFormatter.date(from: "2019-02-01T03:15:00")! - XCTAssertEqual(true, try reccurrenceRule.evaluate(date: date2)) - - // Fri, Feb 1, 2019 03:30:00 - let date3 = dateFormatter.date(from: "2019-02-01T03:30:00")! - XCTAssertEqual(true, try reccurrenceRule.evaluate(date: date3)) - - // Fri, Feb 1, 2019 03:45:00 - let date4 = dateFormatter.date(from: "2019-02-01T03:45:00")! - XCTAssertEqual(true, try reccurrenceRule.evaluate(date: date4)) - - // Fri, Feb 1, 2019 04:45:00 - let date5 = dateFormatter.date(from: "2019-02-01T04:00:00")! - XCTAssertEqual(true, try reccurrenceRule.evaluate(date: date5)) - } - - func testReccurrenceRuleEvaluationStepNotDivisible() throws { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - var reccurrenceRule = RecurrenceRule() - reccurrenceRule.setMonthConstraint(try .atMonth(2)) - reccurrenceRule.setMinuteConstraint(try .minuteStep(22)) - - // Fri, Feb 1, 2019 03:00:00 - let date1 = dateFormatter.date(from: "2019-02-01T03:00:00")! - XCTAssertEqual(true, try reccurrenceRule.evaluate(date: date1)) - - // Fri, Feb 1, 2019 03:22:00 - let date2 = dateFormatter.date(from: "2019-02-01T03:22:00")! - XCTAssertEqual(true, try reccurrenceRule.evaluate(date: date2)) - - // Fri, Feb 1, 2019 03:44:00 - let date3 = dateFormatter.date(from: "2019-02-01T03:44:00")! - XCTAssertEqual(true, try reccurrenceRule.evaluate(date: date3)) - - // Fri, Feb 1, 2019 04:00:00 - let date4 = dateFormatter.date(from: "2019-02-01T04:00:00")! - XCTAssertEqual(true, try reccurrenceRule.evaluate(date: date4)) - - // Fri, Feb 1, 2019 04:04:00 - // should evaluate to false beacuse the minute step constraint is multiples within hour - // ex step constraint of 22 minutes [3:00, 3:22, 3:44, 4:00, 4:22, ...] - let date5 = dateFormatter.date(from: "2019-02-01T04:06:00")! - XCTAssertEqual(false, try reccurrenceRule.evaluate(date: date5)) - - } - - func testReccurrenceRuleEvaluationTimezone() throws { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - guard let timeZoneEST = TimeZone.init(abbreviation: "EST") else { - XCTFail() - return - } - - guard let timeZoneUTC = TimeZone.init(abbreviation: "UTC") else { - XCTFail() - return - } - - // Fri, Feb 1, 2019 03:00:00 EST - dateFormatter.timeZone = timeZoneEST - let dateEST = dateFormatter.date(from: "2019-02-15T22:00:30")! - - // test EST - //let reccurrenceRuleEST = try RecurrenceRule().atMonth(2).atDayOfMonth(15).atHour(22).atSecond(30).usingTimeZone(timeZoneEST) - var reccurrenceRuleEST = RecurrenceRule.init(timeZone: timeZoneEST) - reccurrenceRuleEST.setMonthConstraint(try .atMonth(2)) - reccurrenceRuleEST.setDayOfMonthConstraint(try .atDayOfMonth(15)) - reccurrenceRuleEST.setHourConstraint(try .atHour(22)) - reccurrenceRuleEST.setSecondConstraint(try .atSecond(30)) - - XCTAssertEqual(true, try reccurrenceRuleEST.evaluate(date: dateEST)) - - // test UTC with EST date - var reccurrenceRuleUTC = RecurrenceRule.init(timeZone: timeZoneUTC) - reccurrenceRuleUTC.setMonthConstraint(try .atMonth(2)) - reccurrenceRuleUTC.setDayOfMonthConstraint(try .atDayOfMonth(15)) - reccurrenceRuleUTC.setHourConstraint(try .atHour(22)) - reccurrenceRuleUTC.setSecondConstraint(try .atSecond(30)) - XCTAssertEqual(false, try reccurrenceRuleUTC.evaluate(date: dateEST)) - - // test UTC with UTC date - // Fri, Feb 1, 2019 03:00:00 UTC - dateFormatter.timeZone = timeZoneUTC - let dateUTC = dateFormatter.date(from: "2019-02-15T22:00:30")! - XCTAssertEqual(true, try reccurrenceRuleUTC.evaluate(date: dateUTC)) - } - - func testNextDateWhereSimple() throws { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - // Fri, Feb 1, 2019 03:00:00 - let date1 = dateFormatter.date(from: "2019-02-01T03:00:00")! - /// should reset values less than month to their default values - let date2 = try date1.nextDate(where: .month, is: 4) - XCTAssertEqual(dateFormatter.date(from: "2019-04-01T00:00:00")!, date2) - } - - func testNextDateWhere() throws { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - // Fri, Feb 1, 2019 03:00:00 - let date1 = dateFormatter.date(from: "2019-02-01T03:00:00")! - /// should reset values less than month to their default values - let date2 = try date1.nextDate(where: .month, is: 4) - XCTAssertEqual(dateFormatter.date(from: "2019-04-01T00:00:00")!, date2) - - let date3 = try date2.nextDate(where: .dayOfMonth, is: 26) - /// should reset values less than dayOfMonth to their default values - XCTAssertEqual(dateFormatter.date(from: "2019-04-26T00:00:00")!, date3) - - let date4 = try date3.nextDate(where: .minute, is: 33) - /// should reset values less than dayOfMonth to their default values - XCTAssertEqual(dateFormatter.date(from: "2019-04-26T00:33:00")!, date4) - } - - func testResolveNextDateThatSatisfiesRule() throws { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - var reccurrenceRule = RecurrenceRule() - reccurrenceRule.setMonthConstraint(try .atMonth(4)) - reccurrenceRule.setDayOfMonthConstraint(try .atDayOfMonth(26)) - reccurrenceRule.setMinuteConstraint(try .minuteStep(33)) - - // Fri, Feb 1, 2019 03:00:00 - let date1 = dateFormatter.date(from: "2019-02-01T03:00:00")! - let date2 = try reccurrenceRule.resolveNextDateThatSatisfiesRule(currentDate: date1) - XCTAssertEqual(dateFormatter.date(from: "2019-04-26T00:00:00")!, date2) - - let date3 = try reccurrenceRule.resolveNextDateThatSatisfiesRule(currentDate: date2) - XCTAssertEqual(dateFormatter.date(from: "2019-04-26T00:33:00")!, date3) - } - - func testResolveNextDateThatSatisfiesRuleLeapYear() throws { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - var reccurrenceRule = RecurrenceRule() - reccurrenceRule.setMonthConstraint(try .atMonth(2)) - reccurrenceRule.setDayOfMonthConstraint(try .atDayOfMonth(29)) - reccurrenceRule.setMinuteConstraint(try .atMinute(25)) - reccurrenceRule.setSecondConstraint(try .atSecond(1)) - - // Fri, Feb 1, 2019 03:00:00 - let date1 = dateFormatter.date(from: "2019-02-01T03:00:00")! - let date2 = try reccurrenceRule.resolveNextDateThatSatisfiesRule(currentDate: date1) - XCTAssertEqual(dateFormatter.date(from: "2020-02-29T00:25:01")!, date2) - } - - func testResolveNextDateThatSatisfiesRuleImpossible() throws { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - // impossible as april never has 31 days - var reccurrenceRule = RecurrenceRule() - reccurrenceRule.setMonthConstraint(try .atMonth(2)) - reccurrenceRule.setDayOfMonthConstraint(try .atDayOfMonth(31)) - reccurrenceRule.setMinuteConstraint(try .atMinute(25)) - reccurrenceRule.setSecondConstraint(try .atSecond(1)) - - // Fri, Jan 1, 2019 00:00:00 - let date1 = dateFormatter.date(from: "2019-01-01T00:00:00")! - - XCTAssertThrowsError(try reccurrenceRule.resolveNextDateThatSatisfiesRule(currentDate: date1)) - } - - func testLastDayOfMonthSimple() throws { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - var reccurrenceRule = RecurrenceRule() - reccurrenceRule.setDayOfMonthConstraint(try .atLastDayOfMonth()) - - // Fri, Jan 1, 2019 00:00:00 - let date1 = dateFormatter.date(from: "2019-01-01T00:00:00")! - - // Thu Jan 31, 2019 18:30:00 - let date2 = dateFormatter.date(from: "2019-01-31T00:00:00")! - - XCTAssertEqual(try reccurrenceRule.resolveNextDateThatSatisfiesRule(currentDate: date1), date2) - - // feb 28, 2019 18:30:00 - let date3 = dateFormatter.date(from: "2019-02-28T00:00:00")! - - XCTAssertEqual(try reccurrenceRule.resolveNextDateThatSatisfiesRule(currentDate: date2), date3) - } - - func testLastDayOfMonth() throws { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - var reccurrenceRule = RecurrenceRule() - reccurrenceRule.setDayOfMonthConstraint(try .atLastDayOfMonth()) - reccurrenceRule.setHourConstraint(try .atHour(18)) - reccurrenceRule.setMinuteConstraint(try .atMinute(30)) - reccurrenceRule.setSecondConstraint(try .atSecond(0)) - - // Fri, Jan 1, 2019 00:00:00 - let date1 = dateFormatter.date(from: "2019-01-01T18:30:00")! - - // Thu Jan 31, 2019 18:30:00 - let date2 = dateFormatter.date(from: "2019-01-31T18:30:00")! - - XCTAssertEqual(try reccurrenceRule.resolveNextDateThatSatisfiesRule(currentDate: date1), date2) - - // feb 28, 2019 18:30:00 - let date3 = dateFormatter.date(from: "2019-02-28T18:30:00")! - - XCTAssertEqual(try reccurrenceRule.resolveNextDateThatSatisfiesRule(currentDate: date2), date3) - } - -} diff --git a/Tests/JobsTests/Scheduler/SchedulerTests.swift b/Tests/JobsTests/Scheduler/SchedulerTests.swift deleted file mode 100644 index 23854ff..0000000 --- a/Tests/JobsTests/Scheduler/SchedulerTests.swift +++ /dev/null @@ -1,208 +0,0 @@ -@testable import Jobs -import XCTest - -final class SchedulerTests: XCTestCase { - - func testConfiguration() throws { - var config = JobsConfiguration() - - // yearly - config.schedule(Cleanup()) - .yearly() - .in(.may) - .on(23) - .at(.noon) - - // monthly - config.schedule(Cleanup()) - .monthly() - .on(15) - .at(.midnight) - - // weekly - config.schedule(Cleanup()) - .weekly() - .on(.monday) - .at("3:13am") - - // daily - config.schedule(Cleanup()) - .daily() - .at("5:23pm") - - // daily 2 - config.schedule(Cleanup()) - .daily() - .at(5, 23, .pm) - - // daily 3 - config.schedule(Cleanup()) - .daily() - .at(17, 23) - - // hourly - config.schedule(Cleanup()) - .hourly() - .at(30) - - XCTAssertEqual(config.scheduledStorage.count, 7) - } - - func testScheduleEvaluationYearly() throws { - var config = JobsConfiguration() - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - // yearly - config.schedule(Cleanup()) - .yearly() - .in(.may) - .on(23) - .at(.noon) - - // Fri, Jan 1, 2019 00:00:00 - let date1 = dateFormatter.date(from: "2019-01-01T00:00:00")! - - // May 31, 2019 12:00:00 - let date2 = dateFormatter.date(from: "2019-05-23T12:00:00")! - - let nextDate = try config.scheduledStorage.first?.scheduler.resolveNextDateThatSatisifiesSchedule(date: date1) - XCTAssertEqual(nextDate, date2) - } - - func testScheduleEvaluationMonthly() throws { - var config = JobsConfiguration() - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - // monthly - config.schedule(Cleanup()) - .monthly() - .on(15) - .at(.midnight) - - // Fri, Jan 1, 2019 00:00:00 - let date1 = dateFormatter.date(from: "2019-01-01T00:00:00")! - - // Jan 15, 2019 24:00:00 - let date2 = dateFormatter.date(from: "2019-01-15T00:00:00")! - - let nextDate = try config.scheduledStorage.first?.scheduler.resolveNextDateThatSatisifiesSchedule(date: date1) - XCTAssertEqual(nextDate, date2) - } - - func testScheduleEvaluationWeekly() throws { - var config = JobsConfiguration() - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - // weekly - config.schedule(Cleanup()) - .weekly() - .on(.monday) - .at("3:13am") - - // Fri, Jan 1, 2019 00:00:00 - let date1 = dateFormatter.date(from: "2019-01-01T00:00:00")! - - // Mon Jan 7, 2019 3:13:00 - let date2 = dateFormatter.date(from: "2019-01-07T3:13:00")! - - let nextDate = try config.scheduledStorage.first?.scheduler.resolveNextDateThatSatisifiesSchedule(date: date1) - XCTAssertEqual(nextDate, date2) - } - - func testScheduleEvaluationDaily() throws { - var config = JobsConfiguration() - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - // daily 1 - config.schedule(Cleanup()) - .daily() - .at("5:23pm") - - // Fri, Jan 1, 2019 00:00:00 - let date1 = dateFormatter.date(from: "2019-01-01T00:00:00")! - - // Jan 1, 2019 17:23:00 - let date2 = dateFormatter.date(from: "2019-01-01T17:23:00")! - - let nextDate = try config.scheduledStorage.first?.scheduler.resolveNextDateThatSatisifiesSchedule(date: date1) - XCTAssertEqual(nextDate, date2) - - // daily 2 - config.schedule(Cleanup()) - .daily() - .at(5, 23, .pm) - - // Fri, Jan 1, 2019 00:00:00 - let date3 = dateFormatter.date(from: "2019-01-01T00:00:00")! - - // Jan 1, 2019 17:23:00 - let date4 = dateFormatter.date(from: "2019-01-01T17:23:00")! - - let nextDate2 = try config.scheduledStorage[1].scheduler.resolveNextDateThatSatisifiesSchedule(date: date3) - XCTAssertEqual(nextDate2, date4) - - // daily 3 - config.schedule(Cleanup()) - .daily() - .at(17, 23) - - // Fri, Jan 1, 2019 00:00:00 - let date5 = dateFormatter.date(from: "2019-01-01T00:00:00")! - - // Jan 1, 2019 17:23:00 - let date6 = dateFormatter.date(from: "2019-01-01T17:23:00")! - - let nextDate3 = try config.scheduledStorage[2].scheduler.resolveNextDateThatSatisifiesSchedule(date: date5) - XCTAssertEqual(nextDate3, date6) - } - - func testScheduleEvaluationHourly() throws { - var config = JobsConfiguration() - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - // hourly - config.schedule(Cleanup()) - .hourly() - .at(30) - - // Fri, Jan 1, 2019 00:00:00 - let date1 = dateFormatter.date(from: "2019-01-01T00:00:00")! - - // Jan 1, 2019 0:30:00 - let date2 = dateFormatter.date(from: "2019-01-01T0:30:00")! - - let nextDate = try config.scheduledStorage.first?.scheduler.resolveNextDateThatSatisifiesSchedule(date: date1) - XCTAssertEqual(nextDate, date2) - } - - func testScheduleEvaluationEveryMinute() throws { - var config = JobsConfiguration() - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - // hourly - config.schedule(Cleanup()) - .everyMinute() - .at(30) - - // Fri, Jan 1, 2019 00:00:00 - let date1 = dateFormatter.date(from: "2019-01-01T00:00:00")! - - // Jan 1, 2019 0:00:30 - let date2 = dateFormatter.date(from: "2019-01-01T0:00:30")! - - let nextDate = try config.scheduledStorage.first?.scheduler.resolveNextDateThatSatisifiesSchedule(date: date1) - XCTAssertEqual(nextDate, date2) - } -} - -final class Cleanup: ScheduledJob { - func run(context: JobContext) -> EventLoopFuture { - return context.eventLoop.makeSucceededFuture(()) - } -} diff --git a/Tests/JobsTests/XCTestManifests.swift b/Tests/JobsTests/XCTestManifests.swift deleted file mode 100644 index 940ce1f..0000000 --- a/Tests/JobsTests/XCTestManifests.swift +++ /dev/null @@ -1,129 +0,0 @@ -#if !canImport(ObjectiveC) -import XCTest - -extension DateComponentRetrievalTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__DateComponentRetrievalTests = [ - ("testCalendarIdentifier", testCalendarIdentifier), - ("testDateComponentRetrival", testDateComponentRetrival), - ("testDayOfWeek", testDayOfWeek), - ("testQuarters", testQuarters), - ("testTimeZone", testTimeZone), - ("testWeekOfMonth", testWeekOfMonth), - ("testWeekOfYear", testWeekOfYear), - ("testWeeksInYear", testWeeksInYear), - ] -} - -extension JobStorageTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__JobStorageTests = [ - ("testStringRepresentationIsValidJSON", testStringRepresentationIsValidJSON), - ] -} - -extension JobsConfigTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__JobsConfigTests = [ - ("testAddingAlreadyRegistratedJobsAreIgnored", testAddingAlreadyRegistratedJobsAreIgnored), - ("testAddingJobs", testAddingJobs), - ("testAddingJobsWithTheSameDataType", testAddingJobsWithTheSameDataType), - ("testScheduledJob", testScheduledJob), - ] -} - -extension JobsTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__JobsTests = [ - ("testVaporIntegration", testVaporIntegration), - ("testVaporScheduledJob", testVaporScheduledJob), - ] -} - -extension JobsWorkerTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__JobsWorkerTests = [ - ("testScheduledJob", testScheduledJob), - ("testScheduledJobAt", testScheduledJobAt), - ] -} - -extension QueueNameTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__QueueNameTests = [ - ("testKeyIsGeneratedCorrectly", testKeyIsGeneratedCorrectly), - ] -} - -extension RecurrenceRuleConstraintTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__RecurrenceRuleConstraintTests = [ - ("testRecurrenceRuleConstraintCreationRange", testRecurrenceRuleConstraintCreationRange), - ("testRecurrenceRuleConstraintCreationSetMultipleValues", testRecurrenceRuleConstraintCreationSetMultipleValues), - ("testRecurrenceRuleConstraintCreationSetSingleValue", testRecurrenceRuleConstraintCreationSetSingleValue), - ("testRecurrenceRuleConstraintCreationStep", testRecurrenceRuleConstraintCreationStep), - ] -} - -extension RecurrenceRuleTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__RecurrenceRuleTests = [ - ("testLastDayOfMonth", testLastDayOfMonth), - ("testLastDayOfMonthSimple", testLastDayOfMonthSimple), - ("testNextDateWhere", testNextDateWhere), - ("testNextDateWhereSimple", testNextDateWhereSimple), - ("testReccurrenceRuleEvaluationSimple", testReccurrenceRuleEvaluationSimple), - ("testReccurrenceRuleEvaluationStepNotDivisible", testReccurrenceRuleEvaluationStepNotDivisible), - ("testReccurrenceRuleEvaluationStepSimple", testReccurrenceRuleEvaluationStepSimple), - ("testReccurrenceRuleEvaluationTimezone", testReccurrenceRuleEvaluationTimezone), - ("testResolveNextDateThatSatisfiesRule", testResolveNextDateThatSatisfiesRule), - ("testResolveNextDateThatSatisfiesRuleImpossible", testResolveNextDateThatSatisfiesRuleImpossible), - ("testResolveNextDateThatSatisfiesRuleLeapYear", testResolveNextDateThatSatisfiesRuleLeapYear), - ] -} - -extension SchedulerTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__SchedulerTests = [ - ("testConfiguration", testConfiguration), - ("testScheduleEvaluationDaily", testScheduleEvaluationDaily), - ("testScheduleEvaluationEveryMinute", testScheduleEvaluationEveryMinute), - ("testScheduleEvaluationHourly", testScheduleEvaluationHourly), - ("testScheduleEvaluationMonthly", testScheduleEvaluationMonthly), - ("testScheduleEvaluationWeekly", testScheduleEvaluationWeekly), - ("testScheduleEvaluationYearly", testScheduleEvaluationYearly), - ] -} - -public func __allTests() -> [XCTestCaseEntry] { - return [ - testCase(DateComponentRetrievalTests.__allTests__DateComponentRetrievalTests), - testCase(JobStorageTests.__allTests__JobStorageTests), - testCase(JobsConfigTests.__allTests__JobsConfigTests), - testCase(JobsTests.__allTests__JobsTests), - testCase(JobsWorkerTests.__allTests__JobsWorkerTests), - testCase(QueueNameTests.__allTests__QueueNameTests), - testCase(RecurrenceRuleConstraintTests.__allTests__RecurrenceRuleConstraintTests), - testCase(RecurrenceRuleTests.__allTests__RecurrenceRuleTests), - testCase(SchedulerTests.__allTests__SchedulerTests), - ] -} -#endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 7b97f76..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,8 +0,0 @@ -import XCTest - -import JobsTests - -var tests = [XCTestCaseEntry]() -tests += JobsTests.__allTests() - -XCTMain(tests)