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

jvmMain.zakadabar.lib.schedule.business.Dispatcher.kt Maven / Gradle / Ivy

There is a newer version: 2023.4.20
Show newest version
/*
 * Copyright © 2020-2021, Simplexion, Hungary and contributors. Use of this source code is governed by the Apache 2.0 license.
 */
package zakadabar.lib.schedule.business

import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
import zakadabar.core.data.EntityId
import zakadabar.core.util.UUID
import zakadabar.core.util.fork
import zakadabar.lib.schedule.data.Job
import zakadabar.lib.schedule.data.PushJob
import zakadabar.lib.schedule.data.Subscription
import kotlin.math.absoluteValue

class Dispatcher(
    val jobBl: JobBl
) {
    val events = Channel(UNLIMITED)

    var coroutine = fork { run() }

    @Serializable
    class JobEntry(
        val id: EntityId,
        val startAt: Instant?,
        val actionNamespace: String,
        val actionType: String,
        val actionData: String
    )

    @Serializable
    class SubscriptionEntry(
        val id: EntityId,
        val nodeUrl: String,
        val nodeId: UUID,
        var reuse: Boolean
    )

    @Serializable
    class RunEntry(
        val job: JobEntry,
        val subscription: SubscriptionEntry
    )

    var pendingJobs = mutableListOf()
    val runnableJobs = mutableListOf()
    val runningJobs = mutableListOf()
    val idleSubscriptions = mutableListOf()

    suspend fun run() {
        for (event in events) {
            jobBl.logger.debug("dispatcher event: $event")
            when (event) {
                is PendingCheckEvent -> onPendingCheck()
                is JobCreateEvent -> onJobCreate(event)
                is JobSuccessEvent -> onJobSuccess(event)
                is JobFailEvent -> onJobFail(event)
                is RequestJobCancelEvent -> onRequestJobCancel(event)
                is SubscriptionCreateEvent -> onSubscriptionCreate(event)
                is SubscriptionDeleteEvent -> onSubscriptionDelete(event)
                is PushFailEvent -> onPushFail(event)
            }
        }
    }

    /**
     * Add a job to [pendingJobs] or [runnableJobs], depending on
     * [Job.startAt]
     */
    fun onJobCreate(event: JobCreateEvent) {
        addJobEntry(event, event.actionData, event.startAt) // calls pushJobs
    }

    fun addJobEntry(event: JobEvent, actionData: String, startAt: Instant?) {
        val entry = JobEntry(
            event.jobId,
            startAt,
            event.actionNamespace,
            event.actionType,
            actionData
        )

        if (startAt == null || startAt <= Clock.System.now()) {
            runnableJobs.add(entry)
            pushJobs()
            return
        }

        val index = pendingJobs.binarySearch {
            it.startAt !!.compareTo(startAt)
        }.absoluteValue

        pendingJobs.add(index, entry)

        jobBl.addPending(this)

        pushJobs()
    }

    fun takeRunEntry(jobId: EntityId, reuse : Boolean = true): RunEntry? {
        val index = runningJobs.indexOfFirst { it.job.id == jobId }
        if (index == - 1) return null

        val entry = runningJobs.removeAt(index)
        if (reuse && entry.subscription.reuse) idleSubscriptions += entry.subscription

        return entry
    }

    fun onJobSuccess(event: JobSuccessEvent) {
        takeRunEntry(event.jobId)
        pushJobs()
    }

    fun onJobFail(event: JobFailEvent) {
        takeRunEntry(event.jobId)
        if (event.retryAt != null) {
            addJobEntry(event, event.actionData, event.retryAt) // calls pushJobs
        }
    }

    fun onRequestJobCancel(event: RequestJobCancelEvent) {
        val id = event.jobId

        pendingJobs.indexOfFirst { it.id == id }.takeIf { it != - 1 }?.also {
            pendingJobs.removeAt(it)
            fork { jobBl.jobCancel(id) }
            return
        }

        runnableJobs.indexOfFirst { it.id == id }.takeIf { it != - 1 }?.also {
            runnableJobs.removeAt(it)
            fork { jobBl.jobCancel(id) }
            return
        }

        // TODO send cancel request to the worker
    }

    fun onSubscriptionCreate(event: SubscriptionCreateEvent) {
        idleSubscriptions += SubscriptionEntry(
            id = event.subscriptionId,
            nodeUrl = event.nodeUrl,
            nodeId = event.nodeId,
            reuse = true
        )
        pushJobs()
    }

    fun onSubscriptionDelete(event: SubscriptionDeleteEvent) {
        if (idleSubscriptions.removeAll { it.id == event.subscriptionId }) return

        runningJobs
            .firstOrNull { it.subscription.id == event.subscriptionId }
            ?.subscription
            ?.reuse = false
    }

    /**
     * Move jobs from [pendingJobs] to [runnableJobs] when [Job.startAt] in the past.
     */
    fun onPendingCheck() {
        if (pendingJobs.isEmpty()) {
            jobBl.removePending(this)
            return
        }

        val now = Clock.System.now()

        val jobs = pendingJobs.takeWhile { entry ->
            entry.startAt?.let { it <= now } ?: true
        }

        if (jobs.isEmpty()) return

        pendingJobs = pendingJobs.drop(jobs.size).toMutableList()
        runnableJobs.addAll(jobs)

        pushJobs()
    }

    fun pushJobs() {
        while (idleSubscriptions.isNotEmpty() && runnableJobs.isNotEmpty()) {
            val entry = RunEntry(runnableJobs.removeAt(0), idleSubscriptions.removeAt(0))
            runningJobs += entry
            fork { pushJob(entry) }
        }
    }

    suspend fun pushJob(entry: RunEntry) {
        val job = entry.job
        val subscription = entry.subscription

        jobBl.assignNode(job.id, subscription.nodeId)

        try {
            PushJob(
                jobId = job.id,
                actionNamespace = job.actionNamespace,
                actionType = job.actionType,
                actionData = job.actionData
            ).execute(baseUrl = subscription.nodeUrl)
        } catch (ex: Exception) {
            jobBl.alarmSupport.create(ex)
            events.send(
                PushFailEvent(
                    jobId = job.id,
                    actionNamespace = job.actionNamespace,
                    actionType = job.actionType,
                    specific = false
                )
            )
            return
        }

    }

    fun onPushFail(event: PushFailEvent) {
        // do not reuse this subscription
        // FIXME what if the error is because of the job
        val entry = takeRunEntry(event.jobId, false) ?: return
        runnableJobs.add(0, entry.job)
        pushJobs()
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy