Skip to content

Commit fac5982

Browse files
committed
feature: Better coroutine exception handling
1 parent 045ec59 commit fac5982

File tree

5 files changed

+50
-9
lines changed

5 files changed

+50
-9
lines changed

interfaces/src/main/kotlin/com/noxcrew/interfaces/InterfacesConstants.kt

+23
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,43 @@
11
package com.noxcrew.interfaces
22

3+
import com.noxcrew.interfaces.utilities.InterfacesCoroutineDetails
4+
import kotlinx.coroutines.CoroutineExceptionHandler
35
import kotlinx.coroutines.CoroutineName
46
import kotlinx.coroutines.CoroutineScope
57
import kotlinx.coroutines.Dispatchers
68
import kotlinx.coroutines.SupervisorJob
79
import kotlinx.coroutines.asCoroutineDispatcher
10+
import org.bukkit.Bukkit
11+
import org.slf4j.LoggerFactory
812
import java.util.concurrent.Executors
913
import java.util.concurrent.atomic.AtomicInteger
1014

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

18+
private val EXCEPTION_LOGGER = LoggerFactory.getLogger("InterfacesExceptionHandler")
19+
1420
/** The [CoroutineScope] for any suspending operations performed by interfaces. */
1521
public val SCOPE: CoroutineScope = CoroutineScope(
1622
CoroutineName("interfaces") +
1723
SupervisorJob() +
24+
CoroutineExceptionHandler { context, exception ->
25+
val details = context[InterfacesCoroutineDetails]
26+
27+
if (details == null) {
28+
EXCEPTION_LOGGER.error("An unknown error occurred in a coroutine!", exception)
29+
} else {
30+
val (player, reason) = details
31+
EXCEPTION_LOGGER.error(
32+
"""
33+
An unknown error occurred in a coroutine!
34+
- Player: ${player ?: "N/A"} (${player?.let(Bukkit::getPlayer)?.name ?: "offline"})
35+
- Launch reason: $reason
36+
""".trimIndent(),
37+
exception
38+
)
39+
}
40+
} +
1841
run {
1942
val threadNumber = AtomicInteger()
2043
val factory = { runnable: Runnable ->

interfaces/src/main/kotlin/com/noxcrew/interfaces/InterfacesListeners.kt

+6-5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.noxcrew.interfaces.click.ClickHandler
1010
import com.noxcrew.interfaces.click.CompletableClickHandler
1111
import com.noxcrew.interfaces.grid.GridPoint
1212
import com.noxcrew.interfaces.pane.PlayerPane
13+
import com.noxcrew.interfaces.utilities.InterfacesCoroutineDetails
1314
import com.noxcrew.interfaces.view.AbstractInterfaceView
1415
import com.noxcrew.interfaces.view.ChestInterfaceView
1516
import com.noxcrew.interfaces.view.InterfaceView
@@ -166,7 +167,7 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
166167
/** Re-opens the current background interface of [player]. */
167168
public fun reopenInventory(player: Player) {
168169
getBackgroundPlayerInterface(player.uniqueId)?.also {
169-
SCOPE.launch {
170+
SCOPE.launch(InterfacesCoroutineDetails(player.uniqueId, "reopening background interface")) {
170171
it.open()
171172
}
172173
}
@@ -272,7 +273,7 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
272273
// Saves any persistent items stored in the given inventory before we close it
273274
view.savePersistentItems(event.inventory)
274275

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

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

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

746-
SCOPE.launch {
747+
SCOPE.launch(InterfacesCoroutineDetails(playerId, "aborting chat query")) {
747748
// Run the cancellation handler
748749
query.onCancel()
749750

interfaces/src/main/kotlin/com/noxcrew/interfaces/grid/ChainGridPositionGenerator.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ public data class ChainGridPositionGenerator(
55
/** The first generator. */
66
private val first: GridPositionGenerator,
77
/** The second generator. */
8-
private val second: GridPositionGenerator,
8+
private val second: GridPositionGenerator
99
) : GridPositionGenerator {
1010

1111
public companion object {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.noxcrew.interfaces.utilities
2+
3+
import java.util.UUID
4+
import kotlin.coroutines.AbstractCoroutineContextElement
5+
import kotlin.coroutines.CoroutineContext
6+
7+
/** Context element that contains details used for error handling and debugging. */
8+
internal data class InterfacesCoroutineDetails(
9+
internal val player: UUID?,
10+
internal val reason: String
11+
) : AbstractCoroutineContextElement(InterfacesCoroutineDetails) {
12+
/** Key this element. */
13+
internal companion object : CoroutineContext.Key<InterfacesCoroutineDetails>
14+
}

interfaces/src/main/kotlin/com/noxcrew/interfaces/view/AbstractInterfaceView.kt

+6-3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import com.noxcrew.interfaces.pane.complete
1414
import com.noxcrew.interfaces.properties.Trigger
1515
import com.noxcrew.interfaces.transform.AppliedTransform
1616
import com.noxcrew.interfaces.utilities.CollapsablePaneMap
17+
import com.noxcrew.interfaces.utilities.InterfacesCoroutineDetails
1718
import com.noxcrew.interfaces.utilities.forEachInGrid
1819
import kotlinx.coroutines.Job
1920
import kotlinx.coroutines.async
@@ -240,7 +241,7 @@ public abstract class AbstractInterfaceView<I : InterfacesInventory, T : Interfa
240241
// Ignore if the transforms are empty
241242
if (transforms.isEmpty()) {
242243
// If there are no transforms we still need to open it!
243-
SCOPE.launch {
244+
SCOPE.launch(InterfacesCoroutineDetails(player.uniqueId, "triggering re-render with no transforms")) {
244245
triggerRerender()
245246
}
246247
return true
@@ -250,13 +251,15 @@ public abstract class AbstractInterfaceView<I : InterfacesInventory, T : Interfa
250251
pendingTransforms.addAll(transforms)
251252

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

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

0 commit comments

Comments
 (0)