Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make nextRuntime optional & add optional jobId parameter in shortcut methods #23

Merged
merged 1 commit into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ keeping things simple and easy to use.

### Highlights ✨

- Lightweight with no external dependencies other than `kotlinx:kotlinx-coroutines-core` from the Kotlin standard library.
- Lightweight with no external dependencies other than `kotlinx:kotlinx-coroutines-core` from the Kotlin standard
library.
- Designed to respect time zones, allowing you to set the time zone yourself or use the system's time zone by default.
- Provides four different types of triggers to execute jobs daily, at certain intervals, once at a given time, or with a cron-like schedule.
- Can run multiple instances of a job concurrently while giving you the option to run only one instance if the job is already executing.
- Provides four different types of triggers to execute jobs daily, at certain intervals, once at a given time, or with a
cron-like schedule.
- Can run multiple instances of a job concurrently while giving you the option to run only one instance if the job is
already executing.
- Can be easily extended to suit your specific use case by allowing you to write custom triggers and job stores.
- Easy to use and straightforward API with full KDoc/Javadoc documentation coverage.
- 100% unit test coverage to ensure reliability across different scenarios.
Expand All @@ -50,6 +53,7 @@ dependencies {
implementation("com.github.Pool-Of-Tears:KtScheduler:version")
}
```

------

### Documentation 📑
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ tasks.register("printLineCoverage") {
group = "verification"
dependsOn("koverXmlReport")
doLast {
val report = file("$buildDir/reports/kover/report.xml")
val report = file("${layout.buildDirectory}/reports/kover/report.xml")
val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(report)
val rootNode = doc.firstChild
var childNode = rootNode.firstChild
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/dev/starry/ktscheduler/event/JobEvent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

package dev.starry.ktscheduler.event

import dev.starry.ktscheduler.event.JobStatus.ERROR
import dev.starry.ktscheduler.event.JobStatus.SUCCESS
import java.time.ZonedDateTime

/**
Expand Down
8 changes: 7 additions & 1 deletion src/main/kotlin/dev/starry/ktscheduler/job/Job.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,12 @@ data class Job(

/**
* The next time the job should run.
*
* When adding a new job, it is used as the initial run time.
* If not provided, it will be calculated automatically based
* on the [Trigger.getNextRunTime] method when the job is added.
*/
val nextRunTime: ZonedDateTime,
val nextRunTime: ZonedDateTime? = null,

/**
* Whether to run multiple instances of this job concurrently.
Expand All @@ -67,3 +71,5 @@ data class Job(
val callback: suspend () -> Unit
)



Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class InMemoryJobStore : JobStore {
*/
override fun getDueJobs(currentTime: ZonedDateTime, maxGraceTime: Duration?): List<Job> {
return jobs.values.filter { job ->
val jobNextRunTime = job.nextRunTime
val jobNextRunTime = job.nextRunTime!!
maxGraceTime?.let { graceTime ->
jobNextRunTime <= currentTime && currentTime <= jobNextRunTime.plus(graceTime)
} ?: (jobNextRunTime <= currentTime)
Expand Down
98 changes: 51 additions & 47 deletions src/main/kotlin/dev/starry/ktscheduler/scheduler/KtScheduler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,9 @@ 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 kotlinx.coroutines.*
import java.time.*
import java.util.*
import java.util.logging.Logger

/**
Expand Down Expand Up @@ -163,7 +152,14 @@ class KtScheduler(
*/
override fun addJob(job: Job) {
logger.info("Adding job ${job.jobId}")
jobStore.addJob(job)
val jobToAdd = job.nextRunTime?.let {
job
} ?: job.copy(
nextRunTime = job.trigger.getNextRunTime(ZonedDateTime.now(timeZone), timeZone)
).apply {
logger.info("Calculated next run time for job ${job.jobId}: $nextRunTime")
}
jobStore.addJob(jobToAdd)
}

/**
Expand Down Expand Up @@ -282,17 +278,18 @@ class KtScheduler(
* ```
* 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
* jobId = "runCron-${UUID.randomUUID()}",
* trigger = trigger,
* nextRunTime = trigger.getNextRunTime(ZonedDateTime.now(timeZone), timeZone),
* runConcurrently = runConcurrently,
* dispatcher = dispatcher,
* callback = callback
* )
*
* scheduler.addJob(job)
* ```
*
* @param jobId The ID of the job. Default is a random UUID.
* @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].
Expand All @@ -301,6 +298,7 @@ class KtScheduler(
* @return The ID of the scheduled job.
*/
fun runCron(
jobId: String = "runCron-${UUID.randomUUID()}",
daysOfWeek: Set<DayOfWeek>,
time: LocalTime,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
Expand All @@ -309,14 +307,14 @@ class KtScheduler(
): String {
val trigger = CronTrigger(daysOfWeek, time)
val job = Job(
jobId = "runCron-${UUID.randomUUID()}",
jobId = jobId,
trigger = trigger,
nextRunTime = trigger.getNextRunTime(ZonedDateTime.now(timeZone), timeZone),
runConcurrently = runConcurrently,
dispatcher = dispatcher,
callback = block
)
job.let { addJob(it) }.also { return job.jobId }
addJob(job).also { return job.jobId }
}

/**
Expand All @@ -328,39 +326,41 @@ class KtScheduler(
* ```
* 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
* jobId = "runDaily-${UUID.randomUUID()}",
* trigger = trigger,
* nextRunTime = trigger.getNextRunTime(ZonedDateTime.now(timeZone), timeZone),
* runConcurrently = runConcurrently,
* dispatcher = dispatcher,
* callback = callback
* )
*
* scheduler.addJob(job)
* ```
*
* @param jobId The ID of the job. Default is a random UUID.
* @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(
jobId: String = "runDaily-${UUID.randomUUID()}",
dailyTime: LocalTime,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
runConcurrently: Boolean = true,
block: suspend () -> Unit
): String {
val trigger = DailyTrigger(dailyTime)
val job = Job(
jobId = "runDaily-${UUID.randomUUID()}",
jobId = jobId,
trigger = trigger,
nextRunTime = trigger.getNextRunTime(ZonedDateTime.now(timeZone), timeZone),
runConcurrently = runConcurrently,
dispatcher = dispatcher,
callback = block
)
job.let { addJob(it) }.also { return job.jobId }
addJob(job).also { return job.jobId }
}

/**
Expand All @@ -372,39 +372,41 @@ class KtScheduler(
* ```
* 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
* jobId = "runRepeating-${UUID.randomUUID()}",
* trigger = trigger,
* nextRunTime = trigger.getNextRunTime(ZonedDateTime.now(timeZone), timeZone),
* runConcurrently = runConcurrently,
* dispatcher = dispatcher,
* callback = block
* )
*
* scheduler.addJob(job)
* ```
*
* @param jobId The ID of the job. Default is a random UUID.
* @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(
jobId: String = "runRepeating-${UUID.randomUUID()}",
intervalSeconds: Long,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
runConcurrently: Boolean = true,
block: suspend () -> Unit
): String {
val trigger = IntervalTrigger(intervalSeconds)
val job = Job(
jobId = "runRepeating-${UUID.randomUUID()}",
jobId = jobId,
trigger = trigger,
nextRunTime = trigger.getNextRunTime(ZonedDateTime.now(timeZone), timeZone),
runConcurrently = runConcurrently,
dispatcher = dispatcher,
callback = block
)
job.let { addJob(it) }.also { return job.jobId }
addJob(job).also { return job.jobId }
}

/**
Expand All @@ -415,38 +417,40 @@ class KtScheduler(
*
* ```
* val job = Job(
* jobId = "runOnce-${UUID.randomUUID()}",
* trigger = OneTimeTrigger(runAt),
* nextRunTime = runAt,
* runConcurrently = runConcurrently,
* dispatcher = dispatcher,
* callback = callback
* jobId = "runOnce-${UUID.randomUUID()}",
* trigger = OneTimeTrigger(runAt),
* nextRunTime = runAt,
* runConcurrently = runConcurrently,
* dispatcher = dispatcher,
* callback = callback
* )
*
* scheduler.addJob(job)
* ```
*
* @param jobId The ID of the job. Default is a random UUID.
* @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(
jobId: String = "runOnce-${UUID.randomUUID()}",
runAt: ZonedDateTime,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
runConcurrently: Boolean = true,
block: suspend () -> Unit
): String {
val job = Job(
jobId = "runOnce-${UUID.randomUUID()}",
jobId = jobId,
trigger = OneTimeTrigger(runAt),
nextRunTime = runAt,
runConcurrently = runConcurrently,
dispatcher = dispatcher,
callback = block
)
job.let { addJob(it) }.also { return job.jobId }
addJob(job).also { return job.jobId }
}

// ============================================================================================
Expand Down
21 changes: 9 additions & 12 deletions src/test/kotlin/dev/starry/ktscheduler/CoroutineExecutorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,14 @@ import kotlin.test.assertNotNull
@OptIn(ExperimentalCoroutinesApi::class)
class CoroutineExecutorTest {

private lateinit var executor: CoroutineExecutor
private lateinit var trigger: OneTimeTrigger

@Before
fun setUp() {
executor = CoroutineExecutor()
trigger = OneTimeTrigger(ZonedDateTime.now(ZoneId.of("UTC")).plusSeconds(1))
}

@After
fun tearDown() {
Dispatchers.resetMain()
}

@Test
fun testExecuteSuccess(): Unit = runTest {
val executor = CoroutineExecutor()
val job = createTestJob(scheduler = testScheduler) { }
var onSuccessCalled = false
val onSuccess: () -> Unit = { onSuccessCalled = true }
Expand All @@ -68,6 +60,7 @@ class CoroutineExecutorTest {

@Test
fun testExecuteError(): Unit = runTest {
val executor = CoroutineExecutor()
val job = createTestJob(scheduler = testScheduler) { throw IllegalArgumentException("Error") }

val onSuccess: () -> Unit = { fail("onSuccess should not be called") }
Expand All @@ -83,6 +76,7 @@ class CoroutineExecutorTest {

@Test
fun testConcurrentExecution(): Unit = runTest {
val executor = CoroutineExecutor()
// Create a job that takes 100ms to execute.
val job = createTestJob(
scheduler = testScheduler, runConcurrently = true
Expand All @@ -102,11 +96,14 @@ class CoroutineExecutorTest {

@Test
fun testNonConcurrentExecution(): Unit = runTest {
val executor = CoroutineExecutor()
// Create a job that takes 100ms to execute.
val job = createTestJob(scheduler = testScheduler, runConcurrently = false) { delay(100) }
val job = createTestJob(
scheduler = testScheduler, runConcurrently = false
) { delay(100) }

var onSuccessCalled = 0
val onSuccess: () -> Unit = { onSuccessCalled++ }
val onSuccess: () -> Unit = { onSuccessCalled += 1 }
val onError: (Throwable) -> Unit = { fail("onError should not be called") }
// Execute the job 3 times concurrently.
executor.execute(job, onSuccess, onError)
Expand All @@ -124,7 +121,7 @@ class CoroutineExecutorTest {
callback: suspend () -> Unit,
): Job = Job(
jobId = jobId,
trigger = trigger,
trigger = OneTimeTrigger(ZonedDateTime.now(ZoneId.of("UTC")).plusSeconds(1)),
nextRunTime = ZonedDateTime.now(),
dispatcher = UnconfinedTestDispatcher(scheduler),
runConcurrently = runConcurrently,
Expand Down
Loading
Loading