All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.nemerosa.ontrack.job.support.DefaultJobScheduler.kt Maven / Gradle / Ivy

There is a newer version: 4.4.5
Show newest version
package net.nemerosa.ontrack.job.support

import io.micrometer.core.instrument.MeterRegistry
import net.nemerosa.ontrack.common.Time
import net.nemerosa.ontrack.job.*
import org.apache.commons.lang3.Validate
import org.slf4j.LoggerFactory
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit
import java.util.*
import java.util.concurrent.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
import java.util.function.BiFunction

/**
 * @property meterRegistry If set, the scheduler will register job metrics
 */
class DefaultJobScheduler
@JvmOverloads
constructor(
        private val jobDecorator: JobDecorator,
        private val schedulerPool: ScheduledExecutorService,
        private val jobListener: JobListener,
        initiallyPaused: Boolean,
        private val jobPoolProvider: BiFunction,
        private val scattering: Boolean,
        private val scatteringRatio: Double,
        private val meterRegistry: MeterRegistry? = null
) : JobScheduler {

    private val logger = LoggerFactory.getLogger(JobScheduler::class.java)

    private val services = ConcurrentHashMap(TreeMap())
    private val schedulerPaused: AtomicBoolean

    private val idGenerator = AtomicLong()

    @JvmOverloads
    constructor(
            jobDecorator: JobDecorator,
            schedulerPool: ScheduledExecutorService,
            jobListener: JobListener,
            initiallyPaused: Boolean,
            scattering: Boolean,
            scatteringRatio: Double,
            meterRegistry: MeterRegistry? = null
    ) : this(
            jobDecorator,
            schedulerPool,
            jobListener,
            initiallyPaused,
            BiFunction { executorService, _ -> executorService },
            scattering,
            scatteringRatio,
            meterRegistry
    )

    private fun MeterRegistry.statusGauge(
            name: String,
            statusFilterFn: (JobStatus) -> Boolean
    ) {
        gauge(
                "ontrack_job_${name}_total",
                services,
                {
                    it.filter { (_, service) -> statusFilterFn(service.jobStatus) }
                            .size.toDouble()
                }
        )
    }

    init {
        Validate.inclusiveBetween(0.0, 1.0, scatteringRatio)
        this.schedulerPaused = AtomicBoolean(initiallyPaused)
        // Metrics
        if (meterRegistry != null) {
            // count
            meterRegistry.gaugeMapSize(
                    "ontrack_job_count_total",
                    emptyList(),
                    services
            )
            meterRegistry.statusGauge("running", { it.isRunning })
            meterRegistry.statusGauge("disabled", { it.isDisabled })
            meterRegistry.statusGauge("paused", { it.isPaused })
            meterRegistry.statusGauge("error", { it.isError })
            meterRegistry.statusGauge("invalid", { !it.isValid })
            meterRegistry.gauge(
                    "ontrack_job_error_count_total",
                    services,
                    {
                        it.values.map { it.jobStatus.lastErrorCount }.sum().toDouble()
                    }
            )
        }
    }

    override fun schedule(job: Job, schedule: Schedule) {
        logger.info("[scheduler][job]{} Scheduling with {}", job.key, schedule)
        // Manages existing schedule
        val existingService = services[job.key]
        if (existingService != null) {
            logger.info("[scheduler][job]{} Modifying existing schedule", job.key)
            existingService.update(
                    job,
                    schedule
            )
        }
        // Creates and starts the scheduled service
        else {
            logger.info("[scheduler][job]{} Starting scheduled service", job.key)
            // Copy stats from old schedule
            val jobScheduledService = JobScheduledService(
                    job,
                    schedule,
                    schedulerPool,
                    jobListener.isPausedAtStartup(job.key)
            )
            // Registration
            services[job.key] = jobScheduledService
        }
    }

    override fun unschedule(key: JobKey): Boolean {
        return unschedule(key, true)
    }

    private fun unschedule(key: JobKey, forceStop: Boolean): Boolean {
        logger.debug("[scheduler][job]{} Unscheduling job", key)
        val existingService = services.remove(key)
        return if (existingService != null) {
            logger.debug("[scheduler][job]{} Stopping running job", key)
            existingService.cancel(forceStop)
            true
        } else {
            false
        }
    }

    override fun pause() {
        schedulerPaused.set(true)
    }

    override fun resume() {
        schedulerPaused.set(false)
    }

    override fun isPaused(): Boolean {
        return schedulerPaused.get()
    }

    override fun pause(key: JobKey): Boolean {
        val existingService = services[key]
        if (existingService != null) {
            existingService.pause()
            return true
        } else {
            throw JobNotScheduledException(key)
        }
    }

    override fun resume(key: JobKey): Boolean {
        val existingService = services[key]
        if (existingService != null) {
            existingService.resume()
            return true
        } else {
            throw JobNotScheduledException(key)
        }
    }

    override fun getJobStatus(key: JobKey): Optional {
        val existingService = services[key]
        return if (existingService != null) {
            Optional.of(existingService.jobStatus)
        } else {
            Optional.empty()
        }
    }

    override fun getJobKey(id: Long): Optional {
        return services.values.stream()
                .filter { service -> service.id == id }
                .map { it.jobKey }
                .findFirst()
    }

    override fun stop(key: JobKey): Boolean {
        val existingService = services[key]
        return existingService?.stop() ?: throw JobNotScheduledException(key)
    }

    override fun getAllJobKeys(): Collection {
        return services.keys
    }

    override fun getJobKeysOfType(type: JobType): Collection {
        return allJobKeys
                .filter { key -> key.sameType(type) }
                .toSet()
    }

    override fun getJobKeysOfCategory(category: JobCategory): Collection {
        return allJobKeys
                .filter { key -> key.sameCategory(category) }
                .toSet()
    }

    override fun getJobStatuses(): Collection {
        return services.values
                .map { it.jobStatus }
                .sortedBy { it.id }
                .toList()
    }

    override fun fireImmediately(jobKey: JobKey): Optional> {
        // Gets the existing scheduled service
        val jobScheduledService = services[jobKey] ?: throw JobNotScheduledException(jobKey)
        // Fires the job immediately
        return jobScheduledService.doRun(true)
    }

    private fun getExecutorService(job: Job): ExecutorService {
        return jobPoolProvider.apply(schedulerPool, job)
    }

    private inner class JobScheduledService(
            initialJob: Job,
            initialSchedule: Schedule,
            private val scheduledExecutorService: ScheduledExecutorService,
            pausedAtStartup: Boolean
    ) : Runnable {

        private var job = initialJob
        private var schedule = initialSchedule

        val id: Long = idGenerator.incrementAndGet()
        private var actualSchedule: Schedule = Schedule.NONE
        private var scheduledFuture: ScheduledFuture<*>? = null

        private val paused: AtomicBoolean = AtomicBoolean(pausedAtStartup)

        private val currentExecution = AtomicReference>()
        private val runProgress = AtomicReference()
        private val runCount = AtomicLong()
        private val lastRunDate = AtomicReference()
        private val lastRunDurationMs = AtomicLong()
        private val lastErrorCount = AtomicLong()
        private val lastError = AtomicReference(null)

        init {
            // Paused at startup
            if (pausedAtStartup) {
                logger.debug("[job]{} Job paused at startup", job.key)
            }
            // Initial schedule
            createSchedule()
        }

        private fun createSchedule() {
            // Converting all units to milliseconds
            var initialPeriod = TimeUnit.MILLISECONDS.convert(schedule.initialPeriod, schedule.unit)
            val period = TimeUnit.MILLISECONDS.convert(schedule.period, schedule.unit)
            // Scattering
            if (scattering) {
                // Computes the hash for the job key
                val hash = Math.abs(job.key.toString().hashCode()) % 10000
                // Period to consider
                val scatteringMax = (period * scatteringRatio).toLong()
                if (scatteringMax > 0) {
                    // Modulo on the period
                    val delay = hash * scatteringMax / 10000
                    logger.debug("[job]{} Scattering enabled - additional delay: {} ms", job.key, delay)
                    // Adding to the initial delay
                    initialPeriod += delay
                }
            }
            // Actual schedule
            actualSchedule = Schedule(
                    initialPeriod,
                    period,
                    TimeUnit.MILLISECONDS
            )
            // Scheduling now
            scheduledFuture = if (schedule.period > 0) {
                scheduledExecutorService.scheduleWithFixedDelay(
                        this,
                        initialPeriod,
                        period,
                        TimeUnit.MILLISECONDS
                )
            } else {
                logger.debug("[job]{} Job not scheduled since period = 0", job.key)
                null
            }
        }

        /**
         * Updates (if needed) the service to use the new job.
         */
        fun update(
                newJob: Job,
                newSchedule: Schedule
        ) {
            // Checks the key of the job
            if (job.key != newJob.key) {
                throw IllegalStateException("The job assigned to a job service " +
                        "cannot have a different key. " +
                        "Expected=${job.key}, Actual=${newJob.key}")
            }
            // Adapting the schedule if needed
            if (newSchedule != schedule) {
                // Cancels current execution service (NOT any currently running job!)
                cancel(false)
                // Changes the schedule
                schedule = newSchedule
                // Reschedules
                createSchedule()
            }
            // Replacing the job itself
            job = newJob
        }

        val jobKey: JobKey = job.key

        private val run: Runnable
            get() {
                val jobRunListener = DefaultJobRunListener()
                val rootTask = { job.task.run(jobRunListener) }
                val decoratedTask = jobDecorator.decorate(job, rootTask)
                val runnable = MonitoredRun(decoratedTask, object : MonitoredRunListenerAdapter() {
                    override fun onCompletion() {
                        logger.debug("[job][task]{} Removed job execution", job.key)
                        currentExecution.set(null)
                    }
                })
                val monitoredRunListener = object : MonitoredRunListener {
                    override fun onStart() {
                        logger.debug("[job][task]{} On start", job.key)
                        lastRunDate.set(Time.now())
                        runCount.incrementAndGet()
                        jobListener.onJobStart(job.key)
                    }

                    override fun onSuccess(duration: Long) {
                        lastRunDurationMs.set(duration)
                        logger.debug("[job][task]{} Success in {} ms", job.key, duration)
                        jobListener.onJobEnd(job.key, duration)
                        lastErrorCount.set(0)
                        lastError.set(null)
                    }

                    override fun onFailure(ex: Exception) {
                        lastErrorCount.incrementAndGet()
                        lastError.set(ex.message)
                        logger.debug("[job][task]{} Failure: {}", job.key, ex.message)
                        jobListener.onJobError(jobStatus, ex)
                    }

                    override fun onCompletion() {
                        runProgress.set(null)
                        logger.debug("[job][task]{} Job completed.", job.key)
                        jobListener.onJobComplete(job.key)
                    }
                }
                return MonitoredRun(runnable, monitoredRunListener)
            }

        val jobStatus: JobStatus
            get() {
                val valid = job.isValid
                return JobStatus(
                        id,
                        job.key,
                        schedule,
                        actualSchedule,
                        job.description,
                        currentExecution.get() != null,
                        valid,
                        paused.get(),
                        job.isDisabled,
                        runProgress.get(),
                        runCount.get(),
                        lastRunDate.get(),
                        lastRunDurationMs.get(),
                        getNextRunDate(valid),
                        lastErrorCount.get(),
                        lastError.get()
                )
            }

        override fun run() {
            if (!schedulerPaused.get()) {
                doRun(false)
            }
        }

        fun doRun(force: Boolean): Optional> {
            logger.debug("[job][run]{} Trying to run now - forced = {}", job.key, force)
            if (job.isValid) {
                if (job.isDisabled) {
                    logger.debug("[job][run]{} Not allowed to run now because disabled", job.key)
                    return Optional.empty()
                } else if (paused.get() && !force) {
                    logger.debug("[job][run]{} Not allowed to run now because paused", job.key)
                    return Optional.empty()
                } else if (currentExecution.get() != null) {
                    logger.debug("[job][run]{} Not allowed to run now because already running", job.key)
                    return Optional.empty()
                } else {
                    // Task to run
                    val run = run
                    // Gets the executor for this job
                    val executor = getExecutorService(job)
                    // Scheduling
                    logger.debug("[job][run]{} Job task submitted asynchronously", job.key)
                    val execution = executor.submit(run)
                    currentExecution.set(execution)
                    return Optional.of(execution)
                }
            } else {
                logger.debug("[job][run]{} Not valid - removing from schedule", job.key)
                unschedule(job.key, false)
                return Optional.empty()
            }
        }

        fun stop(): Boolean {
            logger.debug("[job]{} Stopping job", job.key)
            return currentExecution.updateAndGet { current ->
                current?.cancel(true)
                null
            } == null
        }

        fun cancel(forceStop: Boolean): Boolean {
            logger.debug("[job]{} Cancelling job (forcing = {})", job.key, forceStop)
            if (forceStop) {
                stop()
            }
            return scheduledFuture?.cancel(forceStop) ?: false
        }

        private fun getNextRunDate(valid: Boolean): LocalDateTime? {
            return if (valid) {
                scheduledFuture
                        ?.getDelay(TimeUnit.SECONDS)
                        ?.let { Time.now().plus(it, ChronoUnit.SECONDS) }
            } else {
                null
            }
        }

        fun pause() {
            if (scheduledFuture != null) {
                paused.set(true)
                jobListener.onJobPaused(job.key)
            }
        }

        fun resume() {
            if (scheduledFuture != null) {
                paused.set(false)
                jobListener.onJobResumed(job.key)
            }
        }

        private inner class DefaultJobRunListener : JobRunListener {

            override fun progress(progress: JobRunProgress) {
                jobListener.onJobProgress(job.key, progress)
                logger.debug("[job][progress]{} {}",
                        job.key,
                        progress.text
                )
                runProgress.set(progress)
            }

        }

    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy