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

dev.forkhandles.time.DeterministicScheduler.kt Maven / Gradle / Ivy

package dev.forkhandles.time

import dev.forkhandles.time.executors.SimpleScheduler
import java.time.Duration
import java.time.Instant
import java.util.*
import java.util.concurrent.Callable
import java.util.concurrent.CancellationException
import java.util.concurrent.Delayed
import java.util.concurrent.ExecutionException
import java.util.concurrent.Future
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException

/**
 * A [ScheduledExecutorService] that executes commands on the thread that calls
 * [runNextPendingCommand][.runNextPendingCommand], [runUntilIdle][.runUntilIdle] or
 * [tick][.tick].  Objects of this class can also be used as
 * [java.util.concurrent.Executor]s or [java.util.concurrent.ExecutorService]s if
 * you just want to control background execution and don't need to schedule commands.
 */
class DeterministicScheduler(startTime: Instant = Instant.now()) : ScheduledExecutorService, SimpleScheduler {

    private var clock = startTime
    private var isShutdown = false
    private var tasks = emptyTaskList()

    fun currentTime() = clock

    private fun emptyTaskList(): Queue> =
        PriorityQueue(Comparator.comparingLong { tasks -> tasks.getDelay(TimeUnit.MILLISECONDS) })


    override fun submit(task: Runnable): Future<*> {
        return schedule(task, Duration.ZERO)
    }

    override fun  submit(task: Callable): Future {
        return schedule(task, Duration.ZERO)
    }

    override fun isShutdown(): Boolean = isShutdown

    override fun  schedule(callable: Callable, delay: Duration): ScheduledFuture =
        enqueue(SimpleScheduleTask(callable, clock + delay))

    private fun  enqueue(task: SimpleScheduleTask): ScheduledFuture {
        if (!isShutdown) tasks.add(task)
        return task
    }

    override fun schedule(runnable: Runnable, delay: Duration): ScheduledFuture<*> =
        enqueue(
            SimpleScheduleTask(
                {
                    runnable.run()
                    null
                },
                clock + delay
            )
        )

    override fun scheduleWithFixedDelay(
        runnable: Runnable,
        initialDelay: Duration,
        delay: Duration
    ): ScheduledFuture<*> = enqueue(
        SimpleScheduleTask(
            { runnable.run() },
            delay,
            clock + initialDelay
        )
    )

    override fun scheduleAtFixedRate(runnable: Runnable, initialDelay: Duration, period: Duration): ScheduledFuture<*> =
        enqueue(
            SimpleScheduleTask(
                { runnable.run() },
                period,
                clock + initialDelay
            )
        )

    override fun shutdown() {
        isShutdown = true
    }

    fun clear() {
        tasks.clear()
    }

    fun isIdle(): Boolean {
        return tasks.size == 0 || tasks.peek().timeToRun > clock
    }

    fun runUntilIdle() {
        tick(Duration.ZERO)
    }

    fun tick(quantity: Long, unit: TimeUnit) {
        tick(asDuration(quantity, unit))
    }

    fun tick(duration: Duration) {

        val endOfPeriod = clock + duration

        while (true) {
            if (!runNextTask(endOfPeriod)) break
        }
        clock = endOfPeriod
    }

    private fun runNextTask(endOfPeriod: Instant): Boolean {

        val nextTasks = emptyTaskList()
        var ranSomething = false
        var execute = true
        val currentTasks = tasks.toList()

        tasks.clear()

        for (task in currentTasks) {
            if (task.isCancelled) continue
            val executionTimeOfTask = task.timeToRun
            if (execute && executionTimeOfTask <= endOfPeriod) {
                clock = executionTimeOfTask
                ranSomething = true
                val success = task.execute()
                if (task.isPeriodic && success && !task.isCancelled) {
                    nextTasks.add(task.atNextExecutionTimeAfter(executionTimeOfTask))
                }

                if ( tasks.size > 0 ) {
                    // if a task added another task, then we need to drop out
                    // so that the added task runs in the correct order
                    execute = false
                }

            } else {
                nextTasks.add(task)
            }
        }
        tasks.addAll(nextTasks)
        return ranSomething
    }

    override fun  submit(task: Runnable, result: T): Future {
        return schedule(Callable { task.run(); result }, Duration.ZERO)
    }

