Skip to content

Commit

Permalink
feat: Add shortcut methods for all inbuilt triggers
Browse files Browse the repository at this point in the history
Signed-off-by: starry-shivam <[email protected]>
  • Loading branch information
starry-shivam committed Jun 26, 2024
1 parent cdaaf63 commit 1d31be8
Show file tree
Hide file tree
Showing 4 changed files with 264 additions and 24 deletions.
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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! 🌟

------
Expand Down
216 changes: 202 additions & 14 deletions src/main/kotlin/dev/starry/ktscheduler/scheduler/KtScheduler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,24 @@ 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
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

/**
Expand Down Expand Up @@ -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) {
Expand All @@ -103,7 +109,7 @@ class KtScheduler(
delay(tickInterval)
}
}
logger.info("Scheduler started")
logger.info("Scheduler started!")
}

/**
Expand All @@ -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")
}
Expand Down Expand Up @@ -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<DayOfWeek>,
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())

Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 1d31be8

Please sign in to comment.