main.misk.cron.CronManager.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of misk-cron Show documentation
Show all versions of misk-cron Show documentation
Open source application container in Kotlin
package misk.cron
import com.cronutils.model.CronType
import com.cronutils.model.definition.CronDefinitionBuilder
import com.cronutils.model.time.ExecutionTime
import com.cronutils.parser.CronParser
import jakarta.inject.Inject
import jakarta.inject.Singleton
import misk.cron.CronManager.CronEntry.ExecutionTimeMetadata.Companion.toMetadata
import wisp.logging.getLogger
import java.time.Clock
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutorService
import kotlin.jvm.optionals.getOrNull
@Singleton
class CronManager @Inject constructor() {
@Inject private lateinit var clock: Clock
@Inject @ForMiskCron private lateinit var executorService: ExecutorService
@Inject @ForMiskCron private lateinit var zoneId: ZoneId
private val runningCrons = mutableListOf>()
data class CronEntry(
val name: String,
val cronTab: String,
val executionTime: ExecutionTime,
val runnable: Runnable
) {
internal data class ExecutionTimeMetadata(
val nextExecution: String?,
val timeToNextExecution: String?,
val lastExecution: String?,
val timeFromLastExecution: String?
) {
companion object {
fun ExecutionTime.toMetadata(): ExecutionTimeMetadata {
val now = ZonedDateTime.now()
val nextExecution = nextExecution(now)
val timeToNextExecution = timeToNextExecution(now)
val lastExecution = lastExecution(now)
val timeFromLastExecution = timeFromLastExecution(now)
return ExecutionTimeMetadata(
nextExecution = nextExecution.getOrNull().toString(),
timeToNextExecution = timeToNextExecution.getOrNull().toString(),
lastExecution = lastExecution.getOrNull().toString(),
timeFromLastExecution = timeFromLastExecution.getOrNull().toString()
)
}
}
}
internal data class Metadata(
val name: String,
val cronTab: String,
val executionTime: ExecutionTimeMetadata,
val runnable: String
)
internal fun toMetadata() = Metadata(
name = name,
cronTab = cronTab,
executionTime = executionTime.toMetadata(),
runnable = runnable.toString()
)
}
private val cronEntries = mutableMapOf()
internal fun getMetadata() = CronData(
cronEntries = cronEntries.mapValues { it.value.toMetadata() },
runningCrons = runningCrons.map { it.toString() }
)
internal fun addCron(name: String, crontab: String, cron: Runnable) {
require(name.isNotEmpty()) { "Expecting a valid cron name" }
require(cronEntries[name] == null) { "Cron $name is already registered" }
logger.info { "Adding cron entry $name, crontab=$crontab" }
val cronDefinition = CronDefinitionBuilder.instanceDefinitionFor(CronType.UNIX)
val executionTime = ExecutionTime.forCron(CronParser(cronDefinition).parse(crontab))
val cronEntry = CronEntry(name, crontab, executionTime, cron)
cronEntries[name] = cronEntry
}
internal fun removeAllCrons() {
logger.info { "Removing all cron entries" }
cronEntries.clear()
}
fun runReadyCrons(lastRun: Instant) {
val now = clock.instant()
val previousTime = ZonedDateTime.ofInstant(lastRun, zoneId)
logger.info {
"Last execution was at $previousTime, now=${ZonedDateTime.ofInstant(now, zoneId)}"
}
removeCompletedCrons()
cronEntries.values.forEach { cronEntry ->
val nextExecutionTime = cronEntry.executionTime.nextExecution(previousTime).orElseThrow()
.withSecond(0)
.withNano(0)
if (nextExecutionTime.toInstant() <= now) {
logger.info {
"CronJob ${cronEntry.name} was ready at $nextExecutionTime"
}
runCron(cronEntry)
}
}
}
private fun removeCompletedCrons() {
runningCrons.removeIf { it.isDone }
}
private fun runCron(cronEntry: CronEntry) {
runningCrons.add(
CompletableFuture.runAsync(
{
val name = cronEntry.name
try {
logger.info { "Executing cronjob $name" }
cronEntry.runnable.run()
} catch (t: Throwable) {
logger.error { "Exception on cronjob $name: ${t.stackTraceToString()}" }
} finally {
logger.info { "Executing cronjob $name complete" }
}
},
executorService
)
)
}
fun waitForCronsComplete() {
CompletableFuture.allOf(*runningCrons.toTypedArray()).join()
removeCompletedCrons()
}
companion object {
private val logger = getLogger()
}
}