    override fun schedule(command: Runnable, delay: Long, unit: TimeUnit): ScheduledFuture<*> {
        return schedule(command, asDuration(delay, unit))
    }

    override fun  schedule(callable: Callable, delay: Long, unit: TimeUnit): ScheduledFuture {
        return schedule(callable, asDuration(delay, unit))
    }

    private fun asDuration(delay: Long, unit: TimeUnit) =
        Duration.ofMillis(TimeUnit.MILLISECONDS.convert(delay, unit))

    override fun scheduleWithFixedDelay(
        command: Runnable,
        initialDelay: Long,
        delay: Long,
        unit: TimeUnit
    ): ScheduledFuture<*> =
        scheduleWithFixedDelay(
            command,
            initialDelay = asDuration(initialDelay, unit),
            delay = asDuration(delay, unit)
        )

    override fun scheduleAtFixedRate(
        command: Runnable,
        initialDelay: Long,
        period: Long,
        unit: TimeUnit
    ): ScheduledFuture<*> =
        scheduleAtFixedRate(
            command,
            initialDelay = asDuration(initialDelay, unit),
            period = asDuration(period, unit)
        )

    override fun execute(command: Runnable) {
        submit(command)
    }

    override fun shutdownNow(): List {
        shutdown()
        return listOf()
    }

    override fun isTerminated(): Boolean {
        return isShutdown
    }

    override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean {
        if (isShutdown) {
            return true
        }
        blockingOperationsNotSupported()
    }

    override fun  invokeAll(tasks: MutableCollection>): MutableList> =
        blockingOperationsNotSupported()

    override fun  invokeAll(
        tasks: MutableCollection>,
        timeout: Long,
        unit: TimeUnit
    ): MutableList> =
        blockingOperationsNotSupported()

    override fun  invokeAny(tasks: MutableCollection>): T = blockingOperationsNotSupported()

    override fun  invokeAny(tasks: MutableCollection>, timeout: Long, unit: TimeUnit): T =
        blockingOperationsNotSupported()

    private inner class SimpleScheduleTask(
        private val callable: Callable,
        private val period: Duration?,
        val timeToRun: Instant
    ) :
        ScheduledFuture {
        private var isCancelled = false
        private var isDone = false
        private var result: T? = null
        private var error: Throwable? = null

        init {
            period?.run { if (this <= Duration.ZERO) throw IllegalArgumentException("period/rate must be > 0") }
        }

        val isPeriodic: Boolean
            get() = period != null

        constructor(callable: Callable, timeToRun: Instant) : this(callable, null, timeToRun) {}

        override fun getDelay(unit: TimeUnit): Long =
            unit.convert(timeToRun.toEpochMilli() - clock.toEpochMilli(), TimeUnit.MILLISECONDS)

        override fun compareTo(other: Delayed): Int {
            throw UnsupportedOperationException("james didn't write")
        }

        override fun isDone(): Boolean = isDone || isCancelled

        override fun isCancelled(): Boolean = isCancelled

        override fun cancel(mayInterruptIfRunning: Boolean): Boolean {
            if (isDone) {
                return false
            }
            isCancelled = true
            return true
        }

        @Throws(InterruptedException::class, ExecutionException::class)
        override fun get(): T? {
            if (isCancelled) throw CancellationException("get() on cancelled task")
            if (error != null) throw ExecutionException(error)
            if (isDone) return result
            throw UnsupportedSynchronousOperationException(
                "task not scheduled to run for another " + Duration.ofMillis(
                    timeToRun.toEpochMilli() - clock.toEpochMilli()
                )
            )
        }

        @Throws(InterruptedException::class, ExecutionException::class, TimeoutException::class)
        override fun get(timeout: Long, unit: TimeUnit): T? {
            return get()
        }

        fun execute(): Boolean {
            return try {
                if (!isCancelled) {
                    result = callable.call()
                }
                true
            } catch (e: Exception) {
                error = e
                false
            } finally {
                isDone = true
            }
        }

        fun atNextExecutionTimeAfter(clock: Instant): SimpleScheduleTask {
            return SimpleScheduleTask(
                callable, period, clock + period!!
            )
        }
    }

    private fun blockingOperationsNotSupported(): Nothing =
        throw UnsupportedSynchronousOperationException(
            "cannot perform blocking wait on a task scheduled on a " + javaClass.simpleName
        )
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy