From 1d31be8cc386aefb82cf0b71054081c2fd8998ea Mon Sep 17 00:00:00 2001 From: starry-shivam Date: Wed, 26 Jun 2024 14:49:05 +0530 Subject: [PATCH] feat: Add shortcut methods for all inbuilt triggers Signed-off-by: starry-shivam --- README.md | 18 +- .../ktscheduler/scheduler/KtScheduler.kt | 216 ++++++++++++++++-- .../ktscheduler/triggers/CronTrigger.kt | 2 +- .../dev/starry/ktscheduler/KtSchedulerTest.kt | 52 ++++- 4 files changed, 264 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index c66ebaa..79d723f 100644 --- a/README.md +++ b/README.md @@ -77,14 +77,18 @@ val job = Job( // Callback function that will be executed when the job is triggered. callback = { println("OneTime Job executed at ${ZonedDateTime.now(timeZone)}") } ) - -// ..or like this -val job = Job(...) { println("Meow >~<") } - -// Add the job to the scheduler +// add the job to the scheduler scheduler.addJob(job) -// Start the scheduler +// ..or like this (shortcut/convinience method) +// This will create a job with a unique ID and add it to the scheduler +// See the documentation for more details on the parameters and other +// such convinience methods. +scheduler.runRepeating(intervalSeconds = 10) { + println("Meow >~<") // every 10 seconds +} + +// Finally, start the scheduler scheduler.start() // If you're running this as a standalone program, you need to block the current thread @@ -187,7 +191,7 @@ the issue you want to contribute to before starting to work on it. ### Supporting ❤️ If you found this library helpful, you can support me by giving a small tip -via [GitHub Sponsors](https://github.com/sponsors/starry-shivam) and/or joing the list +via [GitHub Sponsors](https://github.com/sponsors/starry-shivam) and/or joining the list of [stargazers](https://github.com/starry-shivam/KtScheduler/stargazers) by leaving a star! 🌟 ------ diff --git a/src/main/kotlin/dev/starry/ktscheduler/scheduler/KtScheduler.kt b/src/main/kotlin/dev/starry/ktscheduler/scheduler/KtScheduler.kt index c152c03..5f6c2e0 100644 --- a/src/main/kotlin/dev/starry/ktscheduler/scheduler/KtScheduler.kt +++ b/src/main/kotlin/dev/starry/ktscheduler/scheduler/KtScheduler.kt @@ -23,6 +23,11 @@ import dev.starry.ktscheduler.executor.CoroutineExecutor import dev.starry.ktscheduler.job.Job import dev.starry.ktscheduler.jobstore.InMemoryJobStore import dev.starry.ktscheduler.jobstore.JobStore +import dev.starry.ktscheduler.triggers.CronTrigger +import dev.starry.ktscheduler.triggers.DailyTrigger +import dev.starry.ktscheduler.triggers.IntervalTrigger +import dev.starry.ktscheduler.triggers.OneTimeTrigger +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -30,9 +35,12 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import java.time.DayOfWeek import java.time.Duration +import java.time.LocalTime import java.time.ZoneId import java.time.ZonedDateTime +import java.util.UUID import java.util.logging.Logger /** @@ -88,12 +96,10 @@ class KtScheduler( * @see isRunning */ override fun start() { + logger.info("Starting scheduler...") // Check if the scheduler is already running. - if (::coroutineScope.isInitialized && coroutineScope.isActive) { - throw IllegalStateException("Scheduler is already running") - } - // Start the scheduler. - logger.info("Starting scheduler") + check(!isRunning()) { "Scheduler is already running" } + // Create a new coroutine scope and start processing due jobs. coroutineScope = createCoroutineScope() coroutineScope.launch { while (isActive) { @@ -103,7 +109,7 @@ class KtScheduler( delay(tickInterval) } } - logger.info("Scheduler started") + logger.info("Scheduler started!") } /** @@ -116,11 +122,9 @@ class KtScheduler( * @see isRunning */ override fun shutdown() { - logger.info("Shutting down scheduler") + logger.info("Shutting down scheduler...") // Check if the scheduler is running or not. - if (!::coroutineScope.isInitialized || !coroutineScope.isActive) { - throw IllegalStateException("Scheduler is not running") - } + check(isRunning()) { "Scheduler is not running" } coroutineScope.cancel() logger.info("Scheduler shut down") } @@ -265,6 +269,190 @@ class KtScheduler( eventListeners.add(listener) } + // ============================================================================================ + // Convenience methods + // ============================================================================================ + + /** + * Schedules a job to run at a specific time on specific days of the week. + * + * This is a convenience method that creates a new job with a [CronTrigger] and adds it to the scheduler. + * It is equivalent to this code: + * + * ``` + * val trigger = CronTrigger(daysOfWeek, time) + * val job = Job( + * jobId = "runCron-${UUID.randomUUID()}", + * trigger = trigger, + * nextRunTime = trigger.getNextRunTime(ZonedDateTime.now(timeZone), timeZone), + * runConcurrently = runConcurrently, + * dispatcher = dispatcher, + * callback = callback + * ) + * + * scheduler.addJob(job) + * ``` + * + * @param daysOfWeek The set of days of the week on which the job should run. + * @param time The time at which the job should run. + * @param dispatcher The coroutine dispatcher to use. Default is [Dispatchers.Default]. + * @param runConcurrently Whether the job should run concurrently. Default is `true`. + * @param block The block of code to execute. + * @return The ID of the scheduled job. + */ + fun runCron( + daysOfWeek: Set, + time: LocalTime, + dispatcher: CoroutineDispatcher = Dispatchers.Default, + runConcurrently: Boolean = true, + block: suspend () -> Unit + ): String { + val trigger = CronTrigger(daysOfWeek, time) + val job = Job( + jobId = "runCron-${UUID.randomUUID()}", + trigger = trigger, + nextRunTime = trigger.getNextRunTime(ZonedDateTime.now(timeZone), timeZone), + runConcurrently = runConcurrently, + dispatcher = dispatcher, + callback = block + ) + job.let { addJob(it) }.also { return job.jobId } + } + + /** + * Schedules a job to run daily at a specific time. + * + * This is a convenience method that creates a new job with a [DailyTrigger] and adds it to the scheduler. + * It is equivalent to this code: + * + * ``` + * val trigger = DailyTrigger(dailyTime) + * val job = Job( + * jobId = "runDaily-${UUID.randomUUID()}", + * trigger = trigger, + * nextRunTime = trigger.getNextRunTime(ZonedDateTime.now(timeZone), timeZone), + * runConcurrently = runConcurrently, + * dispatcher = dispatcher, + * callback = callback + * ) + * + * scheduler.addJob(job) + * ``` + * + * @param dailyTime The time at which the job should run daily. + * @param dispatcher The coroutine dispatcher to use. Default is [Dispatchers.Default]. + * @param runConcurrently Whether the job should run concurrently. Default is `true`. + * @param block The block of code to execute. + * @return The ID of the scheduled job. + */ + fun runDaily( + dailyTime: LocalTime, + dispatcher: CoroutineDispatcher = Dispatchers.Default, + runConcurrently: Boolean = true, + block: suspend () -> Unit + ): String { + val trigger = DailyTrigger(dailyTime) + val job = Job( + jobId = "runDaily-${UUID.randomUUID()}", + trigger = trigger, + nextRunTime = trigger.getNextRunTime(ZonedDateTime.now(timeZone), timeZone), + runConcurrently = runConcurrently, + dispatcher = dispatcher, + callback = block + ) + job.let { addJob(it) }.also { return job.jobId } + } + + /** + * Schedules a job to run at a specific interval. + * + * This is a convenience method that creates a new job with an [IntervalTrigger] and adds it to the scheduler. + * It is equivalent to this code: + * + * ``` + * val trigger = IntervalTrigger(intervalSeconds) + * val job = Job( + * jobId = "runRepeating-${UUID.randomUUID()}", + * trigger = trigger, + * nextRunTime = trigger.getNextRunTime(ZonedDateTime.now(timeZone), timeZone), + * runConcurrently = runConcurrently, + * dispatcher = dispatcher, + * callback = block + * ) + * + * scheduler.addJob(job) + * ``` + * + * @param intervalSeconds The interval in seconds at which the job should run. + * @param dispatcher The coroutine dispatcher to use. Default is [Dispatchers.Default]. + * @param runConcurrently Whether the job should run concurrently. Default is `true`. + * @param block The block of code to execute. + * @return The ID of the scheduled job. + */ + fun runRepeating( + intervalSeconds: Long, + dispatcher: CoroutineDispatcher = Dispatchers.Default, + runConcurrently: Boolean = true, + block: suspend () -> Unit + ): String { + val trigger = IntervalTrigger(intervalSeconds) + val job = Job( + jobId = "runRepeating-${UUID.randomUUID()}", + trigger = trigger, + nextRunTime = trigger.getNextRunTime(ZonedDateTime.now(timeZone), timeZone), + runConcurrently = runConcurrently, + dispatcher = dispatcher, + callback = block + ) + job.let { addJob(it) }.also { return job.jobId } + } + + /** + * Schedules a job to run at a specific time. + * + * This is a convenience method that creates a new job with a [OneTimeTrigger] and adds it to the scheduler. + * It is equivalent to this code: + * + * ``` + * val job = Job( + * jobId = "runOnce-${UUID.randomUUID()}", + * trigger = OneTimeTrigger(runAt), + * nextRunTime = runAt, + * runConcurrently = runConcurrently, + * dispatcher = dispatcher, + * callback = callback + * ) + * + * scheduler.addJob(job) + * ``` + * + * @param runAt The time at which the job should run. + * @param dispatcher The coroutine dispatcher to use. Default is [Dispatchers.Default]. + * @param runConcurrently Whether the job should run concurrently. Default is `true`. + * @param block The block of code to execute. + * @return The ID of the scheduled job. + */ + fun runOnce( + runAt: ZonedDateTime, + dispatcher: CoroutineDispatcher = Dispatchers.Default, + runConcurrently: Boolean = true, + block: suspend () -> Unit + ): String { + val job = Job( + jobId = "runOnce-${UUID.randomUUID()}", + trigger = OneTimeTrigger(runAt), + nextRunTime = runAt, + runConcurrently = runConcurrently, + dispatcher = dispatcher, + callback = block + ) + job.let { addJob(it) }.also { return job.jobId } + } + + // ============================================================================================ + // Private methods + // ============================================================================================ + // Creates a new coroutine scope. private fun createCoroutineScope() = CoroutineScope(Dispatchers.Default + SupervisorJob()) @@ -284,20 +472,20 @@ class KtScheduler( // Execute the job. executor.execute( job = job, - onSuccess = { handleJobCompletion(job, now) }, - onError = { exc -> handleJobError(job, now, exc) } + onSuccess = { handleJobCompletion(job) }, + onError = { exc -> handleJobError(job, exc) } ) } } // Handles the completion of a job by updating the next run time or removing the job. - private fun handleJobCompletion(job: Job, now: ZonedDateTime) { + private fun handleJobCompletion(job: Job) { logger.info("Job ${job.jobId} completed successfully") notifyJobComplete(job.jobId) } // Handles an error encountered while executing a job. - private fun handleJobError(job: Job, now: ZonedDateTime, exception: Exception) { + private fun handleJobError(job: Job, exception: Exception) { logger.severe("Error executing job ${job.jobId}: $exception") notifyJobError(job.jobId, exception) } diff --git a/src/main/kotlin/dev/starry/ktscheduler/triggers/CronTrigger.kt b/src/main/kotlin/dev/starry/ktscheduler/triggers/CronTrigger.kt index dc869cd..15cde85 100644 --- a/src/main/kotlin/dev/starry/ktscheduler/triggers/CronTrigger.kt +++ b/src/main/kotlin/dev/starry/ktscheduler/triggers/CronTrigger.kt @@ -40,7 +40,7 @@ class CronTrigger( * @param timeZone The time zone in which the trigger is operating. * @return The next run time as a [ZonedDateTime]. */ - override fun getNextRunTime(currentTime: ZonedDateTime, timeZone: ZoneId): ZonedDateTime? { + override fun getNextRunTime(currentTime: ZonedDateTime, timeZone: ZoneId): ZonedDateTime { var nextRunTime = currentTime.withZoneSameInstant(timeZone).with(time).withNano(0) if (nextRunTime.isBefore(currentTime) || nextRunTime.isEqual(currentTime)) { nextRunTime = nextRunTime.plusDays(1) diff --git a/src/test/kotlin/dev/starry/ktscheduler/KtSchedulerTest.kt b/src/test/kotlin/dev/starry/ktscheduler/KtSchedulerTest.kt index a32f820..c706767 100644 --- a/src/test/kotlin/dev/starry/ktscheduler/KtSchedulerTest.kt +++ b/src/test/kotlin/dev/starry/ktscheduler/KtSchedulerTest.kt @@ -21,6 +21,8 @@ import dev.starry.ktscheduler.event.JobEvent import dev.starry.ktscheduler.event.JobEventListener import dev.starry.ktscheduler.job.Job import dev.starry.ktscheduler.scheduler.KtScheduler +import dev.starry.ktscheduler.triggers.CronTrigger +import dev.starry.ktscheduler.triggers.DailyTrigger import dev.starry.ktscheduler.triggers.IntervalTrigger import dev.starry.ktscheduler.triggers.OneTimeTrigger import junit.framework.TestCase.assertFalse @@ -29,6 +31,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest import org.junit.Test +import java.time.DayOfWeek +import java.time.LocalTime import java.time.ZonedDateTime import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -339,7 +343,7 @@ class KtSchedulerTest { // Wait for 3 seconds Thread.sleep(3000) // Assert that the job was only executed once in 3 seconds - // because the job is not run concurrently and it takes 2 seconds to execute + // because the job is not run concurrently, and it takes 2 seconds to execute assertEquals(1, eventListener.completedJobs.size) // Assert that the job was executed twice after 4 seconds Thread.sleep(1100) @@ -372,12 +376,56 @@ class KtSchedulerTest { Thread.sleep(3100) scheduler.shutdown() // Assert that the job was executed twice in 3 seconds - // because the job is run concurrently and it takes 2 seconds to execute + // because the job is run concurrently, and it takes 2 seconds to execute assertEquals(2, eventListener.completedJobs.size) assertEquals("longRunningJob", eventListener.completedJobs[0]) assertEquals("longRunningJob", eventListener.completedJobs[1]) } + @Test + fun `test runCron schedules the cron job`() { + val scheduler = KtScheduler() + val jobId = scheduler.runCron(setOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY), LocalTime.of(10, 0)) {} + scheduler.getJob(jobId)?.let { + assertTrue(it.trigger is CronTrigger) + assertTrue(it.jobId.startsWith("runCron")) + assertEquals(it.jobId, jobId) + } ?: fail("Job not found") + } + + @Test + fun `test runDaily schedules the daily job`() { + val scheduler = KtScheduler() + val jobId = scheduler.runDaily(dailyTime = LocalTime.of(10, 0)) {/* do nothing */ } + scheduler.getJob(jobId)?.let { + assertTrue(it.trigger is DailyTrigger) + assertTrue(it.jobId.startsWith("runDaily")) + assertEquals(it.jobId, jobId) + } ?: fail("Job not found") + } + + @Test + fun `test runRepeating schedules the repeating job`() { + val scheduler = KtScheduler() + val jobId = scheduler.runRepeating(intervalSeconds = 10) {/* do nothing */ } + scheduler.getJob(jobId)?.let { + assertTrue(it.trigger is IntervalTrigger) + assertTrue(it.jobId.startsWith("runRepeating")) + assertEquals(it.jobId, jobId) + } ?: fail("Job not found") + } + + @Test + fun `test runOnce schedules the one time job`() { + val scheduler = KtScheduler() + val jobId = scheduler.runOnce(ZonedDateTime.now().plusSeconds(10)) {/* do nothing */ } + scheduler.getJob(jobId)?.let { + assertTrue(it.trigger is OneTimeTrigger) + assertTrue(it.jobId.startsWith("runOnce")) + assertEquals(it.jobId, jobId) + } ?: fail("Job not found") + } + private fun createTestJob( jobId: String, runAt: ZonedDateTime = ZonedDateTime.now().plusSeconds(1),