Skip to content

Commit

Permalink
feature: Better coroutine exception handling
Browse files Browse the repository at this point in the history
  • Loading branch information
kezz committed Sep 20, 2024
1 parent 045ec59 commit fac5982
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -1,20 +1,43 @@
package com.noxcrew.interfaces

import com.noxcrew.interfaces.utilities.InterfacesCoroutineDetails
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import org.bukkit.Bukkit
import org.slf4j.LoggerFactory
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger

/** Holds the shared scope used for any interfaces coroutines. */
public object InterfacesConstants {

private val EXCEPTION_LOGGER = LoggerFactory.getLogger("InterfacesExceptionHandler")

/** The [CoroutineScope] for any suspending operations performed by interfaces. */
public val SCOPE: CoroutineScope = CoroutineScope(
CoroutineName("interfaces") +
SupervisorJob() +
CoroutineExceptionHandler { context, exception ->
val details = context[InterfacesCoroutineDetails]

if (details == null) {
EXCEPTION_LOGGER.error("An unknown error occurred in a coroutine!", exception)
} else {
val (player, reason) = details
EXCEPTION_LOGGER.error(
"""
An unknown error occurred in a coroutine!
- Player: ${player ?: "N/A"} (${player?.let(Bukkit::getPlayer)?.name ?: "offline"})
- Launch reason: $reason
""".trimIndent(),
exception
)
}
} +
run {
val threadNumber = AtomicInteger()
val factory = { runnable: Runnable ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.noxcrew.interfaces.click.ClickHandler
import com.noxcrew.interfaces.click.CompletableClickHandler
import com.noxcrew.interfaces.grid.GridPoint
import com.noxcrew.interfaces.pane.PlayerPane
import com.noxcrew.interfaces.utilities.InterfacesCoroutineDetails
import com.noxcrew.interfaces.view.AbstractInterfaceView
import com.noxcrew.interfaces.view.ChestInterfaceView
import com.noxcrew.interfaces.view.InterfaceView
Expand Down Expand Up @@ -166,7 +167,7 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
/** Re-opens the current background interface of [player]. */
public fun reopenInventory(player: Player) {
getBackgroundPlayerInterface(player.uniqueId)?.also {
SCOPE.launch {
SCOPE.launch(InterfacesCoroutineDetails(player.uniqueId, "reopening background interface")) {
it.open()
}
}
Expand Down Expand Up @@ -272,7 +273,7 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
// Saves any persistent items stored in the given inventory before we close it
view.savePersistentItems(event.inventory)

SCOPE.launch {
SCOPE.launch(InterfacesCoroutineDetails(event.player.uniqueId, "handling inventory close")) {
// Determine if we can re-open a previous interface
val backgroundInterface = getBackgroundPlayerInterface(event.player.uniqueId)
val shouldReopen = reason in REOPEN_REASONS && !event.player.isDead && backgroundInterface != null
Expand Down Expand Up @@ -542,7 +543,7 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
queries.invalidate(player.uniqueId)

// Complete the query and re-open the view
SCOPE.launch {
SCOPE.launch(InterfacesCoroutineDetails(event.player.uniqueId, "completing chat query")) {
if (query.onComplete(event.message())) {
query.view.open()
}
Expand Down Expand Up @@ -726,7 +727,7 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)

// Remove the query, run the cancel handler, and re-open the view
queries.invalidate(playerId)
SCOPE.launch {
SCOPE.launch(InterfacesCoroutineDetails(playerId, "cancelling chat query due to timeout")) {
onCancel()
view.open()
}
Expand All @@ -743,7 +744,7 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
if (view != null && query.view != view) return
queries.invalidate(playerId)

SCOPE.launch {
SCOPE.launch(InterfacesCoroutineDetails(playerId, "aborting chat query")) {
// Run the cancellation handler
query.onCancel()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public data class ChainGridPositionGenerator(
/** The first generator. */
private val first: GridPositionGenerator,
/** The second generator. */
private val second: GridPositionGenerator,
private val second: GridPositionGenerator
) : GridPositionGenerator {

public companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.noxcrew.interfaces.utilities

import java.util.UUID
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext

/** Context element that contains details used for error handling and debugging. */
internal data class InterfacesCoroutineDetails(
internal val player: UUID?,
internal val reason: String
) : AbstractCoroutineContextElement(InterfacesCoroutineDetails) {
/** Key this element. */
internal companion object : CoroutineContext.Key<InterfacesCoroutineDetails>
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.noxcrew.interfaces.pane.complete
import com.noxcrew.interfaces.properties.Trigger
import com.noxcrew.interfaces.transform.AppliedTransform
import com.noxcrew.interfaces.utilities.CollapsablePaneMap
import com.noxcrew.interfaces.utilities.InterfacesCoroutineDetails
import com.noxcrew.interfaces.utilities.forEachInGrid
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
Expand Down Expand Up @@ -240,7 +241,7 @@ public abstract class AbstractInterfaceView<I : InterfacesInventory, T : Interfa
// Ignore if the transforms are empty
if (transforms.isEmpty()) {
// If there are no transforms we still need to open it!
SCOPE.launch {
SCOPE.launch(InterfacesCoroutineDetails(player.uniqueId, "triggering re-render with no transforms")) {
triggerRerender()
}
return true
Expand All @@ -250,13 +251,15 @@ public abstract class AbstractInterfaceView<I : InterfacesInventory, T : Interfa
pendingTransforms.addAll(transforms)

// Check if the job is already running
SCOPE.launch {
SCOPE.launch(InterfacesCoroutineDetails(player.uniqueId, "triggering re-render with transforms")) {
try {
transformMutex.lock()

// Start the job if it's not running currently!
if (transformingJob == null || transformingJob?.isCompleted == true) {
transformingJob = SCOPE.async {
transformingJob = SCOPE.async(
InterfacesCoroutineDetails(player.uniqueId, "running and applying a transform")
) {
// Go through all pending transforms one at a time until
// we're fully done with all of them. Other threads may
// add additional ones as we go through the queue.
Expand Down

0 comments on commit fac5982

Please sign in to comment.