diff --git a/.idea/misc.xml b/.idea/misc.xml
index a56dd24..b0137f1 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,7 +1,7 @@
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 6680b8d..6578861 100644
--- a/README.md
+++ b/README.md
@@ -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.
@@ -50,6 +53,7 @@ dependencies {
implementation("com.github.Pool-Of-Tears:KtScheduler:version")
}
```
+
------
### Documentation 📑
diff --git a/build.gradle.kts b/build.gradle.kts
index 32dd9e4..d979143 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -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
diff --git a/src/main/kotlin/dev/starry/ktscheduler/event/JobEvent.kt b/src/main/kotlin/dev/starry/ktscheduler/event/JobEvent.kt
index abcb855..6d52e07 100644
--- a/src/main/kotlin/dev/starry/ktscheduler/event/JobEvent.kt
+++ b/src/main/kotlin/dev/starry/ktscheduler/event/JobEvent.kt
@@ -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
/**
diff --git a/src/main/kotlin/dev/starry/ktscheduler/job/Job.kt b/src/main/kotlin/dev/starry/ktscheduler/job/Job.kt
index 25adbd2..3452fca 100644
--- a/src/main/kotlin/dev/starry/ktscheduler/job/Job.kt
+++ b/src/main/kotlin/dev/starry/ktscheduler/job/Job.kt
@@ -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.
@@ -67,3 +71,5 @@ data class Job(
val callback: suspend () -> Unit
)
+
+
diff --git a/src/main/kotlin/dev/starry/ktscheduler/jobstore/InMemoryJobStore.kt b/src/main/kotlin/dev/starry/ktscheduler/jobstore/InMemoryJobStore.kt
index 72e9c71..3e16433 100644
--- a/src/main/kotlin/dev/starry/ktscheduler/jobstore/InMemoryJobStore.kt
+++ b/src/main/kotlin/dev/starry/ktscheduler/jobstore/InMemoryJobStore.kt
@@ -57,7 +57,7 @@ class InMemoryJobStore : JobStore {
*/
override fun getDueJobs(currentTime: ZonedDateTime, maxGraceTime: Duration?): List {
return jobs.values.filter { job ->
- val jobNextRunTime = job.nextRunTime
+ val jobNextRunTime = job.nextRunTime!!
maxGraceTime?.let { graceTime ->
jobNextRunTime <= currentTime && currentTime <= jobNextRunTime.plus(graceTime)
} ?: (jobNextRunTime <= currentTime)
diff --git a/src/main/kotlin/dev/starry/ktscheduler/scheduler/KtScheduler.kt b/src/main/kotlin/dev/starry/ktscheduler/scheduler/KtScheduler.kt
index 5f6c2e0..aa70d95 100644
--- a/src/main/kotlin/dev/starry/ktscheduler/scheduler/KtScheduler.kt
+++ b/src/main/kotlin/dev/starry/ktscheduler/scheduler/KtScheduler.kt
@@ -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
/**
@@ -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)
}
/**
@@ -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].
@@ -301,6 +298,7 @@ class KtScheduler(
* @return The ID of the scheduled job.
*/
fun runCron(
+ jobId: String = "runCron-${UUID.randomUUID()}",
daysOfWeek: Set,
time: LocalTime,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
@@ -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 }
}
/**
@@ -328,17 +326,18 @@ 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`.
@@ -346,6 +345,7 @@ class KtScheduler(
* @return The ID of the scheduled job.
*/
fun runDaily(
+ jobId: String = "runDaily-${UUID.randomUUID()}",
dailyTime: LocalTime,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
runConcurrently: Boolean = true,
@@ -353,14 +353,14 @@ class KtScheduler(
): 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 }
}
/**
@@ -372,17 +372,18 @@ 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`.
@@ -390,6 +391,7 @@ class KtScheduler(
* @return The ID of the scheduled job.
*/
fun runRepeating(
+ jobId: String = "runRepeating-${UUID.randomUUID()}",
intervalSeconds: Long,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
runConcurrently: Boolean = true,
@@ -397,14 +399,14 @@ class KtScheduler(
): 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 }
}
/**
@@ -415,17 +417,18 @@ 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`.
@@ -433,20 +436,21 @@ class KtScheduler(
* @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 }
}
// ============================================================================================
diff --git a/src/test/kotlin/dev/starry/ktscheduler/CoroutineExecutorTest.kt b/src/test/kotlin/dev/starry/ktscheduler/CoroutineExecutorTest.kt
index 37165e5..517f4e7 100644
--- a/src/test/kotlin/dev/starry/ktscheduler/CoroutineExecutorTest.kt
+++ b/src/test/kotlin/dev/starry/ktscheduler/CoroutineExecutorTest.kt
@@ -40,15 +40,6 @@ 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()
@@ -56,6 +47,7 @@ class CoroutineExecutorTest {
@Test
fun testExecuteSuccess(): Unit = runTest {
+ val executor = CoroutineExecutor()
val job = createTestJob(scheduler = testScheduler) { }
var onSuccessCalled = false
val onSuccess: () -> Unit = { onSuccessCalled = true }
@@ -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") }
@@ -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
@@ -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)
@@ -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,
diff --git a/src/test/kotlin/dev/starry/ktscheduler/KtSchedulerTest.kt b/src/test/kotlin/dev/starry/ktscheduler/KtSchedulerTest.kt
index c706767..506fd47 100644
--- a/src/test/kotlin/dev/starry/ktscheduler/KtSchedulerTest.kt
+++ b/src/test/kotlin/dev/starry/ktscheduler/KtSchedulerTest.kt
@@ -27,7 +27,6 @@ import dev.starry.ktscheduler.triggers.IntervalTrigger
import dev.starry.ktscheduler.triggers.OneTimeTrigger
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -39,7 +38,6 @@ import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.fail
-@OptIn(ExperimentalCoroutinesApi::class)
class KtSchedulerTest {
@Test
@@ -274,7 +272,7 @@ class KtSchedulerTest {
scheduler.addEventListener(eventListener)
scheduler.start()
- Thread.sleep(2100)
+ Thread.sleep(2200)
// Job 1 should be completed twice
assertEquals(2, eventListener.completedJobs.size)
@@ -284,12 +282,12 @@ class KtSchedulerTest {
// Job 1 should be rescheduled
val rescheduledJob = scheduler.getJob("job1")
assertNotNull(rescheduledJob)
- assertEquals(startTime.plusSeconds(3).year, rescheduledJob.nextRunTime.year)
- assertEquals(startTime.plusSeconds(3).month, rescheduledJob.nextRunTime.month)
- assertEquals(startTime.plusSeconds(3).dayOfMonth, rescheduledJob.nextRunTime.dayOfMonth)
- assertEquals(startTime.plusSeconds(3).hour, rescheduledJob.nextRunTime.hour)
- assertEquals(startTime.plusSeconds(3).minute, rescheduledJob.nextRunTime.minute)
- assertEquals(startTime.plusSeconds(3).second, rescheduledJob.nextRunTime.second)
+ assertEquals(startTime.plusSeconds(3).year, rescheduledJob.nextRunTime!!.year)
+ assertEquals(startTime.plusSeconds(3).month, rescheduledJob.nextRunTime!!.month)
+ assertEquals(startTime.plusSeconds(3).dayOfMonth, rescheduledJob.nextRunTime!!.dayOfMonth)
+ assertEquals(startTime.plusSeconds(3).hour, rescheduledJob.nextRunTime!!.hour)
+ assertEquals(startTime.plusSeconds(3).minute, rescheduledJob.nextRunTime!!.minute)
+ assertEquals(startTime.plusSeconds(3).second, rescheduledJob.nextRunTime!!.second)
scheduler.shutdown()
}
@@ -346,7 +344,7 @@ class KtSchedulerTest {
// 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)
+ Thread.sleep(1200)
assertEquals(2, eventListener.completedJobs.size)
assertEquals("longRunningJob", eventListener.completedJobs[0])
assertEquals("longRunningJob", eventListener.completedJobs[1])
@@ -382,10 +380,71 @@ class KtSchedulerTest {
assertEquals("longRunningJob", eventListener.completedJobs[1])
}
+ @Test
+ fun `test nextRuntime is getting calculated if not passed`() {
+ val scheduler = KtScheduler()
+ val intervalJob = Job(
+ jobId = "intervalJob",
+ trigger = IntervalTrigger(intervalSeconds = 1),
+ callback = { /* Do nothing */ }
+ )
+ val oneTimeJob = Job(
+ jobId = "oneTimeJob",
+ trigger = OneTimeTrigger(runAt = ZonedDateTime.now().plusSeconds(1)),
+ callback = { /* Do nothing */ }
+ )
+ val dailyJob = Job(
+ jobId = "dailyJob",
+ trigger = DailyTrigger(time = LocalTime.of(10, 0)),
+ callback = { /* Do nothing */ }
+ )
+ val cronJob = Job(
+ jobId = "cronJob",
+ trigger = CronTrigger(
+ daysOfWeek = setOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY),
+ time = LocalTime.of(10, 0)
+ ),
+ callback = { /* Do nothing */ }
+ )
+
+ scheduler.addJob(intervalJob)
+ scheduler.addJob(oneTimeJob)
+ scheduler.addJob(dailyJob)
+ scheduler.addJob(cronJob)
+
+ scheduler.getJob("intervalJob")?.let {
+ assertNotNull(it.nextRunTime)
+ assertTrue(it.nextRunTime!! > ZonedDateTime.now())
+ assertTrue(it.nextRunTime!! < ZonedDateTime.now().plusSeconds(2))
+ } ?: fail("Job not found")
+
+ scheduler.getJob("oneTimeJob")?.let {
+ assertNotNull(it.nextRunTime)
+ assertTrue(it.nextRunTime!! > ZonedDateTime.now())
+ assertTrue(it.nextRunTime!! < ZonedDateTime.now().plusSeconds(2))
+ } ?: fail("Job not found")
+
+ scheduler.getJob("dailyJob")?.let {
+ assertNotNull(it.nextRunTime)
+ assertTrue(it.nextRunTime!!.hour == 10)
+ assertTrue(it.nextRunTime!!.minute == 0)
+ } ?: fail("Job not found")
+ scheduler.getJob("cronJob")?.let {
+ assertNotNull(it.nextRunTime)
+ assertTrue(it.nextRunTime!!.dayOfWeek == DayOfWeek.SATURDAY || it.nextRunTime!!.dayOfWeek == DayOfWeek.SUNDAY)
+ assertTrue(it.nextRunTime!!.hour == 10)
+ assertTrue(it.nextRunTime!!.minute == 0)
+ } ?: fail("Job not found")
+ }
+
@Test
fun `test runCron schedules the cron job`() {
val scheduler = KtScheduler()
- val jobId = scheduler.runCron(setOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY), LocalTime.of(10, 0)) {}
+ val jobId = scheduler.runCron(
+ daysOfWeek = setOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY),
+ time = LocalTime.of(10, 0)
+ ) { /* do nothing */ }
+
scheduler.getJob(jobId)?.let {
assertTrue(it.trigger is CronTrigger)
assertTrue(it.jobId.startsWith("runCron"))
@@ -418,7 +477,7 @@ class KtSchedulerTest {
@Test
fun `test runOnce schedules the one time job`() {
val scheduler = KtScheduler()
- val jobId = scheduler.runOnce(ZonedDateTime.now().plusSeconds(10)) {/* do nothing */ }
+ val jobId = scheduler.runOnce(runAt = ZonedDateTime.now().plusSeconds(10)) {/* do nothing */ }
scheduler.getJob(jobId)?.let {
assertTrue(it.trigger is OneTimeTrigger)
assertTrue(it.jobId.startsWith("runOnce"))