Even better event execution scheduling when eventloop is overloaded

This commit is contained in:
DBotThePony 2024-10-31 16:30:00 +07:00
parent 09f7e8ac70
commit 40d306544f
Signed by: DBot
GPG Key ID: DCC23B5715498507

View File

@ -27,7 +27,10 @@ import kotlin.math.absoluteValue
// if you try to use them by yourself :( // if you try to use them by yourself :(
// so I made my own // so I made my own
open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorService { open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorService {
private class ScheduledTask<T>(callable: Callable<T>, var executeAt: Long, val repeat: Boolean, val timeDelay: Long, val isFixedDelay: Boolean) : FutureTask<T>(callable), ScheduledFuture<T> { private class ScheduledTask<T>(callable: Callable<T>, var executeAt: Long, val repeat: Boolean, val timeDelay: Long, val isFixedDelay: Boolean, generation: Long) : FutureTask<T>(callable), ScheduledFuture<T> {
var generation = generation
private set
override fun compareTo(other: Delayed): Int { override fun compareTo(other: Delayed): Int {
if (other is ScheduledTask<*>) if (other is ScheduledTask<*>)
return executeAt.compareTo(other.executeAt) return executeAt.compareTo(other.executeAt)
@ -43,14 +46,14 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
return executeAt <= System.nanoTime() return executeAt <= System.nanoTime()
} }
fun shouldEnqueue(isShutdown: Boolean): Boolean { fun shouldEnqueue(isShutdown: Boolean, generation: Long): Boolean {
if (isShutdown || executeAt <= System.nanoTime()) if (isShutdown || executeAt <= System.nanoTime())
return perform(isShutdown) return perform(isShutdown, generation)
return true return true
} }
fun perform(isShutdown: Boolean): Boolean { fun perform(isShutdown: Boolean, generation: Long): Boolean {
if (repeat) { if (repeat) {
if (isFixedDelay) { if (isFixedDelay) {
// fixed delay // fixed delay
@ -64,6 +67,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
val result = runAndReset() val result = runAndReset()
executeAt = System.nanoTime() + timeDelay executeAt = System.nanoTime() + timeDelay
this.generation = generation
return result && !isShutdown return result && !isShutdown
} else { } else {
// fixed rate // fixed rate
@ -80,6 +84,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
val result = runAndReset() val result = runAndReset()
val now = System.nanoTime() val now = System.nanoTime()
executeAt = now + timeDelay + deadlineMargin - (now - timeBefore) executeAt = now + timeDelay + deadlineMargin - (now - timeBefore)
this.generation = generation
return result && !isShutdown return result && !isShutdown
} }
} else { } else {
@ -91,6 +96,8 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
private class TaskPair<T>(val future: CompletableFuture<T>, var supplier: Callable<T>?) private class TaskPair<T>(val future: CompletableFuture<T>, var supplier: Callable<T>?)
private var generation = 0L
private var extraBudget = 0L
private val eventQueue = ConcurrentLinkedQueue<TaskPair<*>>() private val eventQueue = ConcurrentLinkedQueue<TaskPair<*>>()
private val scheduledQueue = PriorityQueue<ScheduledTask<*>>() private val scheduledQueue = PriorityQueue<ScheduledTask<*>>()
val coroutines = asCoroutineDispatcher() val coroutines = asCoroutineDispatcher()
@ -119,10 +126,12 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
private fun eventLoopIteration(): Boolean { private fun eventLoopIteration(): Boolean {
var executedAnything = false var executedAnything = false
// poll queued event first, so we guarantee at least one is executed even if main event loop is very busy
var next = eventQueue.poll() var next = eventQueue.poll()
while (next != null) { while (next != null) {
executedAnything = true executedAnything = true
val time = System.nanoTime()
try { try {
val callable = next.supplier val callable = next.supplier
@ -140,26 +149,49 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
} }
} }
// keep executing queued tasks until we hit scheduled task deadline if (extraBudget > 0L) {
if (scheduledQueue.isEmpty() || !scheduledQueue.peek()!!.shouldExecuteNow()) // event loop is overloaded, keep executing queued tasks until we empty execution time budget
extraBudget -= System.nanoTime() - time
next = eventQueue.poll()
} else if (scheduledQueue.isEmpty() || !scheduledQueue.peek()!!.shouldExecuteNow())
// keep executing queued tasks until we hit scheduled task deadline
next = eventQueue.poll() next = eventQueue.poll()
else else
next = null next = null
} }
if (scheduledQueue.isNotEmpty()) { if (scheduledQueue.isNotEmpty()) {
val time = System.nanoTime()
val executed = ObjectArrayList<ScheduledTask<*>>(4) val executed = ObjectArrayList<ScheduledTask<*>>(4)
// first, poll all due events into local queue, and prioritize events by FIFO strategy based on how long ago they were scheduled
val queue = PriorityQueue<ScheduledTask<*>>(Comparator { o1, o2 -> o1.generation.compareTo(o2.generation) })
while (scheduledQueue.isNotEmpty() && (isShutdown || scheduledQueue.peek()!!.shouldExecuteNow())) { while (scheduledQueue.isNotEmpty() && (isShutdown || scheduledQueue.peek()!!.shouldExecuteNow())) {
executedAnything = true queue.add(scheduledQueue.poll()!!)
val poll = scheduledQueue.poll()!! }
if (poll.perform(isShutdown)) { executedAnything = executedAnything || queue.isNotEmpty()
// then, execute collected events in proper order
while (queue.isNotEmpty()) {
val poll = queue.poll()!!
if (poll.perform(isShutdown, generation++)) {
executed.add(poll) executed.add(poll)
} }
} }
scheduledQueue.addAll(executed) scheduledQueue.addAll(executed)
if (scheduledQueue.isNotEmpty() && !isShutdown && scheduledQueue.peek()!!.shouldExecuteNow()) {
// event loop is overloaded, interleave queued events with scheduled ones
extraBudget = System.nanoTime() - time
} else {
// no overload, normal queued events execution
extraBudget = 0L
}
} else {
extraBudget = 0L
} }
return executedAnything return executedAnything
@ -422,10 +454,10 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
} }
final override fun schedule(command: Runnable, delay: Long, unit: TimeUnit): ScheduledFuture<*> { final override fun schedule(command: Runnable, delay: Long, unit: TimeUnit): ScheduledFuture<*> {
val task = ScheduledTask({ command.run() }, System.nanoTime() + TimeUnit.NANOSECONDS.convert(delay, unit), false, 0L, false) val task = ScheduledTask({ command.run() }, System.nanoTime() + TimeUnit.NANOSECONDS.convert(delay, unit), false, 0L, false, generation++)
execute { execute {
if (task.shouldEnqueue(isShutdown)) if (task.shouldEnqueue(isShutdown, generation++))
scheduledQueue.add(task) scheduledQueue.add(task)
} }
@ -433,10 +465,10 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
} }
final override fun <V> schedule(callable: Callable<V>, delay: Long, unit: TimeUnit): ScheduledFuture<V> { final override fun <V> schedule(callable: Callable<V>, delay: Long, unit: TimeUnit): ScheduledFuture<V> {
val task = ScheduledTask(callable, System.nanoTime() + TimeUnit.NANOSECONDS.convert(delay, unit), false, 0L, false) val task = ScheduledTask(callable, System.nanoTime() + TimeUnit.NANOSECONDS.convert(delay, unit), false, 0L, false, generation++)
execute { execute {
if (task.shouldEnqueue(isShutdown)) if (task.shouldEnqueue(isShutdown, generation++))
scheduledQueue.add(task) scheduledQueue.add(task)
} }
@ -449,10 +481,10 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
period: Long, period: Long,
unit: TimeUnit unit: TimeUnit
): ScheduledFuture<*> { ): ScheduledFuture<*> {
val task = ScheduledTask({ command.run() }, System.nanoTime() + TimeUnit.NANOSECONDS.convert(initialDelay, unit), true, TimeUnit.NANOSECONDS.convert(period, unit), false) val task = ScheduledTask({ command.run() }, System.nanoTime() + TimeUnit.NANOSECONDS.convert(initialDelay, unit), true, TimeUnit.NANOSECONDS.convert(period, unit), false, generation++)
execute { execute {
if (task.shouldEnqueue(isShutdown)) if (task.shouldEnqueue(isShutdown, generation++))
scheduledQueue.add(task) scheduledQueue.add(task)
} }
@ -465,10 +497,10 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
delay: Long, delay: Long,
unit: TimeUnit unit: TimeUnit
): ScheduledFuture<*> { ): ScheduledFuture<*> {
val task = ScheduledTask({ command.run() }, System.nanoTime() + TimeUnit.NANOSECONDS.convert(initialDelay, unit), true, TimeUnit.NANOSECONDS.convert(delay, unit), true) val task = ScheduledTask({ command.run() }, System.nanoTime() + TimeUnit.NANOSECONDS.convert(initialDelay, unit), true, TimeUnit.NANOSECONDS.convert(delay, unit), true, generation++)
execute { execute {
if (task.shouldEnqueue(isShutdown)) if (task.shouldEnqueue(isShutdown, generation++))
scheduledQueue.add(task) scheduledQueue.add(task)
} }