From 6f3ced2ed4a847c62840a0df519d8fa1ac428396 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:57:37 +0200 Subject: [PATCH 1/7] Implement sequence handling for tie-breaking in enqueue method; add QueueStartHook for server readiness tasks --- .idea/kotlinc.xml | 2 +- .idea/vcs.xml | 2 +- .../queue/common/queue/AbstractSurfQueue.kt | 16 ++- .../surf/queue/common/queue/RedisQueueKeys.kt | 2 +- .../common/queue/RedisQueueLockManager.kt | 21 +++- .../common/queue/RedisQueueScorePacker.kt | 105 +++++++++++++++--- .../queue/common/queue/RedisQueueStore.kt | 13 ++- .../dev/slne/surf/queue/paper/PaperMain.kt | 1 + .../queue/paper/PaperSurfQueueInstance.kt | 10 +- .../hook/startup/DefaultQueueStartHook.kt | 12 ++ .../paper/hook/startup/PolarQueueStartHook.kt | 10 ++ .../paper/hook/startup/QueueStartHook.kt | 25 +++++ .../velocity/listener/QueuePlayerListener.kt | 11 +- .../queue/velocity/queue/QueueTransfer.kt | 5 +- .../queue/velocity/queue/RedisQueueCleanup.kt | 19 +++- .../queue/RedisQueueTransferProcessor.kt | 66 ++++++++--- .../queue/velocity/queue/VelocitySurfQueue.kt | 2 + 17 files changed, 265 insertions(+), 57 deletions(-) create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/DefaultQueueStartHook.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/PolarQueueStartHook.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/QueueStartHook.kt diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index f203762..ebd2e7b 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -2,6 +2,6 @@ \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7..35eb1dd 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/AbstractSurfQueue.kt b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/AbstractSurfQueue.kt index b2a006d..8226abd 100644 --- a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/AbstractSurfQueue.kt +++ b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/AbstractSurfQueue.kt @@ -6,6 +6,7 @@ import dev.slne.surf.surfapi.core.api.util.logger import it.unimi.dsi.fastutil.objects.Object2IntMap import java.time.Instant import java.util.* +import java.util.concurrent.atomic.AtomicInteger abstract class AbstractSurfQueue(override val serverName: String) : SurfQueue { protected val keys = RedisQueueKeys(serverName) @@ -13,6 +14,14 @@ abstract class AbstractSurfQueue(override val serverName: String) : SurfQueue { protected val lockManager = RedisQueueLockManager(keys) protected val epochMs = store.initEpochMs() + /** + * Monotonically increasing sequence counter used to break ties when + * multiple players enqueue within the same millisecond. Wraps around + * at MAX_SEQUENCE; by the time it wraps, the millisecond will have + * advanced so no collision occurs. + */ + private val enqueueSequence = AtomicInteger(0) + companion object { private val log = logger() @@ -41,12 +50,15 @@ abstract class AbstractSurfQueue(override val serverName: String) : SurfQueue { override suspend fun enqueue(uuid: UUID, priority: Int): Boolean { val priorityFixed = fixPriority(uuid, priority) val now = Instant.now().toEpochMilli() + val sequence = enqueueSequence.getAndUpdate { current -> + if (current >= RedisQueueScorePacker.MAX_SEQUENCE.toInt()) 0 else current + 1 + } val score = RedisQueueScorePacker.pack( priorityFixed, now - epochMs, - 0 - ) // TODO: set sequence if it happens to enqueue multiple times in the same ms + sequence + ) val meta = QueueEntry(uuid, now, priorityFixed) diff --git a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueKeys.kt b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueKeys.kt index df413ec..e0568ce 100644 --- a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueKeys.kt +++ b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueKeys.kt @@ -15,7 +15,7 @@ data class RedisQueueKeys( val epochMsKey = "$QUEUE_PREFIX$serverName$EPOCH_MS_SUFFIX" companion object { - val QUEUE_PREFIX = RedisInstance.namespaced("queue:") + val QUEUE_PREFIX = RedisInstance.namespaced("queue:v2:") val EPOCH_MS_KEY_PATTERN = "$QUEUE_PREFIX*$EPOCH_MS_SUFFIX" const val EPOCH_MS_SUFFIX = ":epoch-ms" } diff --git a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueLockManager.kt b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueLockManager.kt index 3ff1135..35cd380 100644 --- a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueLockManager.kt +++ b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueLockManager.kt @@ -1,12 +1,17 @@ package dev.slne.surf.queue.common.queue import dev.slne.surf.queue.common.redis.redisApi +import dev.slne.surf.surfapi.core.api.util.logger import kotlinx.coroutines.future.await class RedisQueueLockManager(private val keys: RedisQueueKeys) { private val transferLock = redisApi.redisson.getLock(keys.transferLockKey) private val cleanupLock = redisApi.redisson.getLock(keys.cleanupLockKey) + companion object { + private val log = logger() + } + suspend fun withTransferLock( block: suspend (acquired: Boolean) -> T ): T { @@ -17,7 +22,13 @@ class RedisQueueLockManager(private val keys: RedisQueueKeys) { try { return block(true) } finally { - transferLock.unlockAsync(threadId).await() + try { + transferLock.unlockAsync(threadId).await() + } catch (e: Exception) { + log.atWarning() + .withCause(e) + .log("Failed to release transfer lock for %s", keys.serverName) + } } } @@ -29,7 +40,13 @@ class RedisQueueLockManager(private val keys: RedisQueueKeys) { try { block() } finally { - cleanupLock.unlockAsync(threadId).await() + try { + cleanupLock.unlockAsync(threadId).await() + } catch (e: Exception) { + log.atWarning() + .withCause(e) + .log("Failed to release cleanup lock for %s", keys.serverName) + } } } } \ No newline at end of file diff --git a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueScorePacker.kt b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueScorePacker.kt index bbc35e9..bc1b643 100644 --- a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueScorePacker.kt +++ b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueScorePacker.kt @@ -1,43 +1,114 @@ package dev.slne.surf.queue.common.queue +/** + * Packs queue metadata into a single 53-bit value stored as a Redis ZSET score (Double). + * + * The layout is designed to: + * - Preserve total ordering inside Redis sorted sets + * - Avoid precision loss (fits exactly into IEEE-754 double: 53 bits) + * - Support priority, timestamp, and tie-breaking sequence + * + * ## Bit layout: + * + * `priority:7`|`deltaMs:40`|`sequence:6` + * + * + * Total: 53 bits → exactly representable as Double + * + * ## Field details: + * + * - priority (7 bits) + * Higher priority should come first. + * To achieve this with ascending ZSET ordering, the value is inverted: + * `storedPriority = MAX_PRIORITY - priority` + * + * - deltaMs (40 bits) + * Time component (usually epoch delta in milliseconds). + * Provides the primary ordering. + * Range: ~34.8 years + * + * - sequence (6 bits) + * Tie-breaker for entries created within the same millisecond. + * Range: 0–63 (64 entries per millisecond). + * + * Ordering behavior in Redis ZSET: + * 1. Lower storedPriority → higher logical priority + * 2. Lower deltaMs → earlier timestamp + * 3. Lower sequence → earlier insertion within same millisecond + * + * Important: + * - The total bit size MUST NOT exceed 53 bits, otherwise precision loss occurs in Double. + * - All values must be within their defined ranges. + */ object RedisQueueScorePacker { private const val PRIORITY_BITS = 7 - private const val DELTA_MS_BITS = 42 - private const val SEQUENCE_BITS = 4 + private const val DELTA_MS_BITS = 40 + private const val SEQUENCE_BITS = 6 private const val DELTA_MS_SHIFT = SEQUENCE_BITS private const val PRIORITY_SHIFT = DELTA_MS_BITS + SEQUENCE_BITS - private const val SEQUENCE_MASK = (1L shl SEQUENCE_BITS) - 1 // 0xF - private const val DELTA_MS_MASK = (1L shl DELTA_MS_BITS) - 1 // 0x3FFFFFFFFFF + private const val SEQUENCE_MASK = (1L shl SEQUENCE_BITS) - 1 // 0x3F (63) + private const val DELTA_MS_MASK = (1L shl DELTA_MS_BITS) - 1 // 0xFFFFFFFFFF private const val PRIORITY_MASK = (1L shl PRIORITY_BITS) - 1 // 0x7F const val MAX_PRIORITY = PRIORITY_MASK.toInt() const val MAX_DELTA_MS = DELTA_MS_MASK - const val MAX_SEQUENCE = SEQUENCE_MASK + const val MAX_SEQUENCE = SEQUENCE_MASK.toInt() + /** + * Unpacks a Redis ZSET score back into its original components. + * + * @param score the packed Double value from Redis + * @return unpacked priority, deltaMs and sequence + */ fun unpack(score: Double): Unpacked { val value = score.toLong() - val priority = PRIORITY_MASK - ((value shr (DELTA_MS_BITS + SEQUENCE_BITS)) and PRIORITY_MASK).toInt() - val deltaMs = (value shr SEQUENCE_BITS) and DELTA_MS_MASK + + val storedPriority = ((value shr PRIORITY_SHIFT) and PRIORITY_MASK).toInt() + val priority = MAX_PRIORITY - storedPriority + val deltaMs = (value shr DELTA_MS_SHIFT) and DELTA_MS_MASK val sequence = (value and SEQUENCE_MASK).toInt() - return Unpacked(priority.toInt(), deltaMs, sequence) + + return Unpacked(priority, deltaMs, sequence) } + /** + * Packs priority, timestamp and sequence into a single Double score. + * + * @param priority logical priority (0..MAX_PRIORITY), higher = more important + * @param deltaMs time value (usually epoch delta in ms) + * @param sequence tie-breaker for same timestamp (0..MAX_SEQUENCE) + * + * @return packed score suitable for Redis ZSET + * + * @throws IllegalArgumentException if any value is out of range + */ fun pack(priority: Int, deltaMs: Long, sequence: Int): Double { - // Priority: invert for desired direction. - val invPriority = PRIORITY_MASK - (priority.toLong() and PRIORITY_MASK) + require(priority in 0..MAX_PRIORITY) { "priority out of range" } + require(deltaMs in 0..MAX_DELTA_MS) { "deltaMs out of range" } + require(sequence in 0..MAX_SEQUENCE) { "sequence out of range" } - require(invPriority in 0..PRIORITY_MASK) { "Priority bounds" } - require(deltaMs in 0..DELTA_MS_MASK) { "deltaMs out of range" } - require(sequence in 0..SEQUENCE_MASK) { "sequence out of range" } + val invertedPriority = (MAX_PRIORITY - priority).toLong() - val value = (invPriority shl (DELTA_MS_BITS + SEQUENCE_BITS)) or - ((deltaMs and DELTA_MS_MASK) shl SEQUENCE_BITS) or - (sequence.toLong() and SEQUENCE_MASK) + val value = + (invertedPriority shl PRIORITY_SHIFT) or + (deltaMs shl DELTA_MS_SHIFT) or + sequence.toLong() return value.toDouble() } - data class Unpacked(val priority: Int, val deltaMs: Long, val sequence: Int) + /** + * Result of unpacking a score. + * + * @property priority logical priority (higher = more important) + * @property deltaMs timestamp component + * @property sequence tie-breaker within same timestamp + */ + data class Unpacked( + val priority: Int, + val deltaMs: Long, + val sequence: Int + ) } \ No newline at end of file diff --git a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueStore.kt b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueStore.kt index e110f8e..e3a63f4 100644 --- a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueStore.kt +++ b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueStore.kt @@ -117,7 +117,18 @@ class RedisQueueStore(private val keys: RedisQueueKeys) { .removeAsync(uuid) batch.executeAsync().await() - return removeAsync.await() + try { + batch.executeAsync().await() + return removeAsync.await() + } catch (e: Exception) { + // If the batch fails, we need to remove the individual elements manually + runCatching { scoredSet.removeAsync(uuid).await() } + runCatching { metaMap.removeAsync(uuid).await() } + runCatching { lastSeenMap.removeAsync(uuid).await() } + runCatching { retryCountMap.removeAsync(uuid).await() } + + throw e + } } suspend fun deleteAll() { diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperMain.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperMain.kt index c6b21a8..499381b 100644 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperMain.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperMain.kt @@ -2,6 +2,7 @@ package dev.slne.surf.queue.paper import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin import dev.slne.surf.queue.common.SurfQueueInstance +import dev.slne.surf.surfapi.core.api.component.surfComponentApi import org.bukkit.plugin.java.JavaPlugin class PaperMain : SuspendingJavaPlugin() { diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperSurfQueueInstance.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperSurfQueueInstance.kt index 80db9ad..cbfba65 100644 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperSurfQueueInstance.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperSurfQueueInstance.kt @@ -3,11 +3,19 @@ package dev.slne.surf.queue.paper import com.google.auto.service.AutoService import dev.slne.surf.queue.common.SurfQueueInstance import dev.slne.surf.queue.common.queue.AbstractSurfQueue +import dev.slne.surf.queue.paper.hook.startup.QueueStartHook import dev.slne.surf.queue.paper.queue.PaperSurfQueue @AutoService(SurfQueueInstance::class) class PaperSurfQueueInstance : SurfQueueInstance() { - override val componentOwner get() = dev.slne.surf.queue.paper.plugin + override val componentOwner get() = plugin + + override suspend fun load() { + super.load() + QueueStartHook.get().onServerReady { + + } + } override fun createQueue(serverName: String): AbstractSurfQueue { return PaperSurfQueue(serverName) diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/DefaultQueueStartHook.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/DefaultQueueStartHook.kt new file mode 100644 index 0000000..cba0636 --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/DefaultQueueStartHook.kt @@ -0,0 +1,12 @@ +package dev.slne.surf.queue.paper.hook.startup + +import dev.slne.surf.surfapi.shared.api.component.ComponentMeta +import dev.slne.surf.surfapi.shared.api.component.requirement.ConditionalOnMissingComponent + +@ComponentMeta +@ConditionalOnMissingComponent(QueueStartHook::class) +class DefaultQueueStartHook : QueueStartHook() { + override suspend fun onEnable() { + runServerReadyTasks() + } +} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/PolarQueueStartHook.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/PolarQueueStartHook.kt new file mode 100644 index 0000000..aeb599d --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/PolarQueueStartHook.kt @@ -0,0 +1,10 @@ +package dev.slne.surf.queue.paper.hook.startup + +import dev.slne.surf.surfapi.shared.api.component.ComponentMeta + +@ComponentMeta +class PolarQueueStartHook : QueueStartHook() { + override suspend fun onEnable() { + + } +} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/QueueStartHook.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/QueueStartHook.kt new file mode 100644 index 0000000..c0ac566 --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/QueueStartHook.kt @@ -0,0 +1,25 @@ +package dev.slne.surf.queue.paper.hook.startup + +import dev.slne.surf.surfapi.core.api.component.AbstractComponent +import dev.slne.surf.surfapi.core.api.component.surfComponentApi +import java.util.concurrent.ConcurrentLinkedQueue + +abstract class QueueStartHook : AbstractComponent() { + private val serverReadyTasks = ConcurrentLinkedQueue<() -> Unit>() + + protected fun runServerReadyTasks() { + val iterator = serverReadyTasks.iterator() + while (iterator.hasNext()) { + iterator.next().invoke() + iterator.remove() + } + } + + fun onServerReady(block: () -> Unit) { + serverReadyTasks.add(block) + } + + companion object { + fun get() = surfComponentApi.componentsOfTypeLoaded(QueueStartHook::class.java).first() + } +} \ No newline at end of file diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/listener/QueuePlayerListener.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/listener/QueuePlayerListener.kt index a979e70..94db33c 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/listener/QueuePlayerListener.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/listener/QueuePlayerListener.kt @@ -1,11 +1,9 @@ package dev.slne.surf.queue.velocity.listener -import com.github.shynixn.mccoroutine.velocity.launch import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.connection.DisconnectEvent import com.velocitypowered.api.event.connection.PostLoginEvent import dev.slne.surf.queue.common.queue.RedisQueueService -import dev.slne.surf.queue.velocity.plugin import dev.slne.surf.queue.velocity.queue.VelocitySurfQueue import dev.slne.surf.surfapi.core.api.util.logger import kotlinx.coroutines.coroutineScope @@ -15,9 +13,9 @@ object QueuePlayerListener { private val log = logger() @Subscribe - fun onPostLogin(event: PostLoginEvent) { + suspend fun onPostLogin(event: PostLoginEvent) { val uuid = event.player.uniqueId - plugin.container.launch { + coroutineScope { for (queue in RedisQueueService.get().getAll()) { require(queue is VelocitySurfQueue) { "Queue must be VelocitySurfQueue" } launch { @@ -31,6 +29,7 @@ object QueuePlayerListener { } } } + } @Subscribe @@ -41,9 +40,7 @@ object QueuePlayerListener { require(queue is VelocitySurfQueue) launch { try { - if (queue.isQueued(uuid)) { - queue.markPlayerDisconnected(uuid) - } + queue.markPlayerDisconnected(uuid) } catch (e: Exception) { log.atWarning() .withCause(e) diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueTransfer.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueTransfer.kt index 5f7a473..ca0b8ae 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueTransfer.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueTransfer.kt @@ -1,5 +1,6 @@ package dev.slne.surf.queue.velocity.queue +import dev.slne.surf.core.api.common.SurfCoreApi import dev.slne.surf.core.api.common.player.SurfPlayer import dev.slne.surf.core.api.common.server.SurfServer import dev.slne.surf.core.api.common.server.connection.SurfServerConnectResult @@ -26,7 +27,7 @@ class QueueTransfer(private val processor: RedisQueueTransferProcessor, private return processor.processTransfers(availableSlots) { entry -> try { - val corePlayer = surfCoreApi.getPlayer(entry.uuid) + val corePlayer = SurfCoreApi.getPlayer(entry.uuid) if (corePlayer == null) { TransferAction.PLAYER_NOT_FOUND } else { @@ -57,7 +58,7 @@ class QueueTransfer(private val processor: RedisQueueTransferProcessor, private ): TransferAction { val (status, message) = try { withTimeout(30.seconds) { - surfCoreApi.sendPlayerAwaiting(player, targetServer) + SurfCoreApi.sendPlayerAwaiting(player, targetServer) } } catch (e: TimeoutCancellationException) { log.atWarning() diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/RedisQueueCleanup.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/RedisQueueCleanup.kt index bc023a2..3495d43 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/RedisQueueCleanup.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/RedisQueueCleanup.kt @@ -14,10 +14,11 @@ class RedisQueueCleanup( companion object { private val log = logger() + private const val CLEANUP_INTERVAL_TICKS = 10L } suspend fun tick() { - if (queue.getTickCount() % 30 == 0L) { + if (queue.getTickCount() % CLEANUP_INTERVAL_TICKS == 0L) { lockManager.withCleanupLock { cleanupExpiredEntries() } @@ -37,9 +38,19 @@ class RedisQueueCleanup( removals++ log.atInfo() .log("Cleanup: removed expired entry %s from queue %s", uuid, queue.serverName) - } catch (_: Exception) { - store.removeAllFor(uuid) - removals++ + } catch (e: Exception) { + log.atWarning() + .withCause(e) + .log("Cleanup: dequeue failed for %s in queue %s, attempting forced removal", uuid, queue.serverName) + + try { + store.removeAllFor(uuid) + removals++ + } catch (e2: Exception) { + log.atWarning() + .withCause(e2) + .log("Cleanup: forced removal also failed for %s in queue %s", uuid, queue.serverName) + } } } } diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/RedisQueueTransferProcessor.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/RedisQueueTransferProcessor.kt index ea58b27..5a66c13 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/RedisQueueTransferProcessor.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/RedisQueueTransferProcessor.kt @@ -26,25 +26,27 @@ class RedisQueueTransferProcessor( companion object { private val log = logger() private fun createDelay(): DelayStrategy = - DecorrelatedJitterDelay(Duration.ofSeconds(2), Duration.ofSeconds(10)) + DecorrelatedJitterDelay(Duration.ofSeconds(2), Duration.ofSeconds(5)) } suspend fun tick() { if (store.isPaused()) return try { - // decrease CPU usage and redis commands when the queue is empty or the server is full -// if (System.currentTimeMillis() < nextTransferTime) return + // Exponential backoff: decrease CPU usage and Redis commands when the + // queue is empty or the target server is full. + if (System.currentTimeMillis() < nextTransferTime) return val transferred = transfer.tryTransfer() -// if (transferred <= 0) { -// val delay = delay.calcDelay(attempts) -// nextTransferTime = System.currentTimeMillis() + delay.toMillis() -// attempts++ -// } else { -// attempts = 0 -// delay = createDelay() -// } + if (transferred <= 0) { + val delayDuration = delay.calcDelay(attempts) + nextTransferTime = System.currentTimeMillis() + delayDuration.toMillis() + attempts++ + } else { + attempts = 0 + delay = createDelay() + nextTransferTime = System.currentTimeMillis() + } } catch (e: Exception) { log.atWarning() @@ -114,11 +116,27 @@ class RedisQueueTransferProcessor( } TransferAction.PLUGIN_CANCELLED_TRANSFER, - TransferAction.ERROR, TransferAction.TIMEOUT -> { + TransferAction.ERROR -> { QueueMetrics.recordFailedTransfer(serverName) retryEntry(uuid, entry, maxRetries = 3) } + TransferAction.TIMEOUT -> { + // Timeout means the target server is likely unreachable. + // Dequeue immediately instead of retrying with another 30 s timeout + // to avoid blocking the entire queue for extended periods. + store.dequeue(uuid) + QueueMetrics.recordFailedTransfer(serverName) + QueueMetrics.recordDequeue(serverName) + log.atWarning() + .log( + "Player %s removed from queue %s due to transfer timeout", + uuid, + serverName + ) + break + } + TransferAction.SERVER_FULL -> break TransferAction.SERVER_NOT_FOUND -> break } @@ -173,22 +191,34 @@ class RedisQueueTransferProcessor( .log("Player %s removed from queue %s (offline > %dms)", uuid, serverName, gracePeriodMs) } + /** + * Moves a queue entry behind the next entry in the sorted set so that + * the transfer loop can proceed to other players. + * + * If the entry is the last one in the queue (no next entry exists), + * we simply leave it in place and return instead of aborting the + * entire transfer loop. + */ private suspend fun skipEntry(uuid: UUID, meta: QueueEntry) { val currentScore = store.getScore(uuid) ?: throw AbortException() val nextEntries = store.entriesAfter(uuid, limit = 1) - if (nextEntries.isEmpty()) throw AbortException() + if (nextEntries.isEmpty()) { + // This entry is the last in the queue — nothing to skip past. + return + } val nextScoreRaw = nextEntries.first().score val nextScore = RedisQueueScorePacker.unpack(nextScoreRaw) - var nextSequence = nextScore.sequence + 1 - if (nextSequence > RedisQueueScorePacker.MAX_SEQUENCE) { - nextSequence = nextScore.sequence - } + val sequenceOverflow = nextScore.sequence >= RedisQueueScorePacker.MAX_SEQUENCE + val nextSequence = if (sequenceOverflow) 0 else nextScore.sequence + 1 + // On overflow, also bump deltaMs so the new score is strictly greater + // than the entry we are skipping past. + val nextDeltaMs = if (sequenceOverflow) nextScore.deltaMs + 1 else nextScore.deltaMs val newScore = RedisQueueScorePacker.pack( meta.priority, - if (nextSequence == nextScore.sequence) nextScore.deltaMs + 1 else nextScore.deltaMs, + nextDeltaMs, nextSequence ) diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/VelocitySurfQueue.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/VelocitySurfQueue.kt index 4419f80..0fe0173 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/VelocitySurfQueue.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/VelocitySurfQueue.kt @@ -7,6 +7,7 @@ import dev.slne.surf.surfapi.core.api.util.logger import java.time.Instant import java.util.* import java.util.concurrent.atomic.AtomicLong +import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration.Companion.minutes class VelocitySurfQueue(serverName: String) : AbstractSurfQueue(serverName) { @@ -55,6 +56,7 @@ class VelocitySurfQueue(serverName: String) : AbstractSurfQueue(serverName) { try { block() } catch (e: Exception) { + if (e is CancellationException) throw e log.atWarning() .withCause(e) .log("Failed to tick %s for queue %s", component, serverName) From a8ec5dad2373fd03edb1b2511b07490a1b208f18 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Mon, 6 Apr 2026 12:09:30 +0200 Subject: [PATCH 2/7] Add PolarLoader dependency and implement PolarQueueStartHook for server readiness tasks --- gradle/libs.versions.toml | 3 ++- surf-queue-paper/build.gradle.kts | 3 +++ .../surf/queue/paper/hook/startup/PolarQueueStartHook.kt | 6 +++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index df596c0..afdab33 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,8 @@ - [versions] +polar = "2.3.0" [libraries] +polar = { module = "top.polar:api", version.ref = "polar" } [bundles] diff --git a/surf-queue-paper/build.gradle.kts b/surf-queue-paper/build.gradle.kts index b6d8de3..5c4ed89 100644 --- a/surf-queue-paper/build.gradle.kts +++ b/surf-queue-paper/build.gradle.kts @@ -1,4 +1,5 @@ import dev.slne.surf.surfapi.gradle.util.registerRequired +import dev.slne.surf.surfapi.gradle.util.registerSoft plugins { id("dev.slne.surf.surfapi.gradle.paper-plugin") @@ -12,9 +13,11 @@ surfPaperPluginApi { authors.addAll(providers.gradleProperty("authors").map { it.split(",") }) serverDependencies { registerRequired("LuckPerms") + registerSoft("PolarLoader") } } dependencies { api(project(":surf-queue-common")) + compileOnly(libs.polar) } \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/PolarQueueStartHook.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/PolarQueueStartHook.kt index aeb599d..2053737 100644 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/PolarQueueStartHook.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/PolarQueueStartHook.kt @@ -1,10 +1,14 @@ package dev.slne.surf.queue.paper.hook.startup import dev.slne.surf.surfapi.shared.api.component.ComponentMeta +import dev.slne.surf.surfapi.shared.api.component.requirement.DependsOnClass +import top.polar.api.loader.LoaderApi @ComponentMeta +@DependsOnClass(LoaderApi::class) class PolarQueueStartHook : QueueStartHook() { - override suspend fun onEnable() { + override suspend fun onLoad() { + LoaderApi.registerEnableCallback(::runServerReadyTasks) } } \ No newline at end of file From 260d55b48884f7921f84332e5cdf7e9e163b72b8 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:28:54 +0200 Subject: [PATCH 3/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(codec):=20opt?= =?UTF-8?q?imize=20buffer=20allocation=20in=20QueueEntryCodec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - set buffer size in QueueEntryCodec to a constant for better readability - improve buffer initialization by specifying min and max capacity --- .../surf/queue/common/queue/codec/QueueEntryCodec.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/codec/QueueEntryCodec.kt b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/codec/QueueEntryCodec.kt index ddea1be..166e970 100644 --- a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/codec/QueueEntryCodec.kt +++ b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/codec/QueueEntryCodec.kt @@ -8,9 +8,19 @@ import dev.slne.surf.redis.shaded.io.netty.buffer.Unpooled import java.util.* class QueueEntryCodec : BaseCodec() { + companion object { + // @formatter:off + private const val BUFFER_SIZE = ( + (Long.SIZE_BYTES * 2) // uuid + + (Long.SIZE_BYTES) // addedAt + + (Int.SIZE_BYTES) // priority + ) + // @formatter:on + } + private val encoder = Encoder { obj -> val entry = obj as QueueEntry - val buf = Unpooled.buffer() + val buf = Unpooled.buffer(BUFFER_SIZE, BUFFER_SIZE) try { buf.writeLong(entry.uuid.mostSignificantBits) From 25258a2046c440f98091fb4f86974ab172efefe6 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:16:04 +0200 Subject: [PATCH 4/7] =?UTF-8?q?=E2=9C=A8=20feat(queue):=20introduce=20new?= =?UTF-8?q?=20queue=20configuration=20and=20transfer=20action=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rename startTransferring to startTicking for clarity - add SurfQueueConfig for managing queue settings - implement TransferAction enum for better transfer state management - enhance PaperQueue with tickSecond method for periodic processing - add QueueMetrics for tracking queue performance metrics --- .../dev/slne/surf/queue/api/SurfQueue.kt | 4 +- .../queue/common/queue/AbstractSurfQueue.kt | 16 ++- .../queue/common/queue/RedisQueueScore.kt | 112 +++++++++++++++ .../common/queue/RedisQueueScorePacker.kt | 114 --------------- .../queue/common/queue/RedisQueueStore.kt | 71 ++++++---- .../queue/paper/PaperSurfQueueInstance.kt | 17 ++- .../surf/queue/paper/commands/QueueCommand.kt | 21 +++ .../paper/commands/sub/QueueCleanupCommand.kt | 42 ++++++ .../paper/commands/sub/QueueClearCommand.kt | 36 +++++ .../paper/commands/sub/QueueDequeCommand.kt | 48 +++++++ .../paper/commands/sub/QueueEnqueueCommand.kt | 58 ++++++++ .../paper/commands/sub/QueueInfoCommand.kt | 134 ++++++++++++++++++ .../paper/commands/sub/QueueListCommand.kt | 49 +++++++ .../paper/commands/sub/QueuePauseCommand.kt | 62 ++++++++ .../queue/paper/config/SurfQueueConfig.kt | 16 +++ .../PlayerKickedDueToFullServerListener.kt | 36 +++++ .../paper/metrics/QueueBstatsIntegration.kt | 79 +++++++++++ .../surf/queue/paper/metrics/QueueMetrics.kt | 124 ++++++++++++++++ .../queue/paper/metrics/QueueMetricsLogger.kt | 61 ++++++++ .../paper/metrics/QueueMetricsSnapshot.kt | 63 ++++++++ .../paper/permission/PaperQueuePermissions.kt | 17 +++ .../queue/paper/queue/PaperQueueCleanup.kt | 11 +- .../queue/paper/queue/PaperQueueTickTask.kt | 51 +++++++ .../queue/paper/queue/PaperQueueTransfer.kt | 36 ++--- .../queue/PaperQueueTransferProcessor.kt | 51 +++++-- .../surf/queue/paper/queue/PaperSurfQueue.kt | 53 ++++++- .../surf/queue/paper}/queue/TransferAction.kt | 2 +- .../velocity/VelocitySurfQueueInstance.kt | 2 +- .../command/test/TestQueueCommands.kt | 3 +- .../queue/{display => }/QueueDisplay.kt | 7 +- .../queue/velocity/queue/QueueTickTask.kt | 2 +- .../queue/velocity/queue/VelocitySurfQueue.kt | 36 +---- 32 files changed, 1204 insertions(+), 230 deletions(-) create mode 100644 surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueScore.kt delete mode 100644 surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueScorePacker.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/QueueCommand.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueCleanupCommand.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueClearCommand.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueDequeCommand.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueEnqueueCommand.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueInfoCommand.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueListCommand.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueuePauseCommand.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/config/SurfQueueConfig.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/listener/PlayerKickedDueToFullServerListener.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/metrics/QueueBstatsIntegration.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/metrics/QueueMetrics.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/metrics/QueueMetricsLogger.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/metrics/QueueMetricsSnapshot.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/permission/PaperQueuePermissions.kt rename surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/RedisQueueCleanup.kt => surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueCleanup.kt (88%) create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTickTask.kt rename surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueTransfer.kt => surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTransfer.kt (71%) rename surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/RedisQueueTransferProcessor.kt => surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTransferProcessor.kt (81%) rename {surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity => surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper}/queue/TransferAction.kt (86%) rename surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/{display => }/QueueDisplay.kt (93%) diff --git a/surf-queue-api/src/main/kotlin/dev/slne/surf/queue/api/SurfQueue.kt b/surf-queue-api/src/main/kotlin/dev/slne/surf/queue/api/SurfQueue.kt index c52a540..f32707f 100644 --- a/surf-queue-api/src/main/kotlin/dev/slne/surf/queue/api/SurfQueue.kt +++ b/surf-queue-api/src/main/kotlin/dev/slne/surf/queue/api/SurfQueue.kt @@ -1,7 +1,7 @@ package dev.slne.surf.queue.api +import dev.slne.surf.core.api.common.SurfCoreApi import dev.slne.surf.core.api.common.server.SurfServer -import dev.slne.surf.core.api.common.surfCoreApi import dev.slne.surf.queue.api.service.SurfQueueService import it.unimi.dsi.fastutil.objects.Object2IntMap import java.util.* @@ -9,7 +9,7 @@ import java.util.* interface SurfQueue { val serverName: String - fun server() = surfCoreApi.getServerByName(serverName) + fun server() = SurfCoreApi.getServerByName(serverName) suspend fun enqueue(uuid: UUID): Boolean suspend fun enqueue(uuid: UUID, priority: Int): Boolean diff --git a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/AbstractSurfQueue.kt b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/AbstractSurfQueue.kt index 8226abd..ad6e61c 100644 --- a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/AbstractSurfQueue.kt +++ b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/AbstractSurfQueue.kt @@ -26,7 +26,7 @@ abstract class AbstractSurfQueue(override val serverName: String) : SurfQueue { private val log = logger() fun fixPriority(uuid: UUID, priority: Int): Int { - return if (priority <= RedisQueueScorePacker.MAX_PRIORITY) { + return if (priority <= RedisQueueScore.MAX_PRIORITY) { priority } else { log.atWarning() @@ -34,10 +34,10 @@ abstract class AbstractSurfQueue(override val serverName: String) : SurfQueue { "Priority %d for %s exceeds max representable priority, capping to %d", priority, uuid, - RedisQueueScorePacker.MAX_PRIORITY + RedisQueueScore.MAX_PRIORITY ) - RedisQueueScorePacker.MAX_PRIORITY + RedisQueueScore.MAX_PRIORITY } } } @@ -51,16 +51,15 @@ abstract class AbstractSurfQueue(override val serverName: String) : SurfQueue { val priorityFixed = fixPriority(uuid, priority) val now = Instant.now().toEpochMilli() val sequence = enqueueSequence.getAndUpdate { current -> - if (current >= RedisQueueScorePacker.MAX_SEQUENCE.toInt()) 0 else current + 1 + if (current >= RedisQueueScore.MAX_SEQUENCE) 0 else current + 1 } - val score = RedisQueueScorePacker.pack( + val score = RedisQueueScore.pack( priorityFixed, now - epochMs, sequence ) - val meta = QueueEntry(uuid, now, priorityFixed) val added = store.enqueueIfAbsent(uuid, meta, score) @@ -114,4 +113,9 @@ abstract class AbstractSurfQueue(override val serverName: String) : SurfQueue { override suspend fun pause() { store.setPaused(true) } + + suspend fun getEntryMeta(uuid: UUID): QueueEntry? = store.getMeta(uuid) + suspend fun getEntryScore(uuid: UUID): RedisQueueScore? = store.getScore(uuid) + suspend fun getEntryLastSeen(uuid: UUID): Long? = store.getLastSeen(uuid) + suspend fun getEntryRetryCount(uuid: UUID): Int? = store.getRetryCount(uuid) } \ No newline at end of file diff --git a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueScore.kt b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueScore.kt new file mode 100644 index 0000000..350405e --- /dev/null +++ b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueScore.kt @@ -0,0 +1,112 @@ +package dev.slne.surf.queue.common.queue + +/** + * Packs queue metadata into a single 53-bit value stored as a Redis ZSET score (Double). + * + * The layout is designed to: + * - Preserve total ordering inside Redis sorted sets + * - Avoid precision loss (fits exactly into IEEE-754 double: 53 bits) + * - Support priority, timestamp, and tie-breaking sequence + * + * ## Bit layout: + * + * `priority:7`|`deltaMs:40`|`sequence:6` + * + * + * Total: 53 bits → exactly representable as Double + * + * ## Field details: + * + * - priority (7 bits) + * Higher priority should come first. + * To achieve this with ascending ZSET ordering, the value is inverted: + * `storedPriority = MAX_PRIORITY - priority` + * + * - deltaMs (40 bits) + * Time component (usually epoch delta in milliseconds). + * Provides the primary ordering. + * Range: ~34.8 years + * + * - sequence (6 bits) + * Tie-breaker for entries created within the same millisecond. + * Range: 0–63 (64 entries per millisecond). + * + * Ordering behavior in Redis ZSET: + * 1. Lower storedPriority → higher logical priority + * 2. Lower deltaMs → earlier timestamp + * 3. Lower sequence → earlier insertion within same millisecond + * + * Important: + * - The total bit size MUST NOT exceed 53 bits, otherwise precision loss occurs in Double. + * - All values must be within their defined ranges. + */ +@JvmInline +value class RedisQueueScore(val packed: Double) { + companion object { + private const val PRIORITY_BITS = 7 + private const val DELTA_MS_BITS = 40 + private const val SEQUENCE_BITS = 6 + + private const val DELTA_MS_SHIFT = SEQUENCE_BITS + private const val PRIORITY_SHIFT = DELTA_MS_BITS + SEQUENCE_BITS + + private const val SEQUENCE_MASK = (1L shl SEQUENCE_BITS) - 1 // 0x3F (63) + private const val DELTA_MS_MASK = (1L shl DELTA_MS_BITS) - 1 // 0xFFFFFFFFFF + private const val PRIORITY_MASK = (1L shl PRIORITY_BITS) - 1 // 0x7F + + const val MAX_PRIORITY = PRIORITY_MASK.toInt() + const val MAX_DELTA_MS = DELTA_MS_MASK + const val MAX_SEQUENCE = SEQUENCE_MASK.toInt() + + /** + * Packs priority, timestamp and sequence into a single Double score. + * + * @param priority logical priority (0..MAX_PRIORITY), higher = more important + * @param deltaMs time value (usually epoch delta in ms) + * @param sequence tie-breaker for same timestamp (0..MAX_SEQUENCE) + * + * @return packed score suitable for Redis ZSET + * + * @throws IllegalArgumentException if any value is out of range + */ + fun pack(priority: Int, deltaMs: Long, sequence: Int): RedisQueueScore { + require(priority in 0..MAX_PRIORITY) { "priority out of range" } + require(deltaMs in 0..MAX_DELTA_MS) { "deltaMs out of range" } + require(sequence in 0..MAX_SEQUENCE) { "sequence out of range" } + + val invertedPriority = (MAX_PRIORITY - priority).toLong() + + val value = + (invertedPriority shl PRIORITY_SHIFT) or + (deltaMs shl DELTA_MS_SHIFT) or + sequence.toLong() + + return RedisQueueScore(value.toDouble()) + } + + fun optional(value: Double?): RedisQueueScore? = value?.let { RedisQueueScore(it) } + } + + /** + * @return packed score as a Long + */ + private val packedLong get() = packed.toLong() + + /** + * @return logical priority (0..[MAX_PRIORITY]), higher = more important + */ + val priority: Int + get() = MAX_PRIORITY - ((packedLong shr PRIORITY_SHIFT) and PRIORITY_MASK).toInt() + + /** + * @return time value (usually epoch delta in ms) + */ + val deltaMs: Long + get() = (packedLong shr DELTA_MS_SHIFT) and DELTA_MS_MASK + + /** + * @return tie-breaker for same timestamp (0..[MAX_SEQUENCE]) + */ + val sequence: Int + get() = (packedLong and SEQUENCE_MASK).toInt() +} \ No newline at end of file diff --git a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueScorePacker.kt b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueScorePacker.kt deleted file mode 100644 index bc1b643..0000000 --- a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueScorePacker.kt +++ /dev/null @@ -1,114 +0,0 @@ -package dev.slne.surf.queue.common.queue - -/** - * Packs queue metadata into a single 53-bit value stored as a Redis ZSET score (Double). - * - * The layout is designed to: - * - Preserve total ordering inside Redis sorted sets - * - Avoid precision loss (fits exactly into IEEE-754 double: 53 bits) - * - Support priority, timestamp, and tie-breaking sequence - * - * ## Bit layout: - * - * `priority:7`|`deltaMs:40`|`sequence:6` - * - * - * Total: 53 bits → exactly representable as Double - * - * ## Field details: - * - * - priority (7 bits) - * Higher priority should come first. - * To achieve this with ascending ZSET ordering, the value is inverted: - * `storedPriority = MAX_PRIORITY - priority` - * - * - deltaMs (40 bits) - * Time component (usually epoch delta in milliseconds). - * Provides the primary ordering. - * Range: ~34.8 years - * - * - sequence (6 bits) - * Tie-breaker for entries created within the same millisecond. - * Range: 0–63 (64 entries per millisecond). - * - * Ordering behavior in Redis ZSET: - * 1. Lower storedPriority → higher logical priority - * 2. Lower deltaMs → earlier timestamp - * 3. Lower sequence → earlier insertion within same millisecond - * - * Important: - * - The total bit size MUST NOT exceed 53 bits, otherwise precision loss occurs in Double. - * - All values must be within their defined ranges. - */ -object RedisQueueScorePacker { - private const val PRIORITY_BITS = 7 - private const val DELTA_MS_BITS = 40 - private const val SEQUENCE_BITS = 6 - - private const val DELTA_MS_SHIFT = SEQUENCE_BITS - private const val PRIORITY_SHIFT = DELTA_MS_BITS + SEQUENCE_BITS - - private const val SEQUENCE_MASK = (1L shl SEQUENCE_BITS) - 1 // 0x3F (63) - private const val DELTA_MS_MASK = (1L shl DELTA_MS_BITS) - 1 // 0xFFFFFFFFFF - private const val PRIORITY_MASK = (1L shl PRIORITY_BITS) - 1 // 0x7F - - const val MAX_PRIORITY = PRIORITY_MASK.toInt() - const val MAX_DELTA_MS = DELTA_MS_MASK - const val MAX_SEQUENCE = SEQUENCE_MASK.toInt() - - /** - * Unpacks a Redis ZSET score back into its original components. - * - * @param score the packed Double value from Redis - * @return unpacked priority, deltaMs and sequence - */ - fun unpack(score: Double): Unpacked { - val value = score.toLong() - - val storedPriority = ((value shr PRIORITY_SHIFT) and PRIORITY_MASK).toInt() - val priority = MAX_PRIORITY - storedPriority - val deltaMs = (value shr DELTA_MS_SHIFT) and DELTA_MS_MASK - val sequence = (value and SEQUENCE_MASK).toInt() - - return Unpacked(priority, deltaMs, sequence) - } - - /** - * Packs priority, timestamp and sequence into a single Double score. - * - * @param priority logical priority (0..MAX_PRIORITY), higher = more important - * @param deltaMs time value (usually epoch delta in ms) - * @param sequence tie-breaker for same timestamp (0..MAX_SEQUENCE) - * - * @return packed score suitable for Redis ZSET - * - * @throws IllegalArgumentException if any value is out of range - */ - fun pack(priority: Int, deltaMs: Long, sequence: Int): Double { - require(priority in 0..MAX_PRIORITY) { "priority out of range" } - require(deltaMs in 0..MAX_DELTA_MS) { "deltaMs out of range" } - require(sequence in 0..MAX_SEQUENCE) { "sequence out of range" } - - val invertedPriority = (MAX_PRIORITY - priority).toLong() - - val value = - (invertedPriority shl PRIORITY_SHIFT) or - (deltaMs shl DELTA_MS_SHIFT) or - sequence.toLong() - - return value.toDouble() - } - - /** - * Result of unpacking a score. - * - * @property priority logical priority (higher = more important) - * @property deltaMs timestamp component - * @property sequence tie-breaker within same timestamp - */ - data class Unpacked( - val priority: Int, - val deltaMs: Long, - val sequence: Int - ) -} \ No newline at end of file diff --git a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueStore.kt b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueStore.kt index e3a63f4..6e57d8f 100644 --- a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueStore.kt +++ b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueStore.kt @@ -4,6 +4,8 @@ import dev.slne.surf.queue.common.queue.codec.QueueEntryCodec import dev.slne.surf.queue.common.redis.redisApi import dev.slne.surf.redis.codec.UUIDCodec import dev.slne.surf.redis.libs.redisson.api.BatchOptions +import dev.slne.surf.redis.libs.redisson.api.BatchResult +import dev.slne.surf.redis.libs.redisson.api.RBatch import dev.slne.surf.redis.libs.redisson.client.codec.IntegerCodec import dev.slne.surf.redis.libs.redisson.client.codec.LongCodec import dev.slne.surf.redis.libs.redisson.client.protocol.ScoredEntry @@ -13,7 +15,7 @@ import org.jetbrains.annotations.Blocking import java.time.Instant import java.util.* -class RedisQueueStore(private val keys: RedisQueueKeys) { +class RedisQueueStore(keys: RedisQueueKeys) { private val scoredSet = redisApi.redisson.getScoredSortedSet(keys.entriesKey, UUIDCodec.INSTANCE) private val metaMap = redisApi.redisson.getMap( keys.metaKey, @@ -32,38 +34,49 @@ class RedisQueueStore(private val keys: RedisQueueKeys) { private val epochMsBucket = redisApi.redisson.getBucket(keys.epochMsKey, LongCodec.INSTANCE) private val pausedBucket = redisApi.redisson.getBucket(keys.pausedKey, IntegerCodec.INSTANCE) - companion object { private fun atomicBatchOptions(): BatchOptions { return BatchOptions.defaults().executionMode(BatchOptions.ExecutionMode.IN_MEMORY_ATOMIC) } } + // region RBatch helper + private inline fun createAtomicBatch(block: RBatch.() -> R) = + redisApi.redisson.createBatch(atomicBatchOptions()).run(block) + + private suspend inline fun executeAtomicBatch(block: RBatch.() -> Unit): BatchResult<*> = createAtomicBatch { + block() + executeAsync().await() + } + + private fun RBatch.getQueueScoredSet() = getScoredSortedSet(scoredSet.name, scoredSet.codec) + private fun RBatch.getQueueMetaMap() = getMap(metaMap.name, metaMap.codec) + private fun RBatch.getQueueLastSeenMap() = getMap(lastSeenMap.name, lastSeenMap.codec) + private fun RBatch.getQueueRetryCountMap() = getMap(retryCountMap.name, retryCountMap.codec) + // endregion + @Blocking fun initEpochMs(): Long { epochMsBucket.setIfAbsent(Instant.now().toEpochMilli()) return epochMsBucket.get() } - suspend fun enqueueIfAbsent(uuid: UUID, meta: QueueEntry, score: Double): Boolean { - val batch = redisApi.redisson.createBatch(atomicBatchOptions()) - - val addAsync = batch.getScoredSortedSet(scoredSet.name, scoredSet.codec) - .addIfAbsentAsync(score, uuid) - batch.getMap(metaMap.name, metaMap.codec) - .fastPutIfAbsentAsync(uuid, meta) + suspend fun enqueueIfAbsent(uuid: UUID, meta: QueueEntry, score: RedisQueueScore): Boolean { + val result = executeAtomicBatch { + getQueueScoredSet().addIfAbsentAsync(score.packed, uuid) + getQueueMetaMap().fastPutIfAbsentAsync(uuid, meta) + } - batch.executeAsync().await() - return addAsync.await() + return result.responses.first() as Boolean } suspend fun dequeue(uuid: UUID): Boolean { - return batchRemove(uuid) + return batchRemove(uuid) } suspend fun isQueued(uuid: UUID): Boolean = scoredSet.containsAsync(uuid).await() suspend fun rank(uuid: UUID): Int? = scoredSet.rankAsync(uuid).await() - suspend fun getScore(uuid: UUID): Double? = scoredSet.getScoreAsync(uuid).await() + suspend fun getScore(uuid: UUID): RedisQueueScore? = RedisQueueScore.optional(scoredSet.getScoreAsync(uuid).await()) suspend fun size(): Int = scoredSet.sizeAsync().await() suspend fun top1(): UUID? = scoredSet.entryRangeAsync(0, 0).await().firstOrNull()?.value @@ -71,13 +84,15 @@ class RedisQueueStore(private val keys: RedisQueueKeys) { suspend fun readAllEntries(): Collection> = scoredSet.entryRangeAsync(0, -1).await() suspend fun entriesAfter(uuid: UUID, limit: Int): Collection> { val currentScore = getScore(uuid) ?: return emptyList() - return scoredSet.entryRangeAsync(currentScore, false, Double.MAX_VALUE, true, 0, limit).await() + return scoredSet.entryRangeAsync(currentScore.packed, false, Double.MAX_VALUE, true, 0, limit).await() } suspend fun incrementRetryCount(uuid: UUID): Int { return retryCountMap.addAndGetAsync(uuid, 1).await() } + suspend fun getRetryCount(uuid: UUID): Int? = retryCountMap.getAsync(uuid).await() + suspend fun clearRetryCount(uuid: UUID) { retryCountMap.removeAsync(uuid).await() } @@ -100,28 +115,22 @@ class RedisQueueStore(private val keys: RedisQueueKeys) { suspend fun readAllLastSeen(): Map = lastSeenMap.readAllMapAsync().await() ?: emptyMap() - suspend fun addOrUpdateScore(uuid: UUID, score: Double): Boolean { - return scoredSet.addAsync(score, uuid).await() + suspend fun addOrUpdateScore(uuid: UUID, score: RedisQueueScore): Boolean { + return scoredSet.addAsync(score.packed, uuid).await() } private suspend fun batchRemove(uuid: UUID): Boolean { - val batch = redisApi.redisson.createBatch(atomicBatchOptions()) - - val removeAsync = batch.getScoredSortedSet(scoredSet.name, scoredSet.codec) - .removeAsync(uuid) - batch.getMap(metaMap.name, metaMap.codec) - .removeAsync(uuid) - batch.getMap(lastSeenMap.name, lastSeenMap.codec) - .removeAsync(uuid) - batch.getMap(retryCountMap.name, retryCountMap.codec) - .removeAsync(uuid) - - batch.executeAsync().await() try { - batch.executeAsync().await() - return removeAsync.await() + val result = executeAtomicBatch { + getQueueScoredSet().removeAsync(uuid) + getQueueMetaMap().removeAsync(uuid) + getQueueLastSeenMap().removeAsync(uuid) + getQueueRetryCountMap().removeAsync(uuid) + } + + return result.responses.first() as Boolean } catch (e: Exception) { - // If the batch fails, we need to remove the individual elements manually + // If the batch fails, we need try to remove the individual elements manually runCatching { scoredSet.removeAsync(uuid).await() } runCatching { metaMap.removeAsync(uuid).await() } runCatching { lastSeenMap.removeAsync(uuid).await() } diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperSurfQueueInstance.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperSurfQueueInstance.kt index cbfba65..8004413 100644 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperSurfQueueInstance.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperSurfQueueInstance.kt @@ -3,20 +3,35 @@ package dev.slne.surf.queue.paper import com.google.auto.service.AutoService import dev.slne.surf.queue.common.SurfQueueInstance import dev.slne.surf.queue.common.queue.AbstractSurfQueue +import dev.slne.surf.queue.paper.config.SurfQueueConfig import dev.slne.surf.queue.paper.hook.startup.QueueStartHook +import dev.slne.surf.queue.paper.listener.PlayerKickedDueToFullServerListener +import dev.slne.surf.queue.paper.queue.PaperQueueTickTask import dev.slne.surf.queue.paper.queue.PaperSurfQueue +import dev.slne.surf.surfapi.bukkit.api.event.register @AutoService(SurfQueueInstance::class) class PaperSurfQueueInstance : SurfQueueInstance() { override val componentOwner get() = plugin override suspend fun load() { + SurfQueueConfig.init() super.load() QueueStartHook.get().onServerReady { - + PaperQueueTickTask.start() } } + override suspend fun enable() { + super.enable() + PlayerKickedDueToFullServerListener.register() + } + + override suspend fun disable() { + PaperQueueTickTask.shutdown() + super.disable() + } + override fun createQueue(serverName: String): AbstractSurfQueue { return PaperSurfQueue(serverName) } diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/QueueCommand.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/QueueCommand.kt new file mode 100644 index 0000000..def638c --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/QueueCommand.kt @@ -0,0 +1,21 @@ +package dev.slne.surf.queue.paper.commands + +import dev.jorel.commandapi.kotlindsl.commandAPICommand +import dev.slne.surf.queue.paper.commands.sub.queueDequeue +import dev.slne.surf.queue.paper.commands.sub.queueCleanup +import dev.slne.surf.queue.paper.commands.sub.queueClear +import dev.slne.surf.queue.paper.commands.sub.queueEnqueue +import dev.slne.surf.queue.paper.commands.sub.queueInfo +import dev.slne.surf.queue.paper.commands.sub.queueList +import dev.slne.surf.queue.paper.permission.PaperQueuePermissions + +fun queueCommand() = commandAPICommand("squeue") { + withPermission(PaperQueuePermissions.COMMAND_QUEUE) + + queueCleanup() + queueClear() + queueDequeue() + queueEnqueue() + queueInfo() + queueList() +} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueCleanupCommand.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueCleanupCommand.kt new file mode 100644 index 0000000..423fcc2 --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueCleanupCommand.kt @@ -0,0 +1,42 @@ +package dev.slne.surf.queue.paper.commands.sub + +import dev.jorel.commandapi.CommandAPICommand +import dev.jorel.commandapi.kotlindsl.getValue +import dev.jorel.commandapi.kotlindsl.subcommand +import dev.slne.surf.core.api.common.SurfCoreApi +import dev.slne.surf.core.api.common.server.SurfServer +import dev.slne.surf.core.api.paper.command.argument.surfBackendServerArgument +import dev.slne.surf.queue.common.queue.RedisQueueService +import dev.slne.surf.queue.paper.permission.PaperQueuePermissions +import dev.slne.surf.queue.paper.queue.PaperSurfQueue +import dev.slne.surf.surfapi.bukkit.api.command.executors.anyExecutorSuspend +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText + +fun CommandAPICommand.queueCleanup() = subcommand("cleanup") { + withPermission(PaperQueuePermissions.COMMAND_CLEANUP) + surfBackendServerArgument("server", optional = true) + + anyExecutorSuspend { sender, arguments -> + val server: SurfServer? by arguments + val serverName = server?.name ?: SurfServer.current().name + val queue = RedisQueueService.get().get(serverName) as PaperSurfQueue + + val sizeBefore = queue.size() + queue.forceCleanup() + val sizeAfter = queue.size() + val removed = sizeBefore - sizeAfter + + sender.sendText { + appendSuccessPrefix() + success("Forced cleanup of queue '") + variableValue(serverName) + success("' complete. Removed ") + variableValue("$removed") + success(" expired entries (") + variableValue("$sizeBefore") + success(" —> ") + variableValue("$sizeAfter") + success(").") + } + } +} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueClearCommand.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueClearCommand.kt new file mode 100644 index 0000000..4a58681 --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueClearCommand.kt @@ -0,0 +1,36 @@ +package dev.slne.surf.queue.paper.commands.sub + +import dev.jorel.commandapi.CommandAPICommand +import dev.jorel.commandapi.kotlindsl.getValue +import dev.jorel.commandapi.kotlindsl.subcommand +import dev.slne.surf.core.api.common.SurfCoreApi +import dev.slne.surf.core.api.common.server.SurfServer +import dev.slne.surf.core.api.paper.command.argument.surfBackendServerArgument +import dev.slne.surf.queue.common.queue.RedisQueueService +import dev.slne.surf.queue.paper.permission.PaperQueuePermissions +import dev.slne.surf.queue.paper.queue.PaperSurfQueue +import dev.slne.surf.surfapi.bukkit.api.command.executors.anyExecutorSuspend +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText + +fun CommandAPICommand.queueClear() = subcommand("clear") { + withPermission(PaperQueuePermissions.COMMAND_CLEAR) + surfBackendServerArgument("server", optional = true) + + anyExecutorSuspend { sender, arguments -> + val server: SurfServer? by arguments + val serverName = server?.name ?: SurfServer.current().name + val queue = RedisQueueService.get().get(serverName) as PaperSurfQueue + + val size = queue.size() + queue.delete() + + sender.sendText { + appendSuccessPrefix() + success("Cleared queue for server ") + variableValue(serverName) + success(". Removed ") + variableValue("$size") + success(" entries.") + } + } +} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueDequeCommand.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueDequeCommand.kt new file mode 100644 index 0000000..79d73f2 --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueDequeCommand.kt @@ -0,0 +1,48 @@ +package dev.slne.surf.queue.paper.commands.sub + +import dev.jorel.commandapi.CommandAPICommand +import dev.jorel.commandapi.arguments.AsyncPlayerProfileArgument +import dev.jorel.commandapi.kotlindsl.argument +import dev.jorel.commandapi.kotlindsl.getValue +import dev.jorel.commandapi.kotlindsl.subcommand +import dev.slne.surf.core.api.common.server.SurfServer +import dev.slne.surf.core.api.paper.command.argument.surfBackendServerArgument +import dev.slne.surf.queue.common.queue.RedisQueueService +import dev.slne.surf.queue.paper.permission.PaperQueuePermissions +import dev.slne.surf.surfapi.bukkit.api.command.executors.anyExecutorSuspend +import dev.slne.surf.surfapi.bukkit.api.command.util.awaitAsyncPlayerProfile +import dev.slne.surf.surfapi.bukkit.api.command.util.idOrThrow +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText + +fun CommandAPICommand.queueDequeue() = subcommand("dequeue") { + withPermission(PaperQueuePermissions.COMMAND_DEQUEUE) + argument(AsyncPlayerProfileArgument("player")) + surfBackendServerArgument("server", optional = true) + + anyExecutorSuspend { sender, arguments -> + val profile = arguments.awaitAsyncPlayerProfile("player") + val uuid = profile.idOrThrow() + val server: SurfServer? by arguments + val serverName = server?.name ?: SurfServer.current().name + val queue = RedisQueueService.get().get(serverName) + + val dequeued = queue.dequeue(uuid) + if (dequeued) { + sender.sendText { + appendSuccessPrefix() + success("Removed ") + variableValue(profile.name ?: uuid.toString()) + success(" from the queue ") + variableValue(serverName) + success(".") + } + } else { + sender.sendText { + appendErrorPrefix() + error("Player ") + variableValue(profile.name ?: uuid.toString()) + error(" is not in the queue.") + } + } + } +} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueEnqueueCommand.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueEnqueueCommand.kt new file mode 100644 index 0000000..29c02b1 --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueEnqueueCommand.kt @@ -0,0 +1,58 @@ +package dev.slne.surf.queue.paper.commands.sub + +import dev.jorel.commandapi.CommandAPICommand +import dev.jorel.commandapi.arguments.AsyncPlayerProfileArgument +import dev.jorel.commandapi.kotlindsl.argument +import dev.jorel.commandapi.kotlindsl.getValue +import dev.jorel.commandapi.kotlindsl.integerArgument +import dev.jorel.commandapi.kotlindsl.subcommand +import dev.slne.surf.core.api.common.server.SurfServer +import dev.slne.surf.core.api.paper.command.argument.surfBackendServerArgument +import dev.slne.surf.queue.common.queue.RedisQueueScore +import dev.slne.surf.queue.common.queue.RedisQueueService +import dev.slne.surf.queue.paper.permission.PaperQueuePermissions +import dev.slne.surf.surfapi.bukkit.api.command.executors.anyExecutorSuspend +import dev.slne.surf.surfapi.bukkit.api.command.util.awaitAsyncPlayerProfile +import dev.slne.surf.surfapi.bukkit.api.command.util.idOrThrow +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText + +fun CommandAPICommand.queueEnqueue() = subcommand("enqueue") { + withPermission(PaperQueuePermissions.COMMAND_ENQUEUE) + + argument(AsyncPlayerProfileArgument("player")) + surfBackendServerArgument("server", optional = true) + integerArgument("priority", optional = true, min = 0, max = RedisQueueScore.MAX_PRIORITY) + + anyExecutorSuspend { sender, arguments -> + val profile = arguments.awaitAsyncPlayerProfile("player") + val uuid = profile.idOrThrow() + val server: SurfServer? by arguments + val priority: Int? by arguments + + val serverName = server?.name ?: SurfServer.current().name + val queue = RedisQueueService.get().get(serverName) + + val enqueued = priority?.let { queue.enqueue(uuid, it) } ?: queue.enqueue(uuid) + if (enqueued) { + sender.sendText { + appendSuccessPrefix() + success("Enqueued ") + variableValue(profile.name ?: uuid.toString()) + success(" to the queue ") + variableValue(serverName) + priority?.let { + success(" with priority ") + variableValue(it) + } + success(".") + } + } else { + sender.sendText { + appendErrorPrefix() + error("Player ") + variableValue(profile.name ?: uuid.toString()) + error(" is already in the queue.") + } + } + } +} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueInfoCommand.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueInfoCommand.kt new file mode 100644 index 0000000..2edb6f4 --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueInfoCommand.kt @@ -0,0 +1,134 @@ +package dev.slne.surf.queue.paper.commands.sub + +import dev.jorel.commandapi.CommandAPICommand +import dev.jorel.commandapi.arguments.AsyncPlayerProfileArgument +import dev.jorel.commandapi.kotlindsl.argument +import dev.jorel.commandapi.kotlindsl.getValue +import dev.jorel.commandapi.kotlindsl.subcommand +import dev.slne.surf.core.api.common.server.SurfServer +import dev.slne.surf.core.api.paper.command.argument.surfBackendServerArgument +import dev.slne.surf.queue.common.queue.RedisQueueService +import dev.slne.surf.queue.paper.permission.PaperQueuePermissions +import dev.slne.surf.surfapi.bukkit.api.command.executors.anyExecutorSuspend +import dev.slne.surf.surfapi.bukkit.api.command.util.awaitAsyncPlayerProfile +import dev.slne.surf.surfapi.bukkit.api.command.util.idOrThrow +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText +import java.time.Duration +import java.time.Instant +import kotlin.time.Duration.Companion.seconds + +fun CommandAPICommand.queueInfo() = subcommand("info") { + withPermission(PaperQueuePermissions.COMMAND_INFO) + + argument(AsyncPlayerProfileArgument("player")) + surfBackendServerArgument("server", optional = true) + + anyExecutorSuspend { sender, arguments -> + val profile = arguments.awaitAsyncPlayerProfile("player") + val uuid = profile.idOrThrow() + val server: SurfServer? by arguments + val serverName = server?.name ?: SurfServer.current().name + val queue = RedisQueueService.get().get(serverName) + + val isQueued = queue.isQueued(uuid) + if (!isQueued) { + sender.sendText { + appendErrorPrefix() + error("Player ") + variableValue(profile.name ?: uuid.toString()) + error(" is not in the queue.") + } + } + + val position = queue.getPosition(uuid) + val size = queue.size() + val meta = queue.getEntryMeta(uuid) + val score = queue.getEntryScore(uuid) + val lastSeen = queue.getEntryLastSeen(uuid) + val retryCount = queue.getEntryRetryCount(uuid) + + sender.sendText { + append { + appendSuccessPrefix() + success("=== Player Info: ") + variableValue(profile.name ?: uuid.toString()) + success(" ===") + } + appendNewline { + appendSuccessPrefix() + variableKey("UUID: ") + variableValue("$uuid") + } + appendNewline { + appendSuccessPrefix() + variableKey("Position: ") + variableValue("${(position ?: -1) + 1}") + spacer(" / ") + variableValue("$size") + } + if (meta != null) { + appendNewline { + appendSuccessPrefix() + variableKey("Priority: ") + variableValue("${meta.priority}") + } + appendNewline { + val addedAt = Instant.ofEpochMilli(meta.addedAt) + val timeInQueue = Duration.between(addedAt, Instant.now()).toSeconds().seconds + appendSuccessPrefix() + variableKey("Added at: ") + variableValue(addedAt.toString()) + spacer(" (") + variableValue(timeInQueue.toString()) + success(" ago)") + } + } + if (score != null) { + appendNewline { + appendSuccessPrefix() + variableKey("Score: ") + variableValue(String.format("%.0f", score.packed)) + spacer(" (") + variableKey("priority") + spacer("=") + variableValue(score.priority) + spacer(", ") + variableKey("deltaMs") + spacer("=") + variableValue(score.deltaMs) + spacer(", ") + variableKey("seq") + spacer("=") + variableValue(score.sequence) + spacer(")") + } + } + + if (lastSeen != null) { + appendNewline { + val lastSeenAt = Instant.ofEpochMilli(lastSeen) + val timeSinceLastSeen = Duration.between(lastSeenAt, Instant.now()).toSeconds().seconds + + appendSuccessPrefix() + variableKey("Last seen: ") + variableValue(lastSeenAt.toString()) + spacer(" (") + variableValue(timeSinceLastSeen.toString()) + success(" ago)") + } + } else { + appendNewline { + appendSuccessPrefix() + variableKey("Last seen: ") + variableValue("n/a (online or no data)") + } + } + + appendNewline { + appendSuccessPrefix() + variableKey("Retry count: ") + variableValue("${retryCount ?: 0}") + } + } + } +} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueListCommand.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueListCommand.kt new file mode 100644 index 0000000..d756006 --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueListCommand.kt @@ -0,0 +1,49 @@ +package dev.slne.surf.queue.paper.commands.sub + +import dev.jorel.commandapi.CommandAPICommand +import dev.jorel.commandapi.kotlindsl.getValue +import dev.jorel.commandapi.kotlindsl.subcommand +import dev.slne.surf.core.api.common.server.SurfServer +import dev.slne.surf.core.api.paper.command.argument.surfBackendServerArgument +import dev.slne.surf.queue.common.queue.RedisQueueService +import dev.slne.surf.queue.paper.permission.PaperQueuePermissions +import dev.slne.surf.surfapi.bukkit.api.command.executors.anyExecutorSuspend +import dev.slne.surf.surfapi.core.api.messages.adventure.buildText +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText +import dev.slne.surf.surfapi.core.api.messages.pagination.Pagination +import it.unimi.dsi.fastutil.objects.Object2IntMap +import java.util.* + +private val pagination = Pagination> { + rowRenderer { value, _ -> + listOf(buildText { + val uuid = value.key + val position = value.intValue + variableKey(position) + info("—") + variableValue(uuid.toString()) + }) + } +} + +fun CommandAPICommand.queueList() = subcommand("list") { + withPermission(PaperQueuePermissions.COMMAND_LIST) + surfBackendServerArgument("server", optional = true) + + anyExecutorSuspend { sender, arguments -> + val server: SurfServer? by arguments + val serverName = server?.name ?: SurfServer.current().name + val queue = RedisQueueService.get().get(serverName) + val entries = queue.getAllUuidsWithPosition() + + if (entries.isEmpty()) { + sender.sendText { + appendInfoPrefix() + info("The queue is empty.") + } + return@anyExecutorSuspend + } + + sender.sendMessage(pagination.renderComponent(entries)) + } +} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueuePauseCommand.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueuePauseCommand.kt new file mode 100644 index 0000000..8c22385 --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueuePauseCommand.kt @@ -0,0 +1,62 @@ +package dev.slne.surf.queue.paper.commands.sub + +import dev.jorel.commandapi.CommandAPICommand +import dev.jorel.commandapi.kotlindsl.getValue +import dev.jorel.commandapi.kotlindsl.subcommand +import dev.slne.surf.core.api.common.server.SurfServer +import dev.slne.surf.core.api.paper.command.argument.surfBackendServerArgument +import dev.slne.surf.queue.common.queue.RedisQueueService +import dev.slne.surf.queue.paper.permission.PaperQueuePermissions +import dev.slne.surf.surfapi.bukkit.api.command.executors.anyExecutorSuspend +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText + +fun CommandAPICommand.queuePause() = subcommand("pause") { + withPermission(PaperQueuePermissions.COMMAND_PAUSE) + + subcommand("pause") { + surfBackendServerArgument("server", optional = true) + anyExecutorSuspend { sender, arguments -> + val server: SurfServer? by arguments + val serverName = server?.name ?: SurfServer.current().name + val queue = RedisQueueService.get().get(serverName) + queue.pause() + + sender.sendText { + appendSuccessPrefix() + success("Paused queue") + } + } + } + + subcommand("resume") { + surfBackendServerArgument("server", optional = true) + anyExecutorSuspend { sender, arguments -> + val server: SurfServer? by arguments + val serverName = server?.name ?: SurfServer.current().name + val queue = RedisQueueService.get().get(serverName) + queue.resume() + sender.sendText { + appendSuccessPrefix() + success("Resumed queue") + } + } + } + + subcommand("status") { + surfBackendServerArgument("server", optional = true) + anyExecutorSuspend { sender, arguments -> + val server: SurfServer? by arguments + val serverName = server?.name ?: SurfServer.current().name + val queue = RedisQueueService.get().get(serverName) + val isPaused = queue.isPaused() + sender.sendText { + appendSuccessPrefix() + if (isPaused) { + info("The queue is currently paused") + } else { + info("The queue is currently running") + } + } + } + } +} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/config/SurfQueueConfig.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/config/SurfQueueConfig.kt new file mode 100644 index 0000000..243a965 --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/config/SurfQueueConfig.kt @@ -0,0 +1,16 @@ +package dev.slne.surf.queue.paper.config + +import dev.slne.surf.queue.paper.plugin +import dev.slne.surf.surfapi.core.api.config.SpongeYmlConfigClass +import org.spongepowered.configurate.objectmapping.ConfigSerializable + +@ConfigSerializable +data class SurfQueueConfig( + val maxTransfersPerSecond: Int = 20, +) { + companion object : SpongeYmlConfigClass( + SurfQueueConfig::class.java, + plugin.dataPath, + "config.yml" + ) +} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/listener/PlayerKickedDueToFullServerListener.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/listener/PlayerKickedDueToFullServerListener.kt new file mode 100644 index 0000000..40bdb56 --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/listener/PlayerKickedDueToFullServerListener.kt @@ -0,0 +1,36 @@ +package dev.slne.surf.queue.paper.listener + +import com.github.benmanes.caffeine.cache.Caffeine +import com.sksamuel.aedile.core.expireAfterWrite +import io.papermc.paper.event.player.PlayerServerFullCheckEvent +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.bukkit.event.player.AsyncPlayerPreLoginEvent +import java.util.* +import kotlin.time.Duration.Companion.minutes + +object PlayerKickedDueToFullServerListener : Listener { + private val kicks = Caffeine.newBuilder() + .expireAfterWrite(2.minutes) + .maximumSize(10000) + .build() + + @EventHandler(priority = EventPriority.MONITOR) + fun onPlayerServerFullCheck(event: PlayerServerFullCheckEvent) { + if (!event.isAllowed) { + event.playerProfile.id?.let { kicks.put(it, true) } + } + } + + @EventHandler(priority = EventPriority.MONITOR) + fun onAsyncPlayerPreLogin(event: AsyncPlayerPreLoginEvent) { + if (event.loginResult == AsyncPlayerPreLoginEvent.Result.KICK_FULL) { + event.uniqueId.let { kicks.put(it, true) } + } + } + + fun consumeWasKickedDueToFullServer(uuid: UUID): Boolean { + return kicks.asMap().remove(uuid) ?: false + } +} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/metrics/QueueBstatsIntegration.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/metrics/QueueBstatsIntegration.kt new file mode 100644 index 0000000..30bec9c --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/metrics/QueueBstatsIntegration.kt @@ -0,0 +1,79 @@ +package dev.slne.surf.queue.paper.metrics + +import dev.slne.surf.queue.common.queue.RedisQueueService +import dev.slne.surf.queue.paper.plugin +import dev.slne.surf.surfapi.bukkit.api.metrics.Metrics +import dev.slne.surf.surfapi.core.api.util.logger +import kotlinx.coroutines.runBlocking +import java.util.concurrent.atomic.AtomicLong + +object QueueBstatsIntegration { + private val log = logger() + + private var lastTransfers = AtomicLong(0) + private var lastEnqueues = AtomicLong(0) + private var lastFailedTransfers = AtomicLong(0) + + fun setup() { + val metrics = Metrics(plugin, 30644) + + metrics.addCustomChart(Metrics.SimplePie("queue_count") { + try { + RedisQueueService.get().getAll().size.toString() + } catch (_: Exception) { + "0" + } + }) + + metrics.addCustomChart(Metrics.SingleLineChart("total_transfers") { + val current = QueueMetrics.totalTransfers.get() + val last = lastTransfers.getAndSet(current) + (current - last).toInt() + }) + + metrics.addCustomChart(Metrics.SingleLineChart("total_enqueues") { + val current = QueueMetrics.totalEnqueues.get() + val last = lastEnqueues.getAndSet(current) + (current - last).toInt() + }) + + metrics.addCustomChart(Metrics.SingleLineChart("total_failed_transfers") { + val current = QueueMetrics.totalFailedTransfers.get() + val last = lastFailedTransfers.getAndSet(current) + (current - last).toInt() + }) + + metrics.addCustomChart(Metrics.SingleLineChart("total_queued_players") { + try { + runBlocking { + QueueMetrics.collectQueueSizes().values.sum() + } + } catch (_: Exception) { + 0 + } + }) + + metrics.addCustomChart(Metrics.AdvancedPie("transfers_per_queue") { + try { + RedisQueueService.get().getAll() + .associate { it.serverName to QueueMetrics.getTransfersFor(it.serverName).toInt() } + .filterValues { it > 0 } + } catch (_: Exception) { + emptyMap() + } + }) + + metrics.addCustomChart(Metrics.AdvancedPie("queue_sizes") { + try { + runBlocking { + QueueMetrics.collectQueueSizes() + } + } catch (_: Exception) { + emptyMap() + } + }) + + log.atInfo() + .log("bStats integration initialized") + } +} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/metrics/QueueMetrics.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/metrics/QueueMetrics.kt new file mode 100644 index 0000000..b3797dc --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/metrics/QueueMetrics.kt @@ -0,0 +1,124 @@ +package dev.slne.surf.queue.paper.metrics + +import dev.slne.surf.queue.common.queue.RedisQueueService +import dev.slne.surf.surfapi.core.api.util.logger +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + +object QueueMetrics { + private val log = logger() + + val totalTransfers = AtomicLong(0) + val totalEnqueues = AtomicLong(0) + val totalDequeues = AtomicLong(0) + val totalFailedTransfers = AtomicLong(0) + val totalGraceExpiries = AtomicLong(0) + val totalRetryExhausted = AtomicLong(0) + val totalLockAttempts = AtomicLong(0) + val totalLockAcquired = AtomicLong(0) + val totalCleanupCycles = AtomicLong(0) + val totalCleanupRemovals = AtomicLong(0) + val totalTicks = AtomicLong(0) + + private val perQueueTransfers = ConcurrentHashMap() + private val perQueueEnqueues = ConcurrentHashMap() + private val perQueueDequeues = ConcurrentHashMap() + private val perQueueFailedTransfers = ConcurrentHashMap() + private val perQueueSkips = ConcurrentHashMap() + + fun recordTransfer(serverName: String) { + totalTransfers.incrementAndGet() + perQueueTransfers.computeIfAbsent(serverName) { AtomicLong(0) }.incrementAndGet() + } + + fun recordEnqueue(serverName: String) { + totalEnqueues.incrementAndGet() + perQueueEnqueues.computeIfAbsent(serverName) { AtomicLong(0) }.incrementAndGet() + } + + fun recordDequeue(serverName: String) { + totalDequeues.incrementAndGet() + perQueueDequeues.computeIfAbsent(serverName) { AtomicLong(0) }.incrementAndGet() + } + + fun recordFailedTransfer(serverName: String) { + totalFailedTransfers.incrementAndGet() + perQueueFailedTransfers.computeIfAbsent(serverName) { AtomicLong(0) }.incrementAndGet() + } + + fun recordSkip(serverName: String) { + perQueueSkips.computeIfAbsent(serverName) { AtomicLong(0) }.incrementAndGet() + } + + fun recordGraceExpiry() { + totalGraceExpiries.incrementAndGet() + } + + fun recordRetryExhausted() { + totalRetryExhausted.incrementAndGet() + } + + fun recordLockAttempt(acquired: Boolean) { + totalLockAttempts.incrementAndGet() + if (acquired) totalLockAcquired.incrementAndGet() + } + + fun recordCleanupCycle(removals: Int) { + totalCleanupCycles.incrementAndGet() + totalCleanupRemovals.addAndGet(removals.toLong()) + } + + fun recordTick() { + totalTicks.incrementAndGet() + } + + fun getTransfersFor(serverName: String): Long = + perQueueTransfers[serverName]?.get() ?: 0 + + fun getEnqueuesFor(serverName: String): Long = + perQueueEnqueues[serverName]?.get() ?: 0 + + fun getDequeuesFor(serverName: String): Long = + perQueueDequeues[serverName]?.get() ?: 0 + + fun getFailedTransfersFor(serverName: String): Long = + perQueueFailedTransfers[serverName]?.get() ?: 0 + + fun getSkipsFor(serverName: String): Long = + perQueueSkips[serverName]?.get() ?: 0 + + fun snapshot(): QueueMetricsSnapshot = QueueMetricsSnapshot( + totalTransfers = totalTransfers.get(), + totalEnqueues = totalEnqueues.get(), + totalDequeues = totalDequeues.get(), + totalFailedTransfers = totalFailedTransfers.get(), + totalGraceExpiries = totalGraceExpiries.get(), + totalRetryExhausted = totalRetryExhausted.get(), + totalLockAttempts = totalLockAttempts.get(), + totalLockAcquired = totalLockAcquired.get(), + totalCleanupCycles = totalCleanupCycles.get(), + totalCleanupRemovals = totalCleanupRemovals.get(), + totalTicks = totalTicks.get(), + perQueue = perQueueTransfers.keys.associateWith { serverName -> + QueueMetricsSnapshot.PerQueueMetrics( + transfers = getTransfersFor(serverName), + enqueues = getEnqueuesFor(serverName), + dequeues = getDequeuesFor(serverName), + failedTransfers = getFailedTransfersFor(serverName), + skips = getSkipsFor(serverName) + ) + } + ) + + suspend fun collectQueueSizes(): Map { + return try { + RedisQueueService.get().getAll() + .associate { it.serverName to it.size() } + } catch (e: Exception) { + log.atWarning() + .withCause(e) + .log("Failed to collect queue sizes for metrics") + emptyMap() + } + } +} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/metrics/QueueMetricsLogger.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/metrics/QueueMetricsLogger.kt new file mode 100644 index 0000000..48323aa --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/metrics/QueueMetricsLogger.kt @@ -0,0 +1,61 @@ +package dev.slne.surf.queue.paper.metrics + +import com.github.shynixn.mccoroutine.folia.launch +import dev.slne.surf.queue.paper.plugin +import dev.slne.surf.surfapi.core.api.util.logger +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlin.time.Duration.Companion.minutes + +object QueueMetricsLogger { + private val log = logger() + private var job: Job? = null + + fun start() { + job = plugin.launch { + while (isActive) { + delay(5.minutes) + try { + val snapshot = QueueMetrics.snapshot() + val queueSizes = QueueMetrics.collectQueueSizes() + + log.atInfo() + .log( + "Queue Metrics: transfers=%d, failed=%d, enqueues=%d, dequeues=%d, " + + "graceExpiries=%d, retryExhausted=%d, lockRate=%.1f%%, cleanupCycles=%d", + snapshot.totalTransfers, + snapshot.totalFailedTransfers, + snapshot.totalEnqueues, + snapshot.totalDequeues, + snapshot.totalGraceExpiries, + snapshot.totalRetryExhausted, + snapshot.lockAcquisitionRate * 100, + snapshot.totalCleanupCycles + ) + + for ((serverName, size) in queueSizes) { + val perQueue = snapshot.perQueue[serverName] + log.atInfo().log( + " Queue [%s]: size=%d, transfers=%d, failed=%d, skips=%d", + serverName, + size, + perQueue?.transfers ?: 0, + perQueue?.failedTransfers ?: 0, + perQueue?.skips ?: 0 + ) + } + } catch (e: Exception) { + log.atWarning() + .withCause(e) + .log("Failed to log metrics snapshot") + } + } + } + } + + fun stop() { + job?.cancel() + job = null + } +} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/metrics/QueueMetricsSnapshot.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/metrics/QueueMetricsSnapshot.kt new file mode 100644 index 0000000..12a8dc1 --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/metrics/QueueMetricsSnapshot.kt @@ -0,0 +1,63 @@ +package dev.slne.surf.queue.paper.metrics + +data class QueueMetricsSnapshot( + val totalTransfers: Long, + val totalEnqueues: Long, + val totalDequeues: Long, + val totalFailedTransfers: Long, + val totalGraceExpiries: Long, + val totalRetryExhausted: Long, + val totalLockAttempts: Long, + val totalLockAcquired: Long, + val totalCleanupCycles: Long, + val totalCleanupRemovals: Long, + val totalTicks: Long, + val perQueue: Map +) { + data class PerQueueMetrics( + val transfers: Long, + val enqueues: Long, + val dequeues: Long, + val failedTransfers: Long, + val skips: Long + ) + + val lockAcquisitionRate: Double + get() = if (totalLockAttempts > 0) totalLockAcquired.toDouble() / totalLockAttempts else 0.0 + + val transferSuccessRate: Double + get() { + val total = totalTransfers + totalFailedTransfers + return if (total > 0) totalTransfers.toDouble() / total else 0.0 + } + + override fun toString(): String = buildString { + appendLine("=== Queue Metrics Snapshot ===") + appendLine( + "Transfers: $totalTransfers successful, $totalFailedTransfers failed (${ + String.format( + "%.1f", + transferSuccessRate * 100 + ) + }% success)" + ) + appendLine("Enqueues: $totalEnqueues | Dequeues: $totalDequeues") + appendLine("Grace Expiries: $totalGraceExpiries | Retry Exhausted: $totalRetryExhausted") + appendLine( + "Lock: $totalLockAcquired/$totalLockAttempts acquired (${ + String.format( + "%.1f", + lockAcquisitionRate * 100 + ) + }%)" + ) + appendLine("Cleanup: $totalCleanupCycles cycles, $totalCleanupRemovals removals") + appendLine("Ticks: $totalTicks") + if (perQueue.isNotEmpty()) { + appendLine("--- Per Queue ---") + for ((name, metrics) in perQueue) { + appendLine(" $name: transfers=${metrics.transfers}, enqueues=${metrics.enqueues}, dequeues=${metrics.dequeues}, failed=${metrics.failedTransfers}, skips=${metrics.skips}") + } + } + } +} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/permission/PaperQueuePermissions.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/permission/PaperQueuePermissions.kt new file mode 100644 index 0000000..b886a93 --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/permission/PaperQueuePermissions.kt @@ -0,0 +1,17 @@ +package dev.slne.surf.queue.paper.permission + +import dev.slne.surf.surfapi.bukkit.api.permission.PermissionRegistry + +object PaperQueuePermissions : PermissionRegistry() { + private const val PREFIX = "surf.queue." + private const val COMMAND_PREFIX = PREFIX + "command" + + val COMMAND_QUEUE = create("$COMMAND_PREFIX.queue") + val COMMAND_CLEANUP = create("$COMMAND_QUEUE.cleanup") + val COMMAND_CLEAR = create("$COMMAND_QUEUE.clear") + val COMMAND_DEQUEUE = create("$COMMAND_QUEUE.dequeue") + val COMMAND_ENQUEUE = create("$COMMAND_QUEUE.enqueue") + val COMMAND_INFO = create("$COMMAND_QUEUE.info") + val COMMAND_LIST = create("$COMMAND_QUEUE.list") + val COMMAND_PAUSE = create("$COMMAND_QUEUE.pause") +} \ No newline at end of file diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/RedisQueueCleanup.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueCleanup.kt similarity index 88% rename from surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/RedisQueueCleanup.kt rename to surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueCleanup.kt index 3495d43..500e0c5 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/RedisQueueCleanup.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueCleanup.kt @@ -1,13 +1,14 @@ -package dev.slne.surf.queue.velocity.queue +package dev.slne.surf.queue.paper.queue import dev.slne.surf.queue.common.queue.RedisQueueLockManager import dev.slne.surf.queue.common.queue.RedisQueueStore -import dev.slne.surf.queue.velocity.metrics.QueueMetrics +import dev.slne.surf.queue.paper.metrics.QueueMetrics import dev.slne.surf.surfapi.core.api.util.logger import java.time.Instant +import kotlin.collections.iterator -class RedisQueueCleanup( - private val queue: VelocitySurfQueue, +class PaperQueueCleanup( + private val queue: PaperSurfQueue, private val store: RedisQueueStore, private val lockManager: RedisQueueLockManager ) { @@ -32,7 +33,7 @@ class RedisQueueCleanup( try { for ((uuid, lastSeenTime) in allLastSeen) { - if (now - lastSeenTime >= VelocitySurfQueue.GRACE_PERIOD_MS) { + if (now - lastSeenTime >= PaperSurfQueue.GRACE_PERIOD_MS) { try { queue.dequeue(uuid) removals++ diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTickTask.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTickTask.kt new file mode 100644 index 0000000..56eb0eb --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTickTask.kt @@ -0,0 +1,51 @@ +package dev.slne.surf.queue.paper.queue + +import com.github.shynixn.mccoroutine.folia.launch +import dev.slne.surf.core.api.common.SurfCoreApi +import dev.slne.surf.queue.common.queue.RedisQueueService +import dev.slne.surf.queue.paper.plugin +import dev.slne.surf.surfapi.core.api.util.logger +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlin.time.Duration.Companion.seconds + +object PaperQueueTickTask { + private val log = logger() + private var job: Job? = null + private lateinit var serverName: String + + fun start() { + this.serverName = SurfCoreApi.getCurrentServerName() + log.atInfo().log("Starting queue tick task for server: %s", serverName) + + job = plugin.launch { + while (isActive) { + delay(1.seconds) + tick() + } + } + } + + suspend fun shutdown() { + job?.cancelAndJoin() + job = null + } + + private suspend fun tick() { + val queue = RedisQueueService.get().get(serverName) as? PaperSurfQueue + if (queue == null) { + log.atWarning().log("Queue for server %s not found", serverName) + return + } + + try { + queue.tickSecond() + } catch (e: Exception) { + log.atWarning() + .withCause(e) + .log("Error during tickSecond for queue %s", serverName) + } + } +} \ No newline at end of file diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueTransfer.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTransfer.kt similarity index 71% rename from surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueTransfer.kt rename to surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTransfer.kt index ca0b8ae..1fb1f25 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueTransfer.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTransfer.kt @@ -1,43 +1,45 @@ -package dev.slne.surf.queue.velocity.queue +package dev.slne.surf.queue.paper.queue import dev.slne.surf.core.api.common.SurfCoreApi import dev.slne.surf.core.api.common.player.SurfPlayer import dev.slne.surf.core.api.common.server.SurfServer import dev.slne.surf.core.api.common.server.connection.SurfServerConnectResult -import dev.slne.surf.core.api.common.surfCoreApi +import dev.slne.surf.queue.paper.config.SurfQueueConfig +import dev.slne.surf.queue.paper.listener.PlayerKickedDueToFullServerListener import dev.slne.surf.surfapi.core.api.util.logger import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.withTimeout +import net.kyori.adventure.text.Component +import org.bukkit.Bukkit +import kotlin.math.min import kotlin.time.Duration.Companion.seconds -class QueueTransfer(private val processor: RedisQueueTransferProcessor, private val serverName: String) { +class PaperQueueTransfer(private val processor: PaperQueueTransferProcessor, private val serverName: String) { companion object { private val log = logger() } suspend fun tryTransfer(): Int { - val coreServer = surfCoreApi.getServerByName(serverName) ?: return 0 - - val playerCount = coreServer.getPlayerCount() - val maxPlayers = coreServer.maxPlayers - val availableSlots = maxPlayers - playerCount + val availableSlots = Bukkit.getMaxPlayers() - Bukkit.getOnlinePlayers().size if (availableSlots <= 0) return 0 + val coreServer = SurfCoreApi.getServerByName(serverName) ?: return 0 + val maxTransfers = min(availableSlots, SurfQueueConfig.getConfig().maxTransfersPerSecond) - return processor.processTransfers(availableSlots) { entry -> + return processor.processTransfers(maxTransfers) { entry -> try { val corePlayer = SurfCoreApi.getPlayer(entry.uuid) if (corePlayer == null) { - TransferAction.PLAYER_NOT_FOUND + TransferAction.PLAYER_NOT_FOUND to null } else { val currentPlayerServer = corePlayer.currentServer val currentPlayerServerName = currentPlayerServer?.name if (currentPlayerServer == null) { // Probably transferring to another proxy - TransferAction.PLAYER_NOT_CONNECTED_TO_A_SERVER + TransferAction.PLAYER_NOT_CONNECTED_TO_A_SERVER to null } else if (currentPlayerServerName == serverName) { - TransferAction.PLAYER_ALREADY_ON_SERVER + TransferAction.PLAYER_ALREADY_ON_SERVER to null } else { tryTransferPlayer(corePlayer, coreServer) } @@ -47,7 +49,7 @@ class QueueTransfer(private val processor: RedisQueueTransferProcessor, private log.atWarning() .withCause(e) .log("Error during transfer for queue %s", serverName) - TransferAction.ERROR + TransferAction.ERROR to null } } } @@ -55,7 +57,7 @@ class QueueTransfer(private val processor: RedisQueueTransferProcessor, private private suspend fun tryTransferPlayer( player: SurfPlayer, targetServer: SurfServer - ): TransferAction { + ): Pair { val (status, message) = try { withTimeout(30.seconds) { SurfCoreApi.sendPlayerAwaiting(player, targetServer) @@ -64,7 +66,7 @@ class QueueTransfer(private val processor: RedisQueueTransferProcessor, private log.atWarning() .withCause(e) .log("Timed out waiting for player %s to connect to server %s", player.uuid, targetServer.name) - return TransferAction.TIMEOUT + return TransferAction.TIMEOUT to null } return when (status) { @@ -73,7 +75,7 @@ class QueueTransfer(private val processor: RedisQueueTransferProcessor, private SurfServerConnectResult.Status.CONNECTION_CANCELLED -> TransferAction.PLUGIN_CANCELLED_TRANSFER SurfServerConnectResult.Status.CONNECTION_IN_PROGRESS -> TransferAction.PLAYER_ALREADY_CONNECTING SurfServerConnectResult.Status.SERVER_DISCONNECTED -> { - if (targetServer.maxPlayers <= targetServer.getPlayerCount()) { + if (PlayerKickedDueToFullServerListener.consumeWasKickedDueToFullServer(player.uuid)) { TransferAction.SERVER_FULL } else { TransferAction.PLAYER_KICKED_FROM_SERVER @@ -82,6 +84,6 @@ class QueueTransfer(private val processor: RedisQueueTransferProcessor, private SurfServerConnectResult.Status.SUCCESS -> TransferAction.DONE SurfServerConnectResult.Status.UNKNOWN_ERROR -> TransferAction.ERROR - } + } to message } } \ No newline at end of file diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/RedisQueueTransferProcessor.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTransferProcessor.kt similarity index 81% rename from surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/RedisQueueTransferProcessor.kt rename to surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTransferProcessor.kt index 5a66c13..9929bf3 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/RedisQueueTransferProcessor.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTransferProcessor.kt @@ -1,24 +1,27 @@ -package dev.slne.surf.queue.velocity.queue +package dev.slne.surf.queue.paper.queue +import dev.slne.surf.core.api.common.SurfCoreApi +import dev.slne.surf.core.api.common.util.sendText import dev.slne.surf.queue.common.queue.QueueEntry import dev.slne.surf.queue.common.queue.RedisQueueLockManager -import dev.slne.surf.queue.common.queue.RedisQueueScorePacker +import dev.slne.surf.queue.common.queue.RedisQueueScore import dev.slne.surf.queue.common.queue.RedisQueueStore -import dev.slne.surf.queue.velocity.metrics.QueueMetrics +import dev.slne.surf.queue.paper.metrics.QueueMetrics import dev.slne.surf.redis.libs.redisson.config.DecorrelatedJitterDelay import dev.slne.surf.redis.libs.redisson.config.DelayStrategy import dev.slne.surf.surfapi.core.api.util.logger +import net.kyori.adventure.text.Component import java.time.Duration import java.time.Instant import java.util.* -class RedisQueueTransferProcessor( +class PaperQueueTransferProcessor( private val serverName: String, private val store: RedisQueueStore, private val lockManager: RedisQueueLockManager, private val gracePeriodMs: Long ) { - private val transfer = QueueTransfer(this, serverName) + private val transfer = PaperQueueTransfer(this, serverName) private var delay = createDelay() private var attempts: Int = 0 private var nextTransferTime = System.currentTimeMillis() @@ -57,7 +60,7 @@ class RedisQueueTransferProcessor( suspend fun processTransfers( maxTransfers: Int, - tryTransfer: suspend (QueueEntry) -> TransferAction + tryTransfer: suspend (QueueEntry) -> Pair ): Int { return lockManager.withTransferLock { acquired -> QueueMetrics.recordLockAttempt(acquired) @@ -71,7 +74,7 @@ class RedisQueueTransferProcessor( private suspend fun doProcessTransfers( maxTransfers: Int, - tryTransfer: suspend (QueueEntry) -> TransferAction + tryTransfer: suspend (QueueEntry) -> Pair ): Int { var transferred = 0 @@ -85,7 +88,8 @@ class RedisQueueTransferProcessor( } try { - when (val result = tryTransfer(entry)) { + val (action, message) = tryTransfer(entry) + when (action) { TransferAction.DONE -> { store.dequeue(uuid) transferred++ @@ -107,7 +111,9 @@ class RedisQueueTransferProcessor( TransferAction.PLAYER_KICKED_FROM_SERVER -> { QueueMetrics.recordFailedTransfer(serverName) - retryEntry(uuid, entry, maxRetries = 5) + retryEntry(uuid, entry, maxRetries = 5) { + sendConnectionResultMessage(entry.uuid, message) + } } TransferAction.PLAYER_ALREADY_CONNECTING -> { @@ -118,7 +124,9 @@ class RedisQueueTransferProcessor( TransferAction.PLUGIN_CANCELLED_TRANSFER, TransferAction.ERROR -> { QueueMetrics.recordFailedTransfer(serverName) - retryEntry(uuid, entry, maxRetries = 3) + retryEntry(uuid, entry, maxRetries = 3) { + sendConnectionResultMessage(entry.uuid, message) + } } TransferAction.TIMEOUT -> { @@ -128,6 +136,7 @@ class RedisQueueTransferProcessor( store.dequeue(uuid) QueueMetrics.recordFailedTransfer(serverName) QueueMetrics.recordDequeue(serverName) + sendConnectionResultMessage(entry.uuid, message) log.atWarning() .log( "Player %s removed from queue %s due to transfer timeout", @@ -148,12 +157,13 @@ class RedisQueueTransferProcessor( return transferred } - private suspend fun retryEntry(uuid: UUID, meta: QueueEntry, maxRetries: Int) { + private suspend fun retryEntry(uuid: UUID, meta: QueueEntry, maxRetries: Int, onMaxRetriesReached: () -> Unit) { val retryCount = store.incrementRetryCount(uuid) if (retryCount >= maxRetries) { store.dequeue(uuid) QueueMetrics.recordRetryExhausted() QueueMetrics.recordDequeue(serverName) + onMaxRetriesReached() log.atWarning() .log("Player %s removed from queue %s after %d failed transfer attempts", uuid, serverName, retryCount) } else { @@ -208,15 +218,15 @@ class RedisQueueTransferProcessor( } val nextScoreRaw = nextEntries.first().score - val nextScore = RedisQueueScorePacker.unpack(nextScoreRaw) + val nextScore = RedisQueueScore(nextScoreRaw) - val sequenceOverflow = nextScore.sequence >= RedisQueueScorePacker.MAX_SEQUENCE + val sequenceOverflow = nextScore.sequence >= RedisQueueScore.MAX_SEQUENCE val nextSequence = if (sequenceOverflow) 0 else nextScore.sequence + 1 // On overflow, also bump deltaMs so the new score is strictly greater // than the entry we are skipping past. val nextDeltaMs = if (sequenceOverflow) nextScore.deltaMs + 1 else nextScore.deltaMs - val newScore = RedisQueueScorePacker.pack( + val newScore = RedisQueueScore.pack( meta.priority, nextDeltaMs, nextSequence @@ -225,5 +235,18 @@ class RedisQueueTransferProcessor( store.addOrUpdateScore(uuid, newScore) } + private fun sendConnectionResultMessage(uuid: UUID, message: Component?) { + try { + if (message != null) { + val player = SurfCoreApi.getPlayer(uuid) ?: return + player.sendText { append(message) } + } + } catch (e: Exception) { + log.atWarning() + .withCause(e) + .log("Failed to send connection result message for player %s", uuid) + } + } + private class AbortException : Exception() } \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperSurfQueue.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperSurfQueue.kt index dcdb3bd..69dcb90 100644 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperSurfQueue.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperSurfQueue.kt @@ -1,5 +1,56 @@ package dev.slne.surf.queue.paper.queue import dev.slne.surf.queue.common.queue.AbstractSurfQueue +import dev.slne.surf.queue.paper.metrics.QueueMetrics +import dev.slne.surf.surfapi.core.api.util.logger +import java.util.concurrent.atomic.AtomicLong +import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration.Companion.minutes -class PaperSurfQueue(serverName: String) : AbstractSurfQueue(serverName) \ No newline at end of file +class PaperSurfQueue(serverName: String) : AbstractSurfQueue(serverName) { + private val transferProcessor = PaperQueueTransferProcessor(serverName, store, lockManager, GRACE_PERIOD_MS) + private val cleanup = PaperQueueCleanup(this, store, lockManager) + + private val tickCount = AtomicLong(0) + + companion object { + private val log = logger() + val GRACE_PERIOD_MS = 1.minutes.inWholeMilliseconds + } + + fun getTickCount() = tickCount.get() + + override fun onEnqueued() { + QueueMetrics.recordEnqueue(serverName) + } + + override fun onDequeued() { + QueueMetrics.recordDequeue(serverName) + } + + suspend fun tickSecond() { + tickCount.incrementAndGet() + + safeTick("cleanup") { cleanup.tick() } + safeTick("transfers") { transferProcessor.tick() } + } + + private inline fun safeTick(component: String, block: () -> Unit) { + try { + block() + } catch (e: Exception) { + if (e is CancellationException) throw e + log.atWarning() + .withCause(e) + .log("Failed to tick %s for queue %s", component, serverName) + } + } + + suspend fun delete() { + store.deleteAll() + } + + suspend fun forceCleanup() { + cleanup.cleanupExpiredEntries() + } +} \ No newline at end of file diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/TransferAction.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/TransferAction.kt similarity index 86% rename from surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/TransferAction.kt rename to surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/TransferAction.kt index ed5727f..1a71361 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/TransferAction.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/TransferAction.kt @@ -1,4 +1,4 @@ -package dev.slne.surf.queue.velocity.queue +package dev.slne.surf.queue.paper.queue enum class TransferAction { DONE, diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/VelocitySurfQueueInstance.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/VelocitySurfQueueInstance.kt index b50aae1..37d738d 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/VelocitySurfQueueInstance.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/VelocitySurfQueueInstance.kt @@ -19,7 +19,7 @@ class VelocitySurfQueueInstance : SurfQueueInstance() { super.enable() plugin.proxy.eventManager.register(plugin, QueuePlayerListener) - QueueTickTask.startTransferring() + QueueTickTask.startTicking() queueCommand() try { diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/test/TestQueueCommands.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/test/TestQueueCommands.kt index 587b4a3..290e202 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/test/TestQueueCommands.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/test/TestQueueCommands.kt @@ -1,7 +1,6 @@ package dev.slne.surf.queue.velocity.command.test import dev.jorel.commandapi.CommandTree -import dev.jorel.commandapi.kotlindsl.anyExecutor import dev.jorel.commandapi.kotlindsl.getValue import dev.jorel.commandapi.kotlindsl.literalArgument import dev.slne.surf.core.api.common.player.SurfPlayer @@ -50,7 +49,7 @@ fun CommandTree.testQueueCommands() = literalArgument("test-queue") { literalArgument("start") { anyExecutorSuspend { source, arguments -> QueueTickTask.shutdown() - QueueTickTask.startTransferring() + QueueTickTask.startTicking() source.sendText { appendSuccessPrefix() success("Started transfer task") diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/display/QueueDisplay.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueDisplay.kt similarity index 93% rename from surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/display/QueueDisplay.kt rename to surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueDisplay.kt index 7d6724a..2514f82 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/display/QueueDisplay.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueDisplay.kt @@ -1,10 +1,9 @@ -package dev.slne.surf.queue.velocity.queue.display +package dev.slne.surf.queue.velocity.queue -import dev.slne.surf.queue.velocity.queue.VelocitySurfQueue import dev.slne.surf.queue.velocity.util.toVelocityPlayer import dev.slne.surf.surfapi.core.api.messages.adventure.buildText import it.unimi.dsi.fastutil.objects.Object2IntMap -import java.util.* +import java.util.UUID class QueueDisplay(private val queue: VelocitySurfQueue) { @@ -26,7 +25,7 @@ class QueueDisplay(private val queue: VelocitySurfQueue) { private suspend fun updateActionBars() { val uuidsWithPosition = cachedUuidsWithPosition ?: return - val spinnerIndex = (queue.getTickCount() % spinner.size).toInt() + val spinnerIndex = (queue.getTickCount() % spinner.size) val spinnerEnd = spinner[spinnerIndex] val spinnerStart = spinnerReversed[spinnerIndex] val paused = queue.isPaused() diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueTickTask.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueTickTask.kt index 47dce93..370276e 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueTickTask.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueTickTask.kt @@ -14,7 +14,7 @@ object QueueTickTask { private var lastFetch = 0L - fun startTransferring() { + fun startTicking() { job = plugin.container.launch { while (isActive) { delay(1.seconds) diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/VelocitySurfQueue.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/VelocitySurfQueue.kt index 0fe0173..9f3dc5b 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/VelocitySurfQueue.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/VelocitySurfQueue.kt @@ -2,27 +2,17 @@ package dev.slne.surf.queue.velocity.queue import dev.slne.surf.queue.common.queue.AbstractSurfQueue import dev.slne.surf.queue.velocity.metrics.QueueMetrics -import dev.slne.surf.queue.velocity.queue.display.QueueDisplay import dev.slne.surf.surfapi.core.api.util.logger import java.time.Instant import java.util.* -import java.util.concurrent.atomic.AtomicLong -import kotlin.coroutines.cancellation.CancellationException -import kotlin.time.Duration.Companion.minutes +import java.util.concurrent.atomic.AtomicInteger class VelocitySurfQueue(serverName: String) : AbstractSurfQueue(serverName) { - private val transferProcessor = RedisQueueTransferProcessor(serverName, store, lockManager, GRACE_PERIOD_MS) - private val cleanup = RedisQueueCleanup(this, store, lockManager) - - private val tickCount = AtomicLong(0) - val display = QueueDisplay(this) + private val ticks = AtomicInteger(0) companion object { private val log = logger() - - val GRACE_PERIOD_MS = 1.minutes.inWholeMilliseconds - const val LOCK_LEASE_SECONDS = 30L } override fun onEnqueued() { @@ -41,30 +31,16 @@ class VelocitySurfQueue(serverName: String) : AbstractSurfQueue(serverName) { store.putLastSeen(uuid, Instant.now().toEpochMilli()) } - fun getTickCount() = tickCount.get() + fun getTickCount(): Int = ticks.get() suspend fun tickSecond() { - tickCount.incrementAndGet() - QueueMetrics.recordTick() - - safeTick("cleanup") { cleanup.tick() } - safeTick("transfers") { transferProcessor.tick() } - safeTick("display") { display.tick() } - } - - private inline fun safeTick(component: String, block: () -> Unit) { try { - block() + ticks.incrementAndGet() + display.tick() } catch (e: Exception) { - if (e is CancellationException) throw e log.atWarning() .withCause(e) - .log("Failed to tick %s for queue %s", component, serverName) + .log("Error during tickSecond for queue %s", serverName) } } - - suspend fun delete() { - store.deleteAll() - } - } \ No newline at end of file From 2a5622025ef3e81b19e17bb8d3b3470f003e67a5 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Thu, 9 Apr 2026 18:52:27 +0200 Subject: [PATCH 5/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(queue):=20ren?= =?UTF-8?q?ame=20command=20tree=20for=20velocity=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - change command tree name from "squeue" to "squeue-velocity" for clarity --- .../kotlin/dev/slne/surf/queue/velocity/command/QueueCommand.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/QueueCommand.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/QueueCommand.kt index a4e2a0f..00ab39a 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/QueueCommand.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/QueueCommand.kt @@ -6,7 +6,7 @@ import dev.slne.surf.queue.velocity.command.pause.queuePauseCommand import dev.slne.surf.queue.velocity.command.test.testQueueCommands import dev.slne.surf.queue.velocity.permission.SurfQueuePermissions -fun queueCommand() = commandTree("squeue") { +fun queueCommand() = commandTree("squeue-velocity") { withPermission(SurfQueuePermissions.COMMAND_QUEUE) testQueueCommands() metricsCommand() From d70aa9293ac74c78dab8a7f9b51ba4090deac12a Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sat, 11 Apr 2026 13:32:54 +0200 Subject: [PATCH 6/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(queue):=20ren?= =?UTF-8?q?ame=20AbstractSurfQueue=20to=20AbstractQueue=20and=20related=20?= =?UTF-8?q?updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rename AbstractSurfQueue to AbstractQueue for consistency - update references in PaperSurfQueue and related files - introduce QueueTicker for managing queue ticks - implement cleanup logic in PaperQueueCleanup - enhance performance with optimized UUID handling in queue methods --- .../dev/slne/surf/queue/api/SurfQueue.kt | 9 +- ...{SurfQueueInstance.kt => QueueInstance.kt} | 11 +- ...{AbstractSurfQueue.kt => AbstractQueue.kt} | 42 +++++- .../surf/queue/common/queue/QueueTicker.kt | 40 ++++++ .../queue/common/queue/RedisQueueService.kt | 27 ++-- .../queue/common/queue/tick/SafeQueueTick.kt | 20 +++ .../surf/queue/common/redis/RedisInstance.kt | 5 + .../dev/slne/surf/queue/paper/PaperMain.kt | 9 +- .../queue/paper/PaperSurfQueueInstance.kt | 23 ++-- .../surf/queue/paper/commands/QueueCommand.kt | 2 + .../paper/commands/sub/QueueCleanupCommand.kt | 5 +- .../paper/commands/sub/QueueClearCommand.kt | 5 +- .../paper/commands/sub/QueueMetricsCommand.kt | 24 ++-- .../hook/startup/DefaultQueueStartHook.kt | 1 + .../paper/permission/PaperQueuePermissions.kt | 1 + .../queue/paper/queue/PaperQueueCleanup.kt | 66 --------- .../surf/queue/paper/queue/PaperQueueImpl.kt | 44 ++++++ .../queue/paper/queue/PaperQueueTickTask.kt | 51 ------- .../surf/queue/paper/queue/PaperSurfQueue.kt | 56 -------- .../paper/queue/cleanup/PaperQueueCleanup.kt | 80 +++++++++++ .../{ => transfer}/PaperQueueTransfer.kt | 52 ++++---- .../PaperQueueTransferProcessor.kt | 18 ++- .../queue/{ => transfer}/TransferAction.kt | 2 +- .../queue/paper/redis/PaperRedisInstance.kt | 7 - .../slne/surf/queue/velocity/VelocityMain.kt | 12 +- .../velocity/VelocitySurfQueueInstance.kt | 36 +---- .../queue/velocity/command/QueueCommand.kt | 4 - .../command/test/TestQueueCommands.kt | 74 ---------- .../metrics/QueueBstatsIntegration.kt | 81 ----------- .../queue/velocity/metrics/QueueMetrics.kt | 126 ------------------ .../velocity/metrics/QueueMetricsLogger.kt | 61 --------- .../velocity/metrics/QueueMetricsSnapshot.kt | 63 --------- .../permission/SurfQueuePermissions.kt | 2 - .../surf/queue/velocity/queue/QueueDisplay.kt | 18 ++- .../queue/velocity/queue/QueueTickTask.kt | 53 -------- .../queue/velocity/queue/VelocitySurfQueue.kt | 35 +---- .../velocity/redis/VelocityRedisInstance.kt | 11 -- 37 files changed, 352 insertions(+), 824 deletions(-) rename surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/{SurfQueueInstance.kt => QueueInstance.kt} (71%) rename surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/{AbstractSurfQueue.kt => AbstractQueue.kt} (74%) create mode 100644 surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/QueueTicker.kt create mode 100644 surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/tick/SafeQueueTick.kt rename surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/metrics/MetricsCommand.kt => surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueMetricsCommand.kt (84%) delete mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueCleanup.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueImpl.kt delete mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTickTask.kt delete mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperSurfQueue.kt create mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/cleanup/PaperQueueCleanup.kt rename surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/{ => transfer}/PaperQueueTransfer.kt (65%) rename surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/{ => transfer}/PaperQueueTransferProcessor.kt (96%) rename surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/{ => transfer}/TransferAction.kt (85%) delete mode 100644 surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/redis/PaperRedisInstance.kt delete mode 100644 surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/test/TestQueueCommands.kt delete mode 100644 surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/metrics/QueueBstatsIntegration.kt delete mode 100644 surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/metrics/QueueMetrics.kt delete mode 100644 surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/metrics/QueueMetricsLogger.kt delete mode 100644 surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/metrics/QueueMetricsSnapshot.kt delete mode 100644 surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueTickTask.kt delete mode 100644 surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/redis/VelocityRedisInstance.kt diff --git a/surf-queue-api/src/main/kotlin/dev/slne/surf/queue/api/SurfQueue.kt b/surf-queue-api/src/main/kotlin/dev/slne/surf/queue/api/SurfQueue.kt index f32707f..628862e 100644 --- a/surf-queue-api/src/main/kotlin/dev/slne/surf/queue/api/SurfQueue.kt +++ b/surf-queue-api/src/main/kotlin/dev/slne/surf/queue/api/SurfQueue.kt @@ -4,6 +4,7 @@ import dev.slne.surf.core.api.common.SurfCoreApi import dev.slne.surf.core.api.common.server.SurfServer import dev.slne.surf.queue.api.service.SurfQueueService import it.unimi.dsi.fastutil.objects.Object2IntMap +import it.unimi.dsi.fastutil.objects.ObjectList import java.util.* interface SurfQueue { @@ -22,7 +23,13 @@ interface SurfQueue { suspend fun pause() suspend fun resume() - suspend fun getAllUuidsWithPosition(): Collection> + @Deprecated( + "Use getAllUuidsOrderedByPosition for better performance", + ReplaceWith("getAllUuidsOrderedByPosition()") + ) + suspend fun getAllUuidsWithPosition(): ObjectList> + + suspend fun getAllUuidsOrderedByPosition(): ObjectList @OptIn(InternalSurfQueueApi::class) companion object { diff --git a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/SurfQueueInstance.kt b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/QueueInstance.kt similarity index 71% rename from surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/SurfQueueInstance.kt rename to surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/QueueInstance.kt index 6ce476f..2f9c2fa 100644 --- a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/SurfQueueInstance.kt +++ b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/QueueInstance.kt @@ -1,13 +1,14 @@ package dev.slne.surf.queue.common -import dev.slne.surf.queue.common.queue.AbstractSurfQueue +import dev.slne.surf.queue.common.queue.AbstractQueue +import dev.slne.surf.queue.common.queue.QueueTicker import dev.slne.surf.queue.common.queue.RedisQueueService import dev.slne.surf.queue.common.redis.RedisInstance import dev.slne.surf.surfapi.core.api.component.SurfComponentApi import dev.slne.surf.surfapi.core.api.util.requiredService import org.jetbrains.annotations.MustBeInvokedByOverriders -abstract class SurfQueueInstance { +abstract class QueueInstance { // Implementations are responsible for starting the queue ticker task protected abstract val componentOwner: Any @MustBeInvokedByOverriders @@ -24,14 +25,16 @@ abstract class SurfQueueInstance { @MustBeInvokedByOverriders open suspend fun disable() { + QueueTicker.dispose() + SurfComponentApi.disable(componentOwner) RedisInstance.get().disconnect() } - abstract fun createQueue(serverName: String): AbstractSurfQueue + abstract fun createQueue(serverName: String): AbstractQueue companion object { - val instance = requiredService() + val instance = requiredService() fun get() = instance } } \ No newline at end of file diff --git a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/AbstractSurfQueue.kt b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/AbstractQueue.kt similarity index 74% rename from surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/AbstractSurfQueue.kt rename to surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/AbstractQueue.kt index ad6e61c..bd09658 100644 --- a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/AbstractSurfQueue.kt +++ b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/AbstractQueue.kt @@ -4,11 +4,14 @@ import dev.slne.surf.queue.api.SurfQueue import dev.slne.surf.queue.common.hook.priority.LuckpermsPriorityHook import dev.slne.surf.surfapi.core.api.util.logger import it.unimi.dsi.fastutil.objects.Object2IntMap +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import it.unimi.dsi.fastutil.objects.ObjectList +import org.jetbrains.annotations.MustBeInvokedByOverriders import java.time.Instant import java.util.* import java.util.concurrent.atomic.AtomicInteger -abstract class AbstractSurfQueue(override val serverName: String) : SurfQueue { +abstract class AbstractQueue(override val serverName: String) : SurfQueue { protected val keys = RedisQueueKeys(serverName) protected val store = RedisQueueStore(keys) protected val lockManager = RedisQueueLockManager(keys) @@ -22,6 +25,9 @@ abstract class AbstractSurfQueue(override val serverName: String) : SurfQueue { */ private val enqueueSequence = AtomicInteger(0) + var tickCount = 0 + private set + companion object { private val log = logger() @@ -42,6 +48,11 @@ abstract class AbstractSurfQueue(override val serverName: String) : SurfQueue { } } + @MustBeInvokedByOverriders + open suspend fun tick() { + tickCount++ + } + override suspend fun enqueue(uuid: UUID): Boolean { val priority = LuckpermsPriorityHook.getPriority(uuid) return enqueue(uuid, priority) @@ -91,11 +102,30 @@ abstract class AbstractSurfQueue(override val serverName: String) : SurfQueue { return store.rank(uuid) } - override suspend fun getAllUuidsWithPosition(): Collection> { - return store.readAllEntries() - .mapIndexed { index, entry -> - Object2IntMap.entry(entry.value, index + 1) - } + @Deprecated( + "Use getAllUuidsOrderedByPosition for better performance", + replaceWith = ReplaceWith("getAllUuidsOrderedByPosition()") + ) + override suspend fun getAllUuidsWithPosition(): ObjectList> { + val entries = store.readAllEntries() + val uuidsWithPosition = ObjectArrayList>(entries.size) + + for ((index, entry) in entries.withIndex()) { + uuidsWithPosition.add(Object2IntMap.entry(entry.value, index + 1)) + } + + return uuidsWithPosition + } + + override suspend fun getAllUuidsOrderedByPosition(): ObjectList { + val entries = store.readAllEntries() + val uuids = ObjectArrayList(entries.size) + + for (entry in entries) { + uuids.add(entry.value) + } + + return uuids } override suspend fun size(): Int { diff --git a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/QueueTicker.kt b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/QueueTicker.kt new file mode 100644 index 0000000..3e733fd --- /dev/null +++ b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/QueueTicker.kt @@ -0,0 +1,40 @@ +package dev.slne.surf.queue.common.queue + +import dev.slne.surf.queue.common.queue.tick.SafeQueueTick +import dev.slne.surf.surfapi.core.api.util.logger +import kotlinx.coroutines.* +import kotlin.time.Duration.Companion.seconds + +@OptIn(ExperimentalCoroutinesApi::class) +object QueueTicker { + private val log = logger() + private val queueTickerScope = + CoroutineScope(newSingleThreadContext("QueueTicker") + CoroutineExceptionHandler { _, throwable -> + log.atSevere() + .withCause(throwable) + .log("Unhandled exception in QueueTicker:") + }) + + fun start() { + var secondsElapsed = 0 + + queueTickerScope.launch { + delay(1.seconds) + secondsElapsed++ + + if (secondsElapsed % RedisQueueService.QUEUE_REFRESH_INTERVAL_SECONDS == 0) { + RedisQueueService.get().fetchFromRedis() + } + + for (queue in RedisQueueService.get().getAll()) { + SafeQueueTick.tickSafe(queue, "heartbeat") { + queue.tick() + } + } + } + } + + fun dispose() { + queueTickerScope.cancel("QueueTicker disposed") + } +} \ No newline at end of file diff --git a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueService.kt b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueService.kt index 43a9aec..fc8a029 100644 --- a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueService.kt +++ b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/RedisQueueService.kt @@ -4,17 +4,18 @@ import com.github.benmanes.caffeine.cache.Caffeine import com.google.auto.service.AutoService import dev.slne.surf.queue.api.InternalSurfQueueApi import dev.slne.surf.queue.api.service.SurfQueueService -import dev.slne.surf.queue.common.SurfQueueInstance +import dev.slne.surf.queue.common.QueueInstance import dev.slne.surf.queue.common.redis.redisApi import dev.slne.surf.redis.libs.redisson.api.options.KeysScanOptions -import dev.slne.surf.surfapi.core.api.util.logger -import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.reactive.collect +import java.time.Duration @OptIn(InternalSurfQueueApi::class) @AutoService(SurfQueueService::class) class RedisQueueService : SurfQueueService { private val queues = Caffeine.newBuilder() - .build { serverName -> SurfQueueInstance.get().createQueue(serverName) } + .expireAfterWrite(Duration.ofSeconds(QUEUE_REFRESH_INTERVAL_SECONDS * 4L)) + .build { serverName -> QueueInstance.get().createQueue(serverName) } override fun get(serverName: String) = queues.get(serverName) fun getAll() = queues.asMap().values @@ -26,20 +27,16 @@ class RedisQueueService : SurfQueueService { .getKeys( KeysScanOptions.defaults() .pattern(RedisQueueKeys.EPOCH_MS_KEY_PATTERN) - ).asFlow() - .collect { - val serverName = - it.replaceFirst(RedisQueueKeys.QUEUE_PREFIX, "").replaceFirst(RedisQueueKeys.EPOCH_MS_SUFFIX, "") - - log.atInfo() - .log("Found queue for server $serverName in Redis, fetching...") - - get(serverName) - } + ).map(::extractServerName) + .collect(::get) } + private fun extractServerName(key: String) = key + .replaceFirst(RedisQueueKeys.QUEUE_PREFIX, "") + .replaceFirst(RedisQueueKeys.EPOCH_MS_SUFFIX, "") + companion object { - private val log = logger() + const val QUEUE_REFRESH_INTERVAL_SECONDS = 30 fun get() = SurfQueueService.instance as RedisQueueService } } \ No newline at end of file diff --git a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/tick/SafeQueueTick.kt b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/tick/SafeQueueTick.kt new file mode 100644 index 0000000..4c99373 --- /dev/null +++ b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/tick/SafeQueueTick.kt @@ -0,0 +1,20 @@ +package dev.slne.surf.queue.common.queue.tick + +import dev.slne.surf.queue.common.queue.AbstractQueue +import dev.slne.surf.surfapi.core.api.util.logger +import kotlin.coroutines.cancellation.CancellationException + +object SafeQueueTick { + val log = logger() + + inline fun tickSafe(queue: AbstractQueue, component: String, block: () -> Unit) { + try { + block() + } catch (e: Exception) { + if (e is CancellationException) throw e + log.atWarning() + .withCause(e) + .log("Failed to tick %s for queue %s", component, queue.serverName) + } + } +} \ No newline at end of file diff --git a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/redis/RedisInstance.kt b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/redis/RedisInstance.kt index 9a60b7c..a797c79 100644 --- a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/redis/RedisInstance.kt +++ b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/redis/RedisInstance.kt @@ -1,7 +1,9 @@ package dev.slne.surf.queue.common.redis +import com.google.auto.service.AutoService import dev.slne.surf.redis.RedisApi import dev.slne.surf.surfapi.core.api.util.requiredService +import net.kyori.adventure.util.Services import org.jetbrains.annotations.MustBeInvokedByOverriders abstract class RedisInstance { @@ -26,6 +28,9 @@ abstract class RedisInstance { fun get() = instance fun namespaced(key: String) = "surf-queue:$key" + + @AutoService(RedisInstance::class) + class Fallback : RedisInstance(), Services.Fallback } } diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperMain.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperMain.kt index 499381b..ee3c4c0 100644 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperMain.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperMain.kt @@ -1,21 +1,20 @@ package dev.slne.surf.queue.paper import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin -import dev.slne.surf.queue.common.SurfQueueInstance -import dev.slne.surf.surfapi.core.api.component.surfComponentApi +import dev.slne.surf.queue.common.QueueInstance import org.bukkit.plugin.java.JavaPlugin class PaperMain : SuspendingJavaPlugin() { override suspend fun onLoadAsync() { - SurfQueueInstance.get().load() + QueueInstance.get().load() } override suspend fun onEnableAsync() { - SurfQueueInstance.get().enable() + QueueInstance.get().enable() } override suspend fun onDisableAsync() { - SurfQueueInstance.get().disable() + QueueInstance.get().disable() } } diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperSurfQueueInstance.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperSurfQueueInstance.kt index 8004413..8a3ac7f 100644 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperSurfQueueInstance.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/PaperSurfQueueInstance.kt @@ -1,38 +1,41 @@ package dev.slne.surf.queue.paper import com.google.auto.service.AutoService -import dev.slne.surf.queue.common.SurfQueueInstance -import dev.slne.surf.queue.common.queue.AbstractSurfQueue +import dev.slne.surf.queue.common.QueueInstance +import dev.slne.surf.queue.common.queue.AbstractQueue +import dev.slne.surf.queue.common.queue.QueueTicker +import dev.slne.surf.queue.paper.commands.queueCommand import dev.slne.surf.queue.paper.config.SurfQueueConfig import dev.slne.surf.queue.paper.hook.startup.QueueStartHook import dev.slne.surf.queue.paper.listener.PlayerKickedDueToFullServerListener -import dev.slne.surf.queue.paper.queue.PaperQueueTickTask -import dev.slne.surf.queue.paper.queue.PaperSurfQueue +import dev.slne.surf.queue.paper.metrics.QueueMetricsLogger +import dev.slne.surf.queue.paper.queue.PaperQueueImpl import dev.slne.surf.surfapi.bukkit.api.event.register -@AutoService(SurfQueueInstance::class) -class PaperSurfQueueInstance : SurfQueueInstance() { +@AutoService(QueueInstance::class) +class PaperSurfQueueInstance : QueueInstance() { override val componentOwner get() = plugin override suspend fun load() { SurfQueueConfig.init() super.load() QueueStartHook.get().onServerReady { - PaperQueueTickTask.start() + QueueTicker.start() } } override suspend fun enable() { super.enable() PlayerKickedDueToFullServerListener.register() + queueCommand() } override suspend fun disable() { - PaperQueueTickTask.shutdown() super.disable() + QueueMetricsLogger.stop() } - override fun createQueue(serverName: String): AbstractSurfQueue { - return PaperSurfQueue(serverName) + override fun createQueue(serverName: String): AbstractQueue { + return PaperQueueImpl(serverName) } } \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/QueueCommand.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/QueueCommand.kt index def638c..9c3b416 100644 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/QueueCommand.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/QueueCommand.kt @@ -1,6 +1,7 @@ package dev.slne.surf.queue.paper.commands import dev.jorel.commandapi.kotlindsl.commandAPICommand +import dev.slne.surf.queue.paper.commands.sub.metricsCommand import dev.slne.surf.queue.paper.commands.sub.queueDequeue import dev.slne.surf.queue.paper.commands.sub.queueCleanup import dev.slne.surf.queue.paper.commands.sub.queueClear @@ -18,4 +19,5 @@ fun queueCommand() = commandAPICommand("squeue") { queueEnqueue() queueInfo() queueList() + metricsCommand() } \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueCleanupCommand.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueCleanupCommand.kt index 423fcc2..b37f896 100644 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueCleanupCommand.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueCleanupCommand.kt @@ -3,12 +3,11 @@ package dev.slne.surf.queue.paper.commands.sub import dev.jorel.commandapi.CommandAPICommand import dev.jorel.commandapi.kotlindsl.getValue import dev.jorel.commandapi.kotlindsl.subcommand -import dev.slne.surf.core.api.common.SurfCoreApi import dev.slne.surf.core.api.common.server.SurfServer import dev.slne.surf.core.api.paper.command.argument.surfBackendServerArgument import dev.slne.surf.queue.common.queue.RedisQueueService import dev.slne.surf.queue.paper.permission.PaperQueuePermissions -import dev.slne.surf.queue.paper.queue.PaperSurfQueue +import dev.slne.surf.queue.paper.queue.PaperQueueImpl import dev.slne.surf.surfapi.bukkit.api.command.executors.anyExecutorSuspend import dev.slne.surf.surfapi.core.api.messages.adventure.sendText @@ -19,7 +18,7 @@ fun CommandAPICommand.queueCleanup() = subcommand("cleanup") { anyExecutorSuspend { sender, arguments -> val server: SurfServer? by arguments val serverName = server?.name ?: SurfServer.current().name - val queue = RedisQueueService.get().get(serverName) as PaperSurfQueue + val queue = RedisQueueService.get().get(serverName) as PaperQueueImpl val sizeBefore = queue.size() queue.forceCleanup() diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueClearCommand.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueClearCommand.kt index 4a58681..7bd58ce 100644 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueClearCommand.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueClearCommand.kt @@ -3,12 +3,11 @@ package dev.slne.surf.queue.paper.commands.sub import dev.jorel.commandapi.CommandAPICommand import dev.jorel.commandapi.kotlindsl.getValue import dev.jorel.commandapi.kotlindsl.subcommand -import dev.slne.surf.core.api.common.SurfCoreApi import dev.slne.surf.core.api.common.server.SurfServer import dev.slne.surf.core.api.paper.command.argument.surfBackendServerArgument import dev.slne.surf.queue.common.queue.RedisQueueService import dev.slne.surf.queue.paper.permission.PaperQueuePermissions -import dev.slne.surf.queue.paper.queue.PaperSurfQueue +import dev.slne.surf.queue.paper.queue.PaperQueueImpl import dev.slne.surf.surfapi.bukkit.api.command.executors.anyExecutorSuspend import dev.slne.surf.surfapi.core.api.messages.adventure.sendText @@ -19,7 +18,7 @@ fun CommandAPICommand.queueClear() = subcommand("clear") { anyExecutorSuspend { sender, arguments -> val server: SurfServer? by arguments val serverName = server?.name ?: SurfServer.current().name - val queue = RedisQueueService.get().get(serverName) as PaperSurfQueue + val queue = RedisQueueService.get().get(serverName) as PaperQueueImpl val size = queue.size() queue.delete() diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/metrics/MetricsCommand.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueMetricsCommand.kt similarity index 84% rename from surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/metrics/MetricsCommand.kt rename to surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueMetricsCommand.kt index 059f755..d0a38cc 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/metrics/MetricsCommand.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/commands/sub/QueueMetricsCommand.kt @@ -1,18 +1,18 @@ -package dev.slne.surf.queue.velocity.command.metrics +package dev.slne.surf.queue.paper.commands.sub -import dev.jorel.commandapi.CommandTree +import dev.jorel.commandapi.CommandAPICommand import dev.jorel.commandapi.kotlindsl.anyExecutor -import dev.jorel.commandapi.kotlindsl.literalArgument -import dev.slne.surf.queue.velocity.metrics.QueueMetrics -import dev.slne.surf.queue.velocity.metrics.QueueMetricsLogger -import dev.slne.surf.queue.velocity.permission.SurfQueuePermissions +import dev.jorel.commandapi.kotlindsl.subcommand +import dev.slne.surf.queue.paper.metrics.QueueMetrics +import dev.slne.surf.queue.paper.metrics.QueueMetricsLogger +import dev.slne.surf.queue.paper.permission.PaperQueuePermissions +import dev.slne.surf.surfapi.bukkit.api.command.executors.anyExecutorSuspend import dev.slne.surf.surfapi.core.api.messages.adventure.sendText -import dev.slne.surf.surfapi.velocity.api.command.executors.anyExecutorSuspend -fun CommandTree.metricsCommand() = literalArgument("metrics") { - withPermission(SurfQueuePermissions.COMMAND_METRICS) +fun CommandAPICommand.metricsCommand() = subcommand("metrics") { + withPermission(PaperQueuePermissions.COMMAND_METRICS) - literalArgument("startLogging") { + subcommand("startLogging") { anyExecutor { source, arguments -> QueueMetricsLogger.stop() QueueMetricsLogger.start() @@ -23,7 +23,7 @@ fun CommandTree.metricsCommand() = literalArgument("metrics") { } } - literalArgument("stopLogging") { + subcommand("stopLogging") { anyExecutor { source, arguments -> QueueMetricsLogger.stop() source.sendText { @@ -33,7 +33,7 @@ fun CommandTree.metricsCommand() = literalArgument("metrics") { } } - literalArgument("snapshot") { + subcommand("snapshot") { anyExecutorSuspend { sender, _ -> val snapshot = QueueMetrics.snapshot() val queueSizes = QueueMetrics.collectQueueSizes() diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/DefaultQueueStartHook.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/DefaultQueueStartHook.kt index cba0636..2d217d4 100644 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/DefaultQueueStartHook.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/hook/startup/DefaultQueueStartHook.kt @@ -6,6 +6,7 @@ import dev.slne.surf.surfapi.shared.api.component.requirement.ConditionalOnMissi @ComponentMeta @ConditionalOnMissingComponent(QueueStartHook::class) class DefaultQueueStartHook : QueueStartHook() { + override suspend fun onEnable() { runServerReadyTasks() } diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/permission/PaperQueuePermissions.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/permission/PaperQueuePermissions.kt index b886a93..42b880f 100644 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/permission/PaperQueuePermissions.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/permission/PaperQueuePermissions.kt @@ -14,4 +14,5 @@ object PaperQueuePermissions : PermissionRegistry() { val COMMAND_INFO = create("$COMMAND_QUEUE.info") val COMMAND_LIST = create("$COMMAND_QUEUE.list") val COMMAND_PAUSE = create("$COMMAND_QUEUE.pause") + val COMMAND_METRICS = create("$COMMAND_PREFIX.metrics") } \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueCleanup.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueCleanup.kt deleted file mode 100644 index 500e0c5..0000000 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueCleanup.kt +++ /dev/null @@ -1,66 +0,0 @@ -package dev.slne.surf.queue.paper.queue - -import dev.slne.surf.queue.common.queue.RedisQueueLockManager -import dev.slne.surf.queue.common.queue.RedisQueueStore -import dev.slne.surf.queue.paper.metrics.QueueMetrics -import dev.slne.surf.surfapi.core.api.util.logger -import java.time.Instant -import kotlin.collections.iterator - -class PaperQueueCleanup( - private val queue: PaperSurfQueue, - private val store: RedisQueueStore, - private val lockManager: RedisQueueLockManager -) { - - companion object { - private val log = logger() - private const val CLEANUP_INTERVAL_TICKS = 10L - } - - suspend fun tick() { - if (queue.getTickCount() % CLEANUP_INTERVAL_TICKS == 0L) { - lockManager.withCleanupLock { - cleanupExpiredEntries() - } - } - } - - suspend fun cleanupExpiredEntries() { - val now = Instant.now().toEpochMilli() - val allLastSeen = store.readAllLastSeen() - var removals = 0 - - try { - for ((uuid, lastSeenTime) in allLastSeen) { - if (now - lastSeenTime >= PaperSurfQueue.GRACE_PERIOD_MS) { - try { - queue.dequeue(uuid) - removals++ - log.atInfo() - .log("Cleanup: removed expired entry %s from queue %s", uuid, queue.serverName) - } catch (e: Exception) { - log.atWarning() - .withCause(e) - .log("Cleanup: dequeue failed for %s in queue %s, attempting forced removal", uuid, queue.serverName) - - try { - store.removeAllFor(uuid) - removals++ - } catch (e2: Exception) { - log.atWarning() - .withCause(e2) - .log("Cleanup: forced removal also failed for %s in queue %s", uuid, queue.serverName) - } - } - } - } - } catch (e: Exception) { - log.atWarning() - .withCause(e) - .log("Failed to cleanup expired entries for queue %s", queue.serverName) - } - - QueueMetrics.recordCleanupCycle(removals) - } -} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueImpl.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueImpl.kt new file mode 100644 index 0000000..fbd0758 --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueImpl.kt @@ -0,0 +1,44 @@ +package dev.slne.surf.queue.paper.queue + +import dev.slne.surf.core.api.common.server.SurfServer +import dev.slne.surf.queue.common.queue.AbstractQueue +import dev.slne.surf.queue.common.queue.tick.SafeQueueTick +import dev.slne.surf.queue.paper.metrics.QueueMetrics +import dev.slne.surf.queue.paper.queue.cleanup.PaperQueueCleanup +import dev.slne.surf.queue.paper.queue.transfer.PaperQueueTransferProcessor +import kotlin.time.Duration.Companion.minutes + +class PaperQueueImpl(serverName: String) : AbstractQueue(serverName) { + private val transferProcessor = PaperQueueTransferProcessor(serverName, store, lockManager, GRACE_PERIOD_MS) + private val cleanup = PaperQueueCleanup(this, store, lockManager) + + private val isTargetServer = SurfServer.current().name == serverName + + companion object { + val GRACE_PERIOD_MS = 1.minutes.inWholeMilliseconds + } + + override fun onEnqueued() { + QueueMetrics.recordEnqueue(serverName) + } + + override fun onDequeued() { + QueueMetrics.recordDequeue(serverName) + } + + suspend fun tickSecond() { + if (isTargetServer) { + QueueMetrics.recordTick() + SafeQueueTick.tickSafe(this, "cleanup") { cleanup.tick() } + SafeQueueTick.tickSafe(this, "transfers") { transferProcessor.tick() } + } + } + + suspend fun delete() { + store.deleteAll() + } + + suspend fun forceCleanup() { + cleanup.cleanupExpiredEntries() + } +} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTickTask.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTickTask.kt deleted file mode 100644 index 56eb0eb..0000000 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTickTask.kt +++ /dev/null @@ -1,51 +0,0 @@ -package dev.slne.surf.queue.paper.queue - -import com.github.shynixn.mccoroutine.folia.launch -import dev.slne.surf.core.api.common.SurfCoreApi -import dev.slne.surf.queue.common.queue.RedisQueueService -import dev.slne.surf.queue.paper.plugin -import dev.slne.surf.surfapi.core.api.util.logger -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlin.time.Duration.Companion.seconds - -object PaperQueueTickTask { - private val log = logger() - private var job: Job? = null - private lateinit var serverName: String - - fun start() { - this.serverName = SurfCoreApi.getCurrentServerName() - log.atInfo().log("Starting queue tick task for server: %s", serverName) - - job = plugin.launch { - while (isActive) { - delay(1.seconds) - tick() - } - } - } - - suspend fun shutdown() { - job?.cancelAndJoin() - job = null - } - - private suspend fun tick() { - val queue = RedisQueueService.get().get(serverName) as? PaperSurfQueue - if (queue == null) { - log.atWarning().log("Queue for server %s not found", serverName) - return - } - - try { - queue.tickSecond() - } catch (e: Exception) { - log.atWarning() - .withCause(e) - .log("Error during tickSecond for queue %s", serverName) - } - } -} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperSurfQueue.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperSurfQueue.kt deleted file mode 100644 index 69dcb90..0000000 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperSurfQueue.kt +++ /dev/null @@ -1,56 +0,0 @@ -package dev.slne.surf.queue.paper.queue - -import dev.slne.surf.queue.common.queue.AbstractSurfQueue -import dev.slne.surf.queue.paper.metrics.QueueMetrics -import dev.slne.surf.surfapi.core.api.util.logger -import java.util.concurrent.atomic.AtomicLong -import kotlin.coroutines.cancellation.CancellationException -import kotlin.time.Duration.Companion.minutes - -class PaperSurfQueue(serverName: String) : AbstractSurfQueue(serverName) { - private val transferProcessor = PaperQueueTransferProcessor(serverName, store, lockManager, GRACE_PERIOD_MS) - private val cleanup = PaperQueueCleanup(this, store, lockManager) - - private val tickCount = AtomicLong(0) - - companion object { - private val log = logger() - val GRACE_PERIOD_MS = 1.minutes.inWholeMilliseconds - } - - fun getTickCount() = tickCount.get() - - override fun onEnqueued() { - QueueMetrics.recordEnqueue(serverName) - } - - override fun onDequeued() { - QueueMetrics.recordDequeue(serverName) - } - - suspend fun tickSecond() { - tickCount.incrementAndGet() - - safeTick("cleanup") { cleanup.tick() } - safeTick("transfers") { transferProcessor.tick() } - } - - private inline fun safeTick(component: String, block: () -> Unit) { - try { - block() - } catch (e: Exception) { - if (e is CancellationException) throw e - log.atWarning() - .withCause(e) - .log("Failed to tick %s for queue %s", component, serverName) - } - } - - suspend fun delete() { - store.deleteAll() - } - - suspend fun forceCleanup() { - cleanup.cleanupExpiredEntries() - } -} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/cleanup/PaperQueueCleanup.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/cleanup/PaperQueueCleanup.kt new file mode 100644 index 0000000..e9b505f --- /dev/null +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/cleanup/PaperQueueCleanup.kt @@ -0,0 +1,80 @@ +package dev.slne.surf.queue.paper.queue.cleanup + +import dev.slne.surf.queue.common.queue.RedisQueueLockManager +import dev.slne.surf.queue.common.queue.RedisQueueStore +import dev.slne.surf.queue.paper.metrics.QueueMetrics +import dev.slne.surf.queue.paper.queue.PaperQueueImpl +import dev.slne.surf.surfapi.core.api.util.logger +import java.time.Instant +import java.util.UUID +import kotlin.collections.iterator +import kotlin.coroutines.cancellation.CancellationException + +class PaperQueueCleanup( + private val queue: PaperQueueImpl, + private val store: RedisQueueStore, + private val lockManager: RedisQueueLockManager +) { + companion object { + private val log = logger() + private const val CLEANUP_INTERVAL_TICKS = 10L + + @JvmStatic + private fun isExpired(now: Long, lastSeenTime: Long): Boolean { + return now - lastSeenTime >= PaperQueueImpl.GRACE_PERIOD_MS + } + } + + suspend fun tick() { + if (queue.tickCount % CLEANUP_INTERVAL_TICKS == 0L) { + lockManager.withCleanupLock { + cleanupExpiredEntries() + } + } + } + + suspend fun cleanupExpiredEntries() { + val now = Instant.now().toEpochMilli() + val allLastSeen = store.readAllLastSeen() + var removals = 0 + + try { + for ((uuid, lastSeenTime) in allLastSeen) { + if (isExpired(now, lastSeenTime)) { + removals += processExpiredEntry(uuid) + } + } + } catch (e: Exception) { + if (e is CancellationException) throw e + log.atWarning() + .withCause(e) + .log("Failed to cleanup expired entries for queue %s", queue.serverName) + } + + QueueMetrics.recordCleanupCycle(removals) + } + + private suspend fun processExpiredEntry(uuid: UUID): Int = try { + queue.dequeue(uuid) + log.atInfo() + .log("Cleanup: removed expired entry %s from queue %s", uuid, queue.serverName) + 1 + } catch (e: Exception) { + if (e is CancellationException) throw e + log.atWarning() + .withCause(e) + .log("Cleanup: dequeue failed for %s in queue %s, attempting forced removal", uuid, queue.serverName) + forceRemoveEntry(uuid) + } + + private suspend fun forceRemoveEntry(uuid: UUID): Int = try { + store.removeAllFor(uuid) + 1 + } catch (e: Exception) { + if (e is CancellationException) throw e + log.atWarning() + .withCause(e) + .log("Cleanup: forced removal also failed for %s in queue %s", uuid, queue.serverName) + 0 + } +} \ No newline at end of file diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTransfer.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/transfer/PaperQueueTransfer.kt similarity index 65% rename from surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTransfer.kt rename to surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/transfer/PaperQueueTransfer.kt index 1fb1f25..97aaf1f 100644 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTransfer.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/transfer/PaperQueueTransfer.kt @@ -1,4 +1,4 @@ -package dev.slne.surf.queue.paper.queue +package dev.slne.surf.queue.paper.queue.transfer import dev.slne.surf.core.api.common.SurfCoreApi import dev.slne.surf.core.api.common.player.SurfPlayer @@ -11,10 +11,15 @@ import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.withTimeout import net.kyori.adventure.text.Component import org.bukkit.Bukkit +import java.util.* +import kotlin.coroutines.cancellation.CancellationException import kotlin.math.min import kotlin.time.Duration.Companion.seconds -class PaperQueueTransfer(private val processor: PaperQueueTransferProcessor, private val serverName: String) { +class PaperQueueTransfer( + private val processor: PaperQueueTransferProcessor, + private val serverName: String +) { companion object { private val log = logger() @@ -24,33 +29,32 @@ class PaperQueueTransfer(private val processor: PaperQueueTransferProcessor, pri val availableSlots = Bukkit.getMaxPlayers() - Bukkit.getOnlinePlayers().size if (availableSlots <= 0) return 0 - val coreServer = SurfCoreApi.getServerByName(serverName) ?: return 0 + val coreServer = SurfServer[serverName] ?: return 0 val maxTransfers = min(availableSlots, SurfQueueConfig.getConfig().maxTransfersPerSecond) - return processor.processTransfers(maxTransfers) { entry -> - try { - val corePlayer = SurfCoreApi.getPlayer(entry.uuid) - if (corePlayer == null) { - TransferAction.PLAYER_NOT_FOUND to null - } else { - val currentPlayerServer = corePlayer.currentServer - val currentPlayerServerName = currentPlayerServer?.name + return processor.processTransfers(maxTransfers) { (uuid) -> + transferEntry(uuid, coreServer) + } + } - if (currentPlayerServer == null) { // Probably transferring to another proxy - TransferAction.PLAYER_NOT_CONNECTED_TO_A_SERVER to null - } else if (currentPlayerServerName == serverName) { - TransferAction.PLAYER_ALREADY_ON_SERVER to null - } else { - tryTransferPlayer(corePlayer, coreServer) - } - } + private suspend fun transferEntry(uuid: UUID, targetServer: SurfServer): Pair { + try { + val corePlayer = SurfCoreApi.getPlayer(uuid) ?: return TransferAction.PLAYER_NOT_FOUND to null + val currentPlayerServer = corePlayer.currentServer + val currentPlayerServerName = currentPlayerServer?.name + ?: return TransferAction.PLAYER_NOT_CONNECTED_TO_A_SERVER to null // Probably transferring to another proxy - } catch (e: Exception) { - log.atWarning() - .withCause(e) - .log("Error during transfer for queue %s", serverName) - TransferAction.ERROR to null + if (currentPlayerServerName == serverName) { + return TransferAction.PLAYER_ALREADY_ON_SERVER to null } + + return tryTransferPlayer(corePlayer, targetServer) + } catch (e: Exception) { + if (e is CancellationException) throw e + log.atWarning() + .withCause(e) + .log("Error during transfer for queue %s", serverName) + return TransferAction.ERROR to null } } diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTransferProcessor.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/transfer/PaperQueueTransferProcessor.kt similarity index 96% rename from surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTransferProcessor.kt rename to surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/transfer/PaperQueueTransferProcessor.kt index 9929bf3..4d084ee 100644 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/PaperQueueTransferProcessor.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/transfer/PaperQueueTransferProcessor.kt @@ -1,4 +1,4 @@ -package dev.slne.surf.queue.paper.queue +package dev.slne.surf.queue.paper.queue.transfer import dev.slne.surf.core.api.common.SurfCoreApi import dev.slne.surf.core.api.common.util.sendText @@ -61,14 +61,12 @@ class PaperQueueTransferProcessor( suspend fun processTransfers( maxTransfers: Int, tryTransfer: suspend (QueueEntry) -> Pair - ): Int { - return lockManager.withTransferLock { acquired -> - QueueMetrics.recordLockAttempt(acquired) - if (acquired) { - doProcessTransfers(maxTransfers, tryTransfer) - } else { - 0 - } + ): Int = lockManager.withTransferLock { acquired -> + QueueMetrics.recordLockAttempt(acquired) + if (acquired) { + doProcessTransfers(maxTransfers, tryTransfer) + } else { + 0 } } @@ -131,7 +129,7 @@ class PaperQueueTransferProcessor( TransferAction.TIMEOUT -> { // Timeout means the target server is likely unreachable. - // Dequeue immediately instead of retrying with another 30 s timeout + // Dequeue immediately instead of retrying with another 30s timeout // to avoid blocking the entire queue for extended periods. store.dequeue(uuid) QueueMetrics.recordFailedTransfer(serverName) diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/TransferAction.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/transfer/TransferAction.kt similarity index 85% rename from surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/TransferAction.kt rename to surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/transfer/TransferAction.kt index 1a71361..2c232fc 100644 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/TransferAction.kt +++ b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/queue/transfer/TransferAction.kt @@ -1,4 +1,4 @@ -package dev.slne.surf.queue.paper.queue +package dev.slne.surf.queue.paper.queue.transfer enum class TransferAction { DONE, diff --git a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/redis/PaperRedisInstance.kt b/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/redis/PaperRedisInstance.kt deleted file mode 100644 index 9c8c567..0000000 --- a/surf-queue-paper/src/main/kotlin/dev/slne/surf/queue/paper/redis/PaperRedisInstance.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.slne.surf.queue.paper.redis - -import com.google.auto.service.AutoService -import dev.slne.surf.queue.common.redis.RedisInstance - -@AutoService(RedisInstance::class) -class PaperRedisInstance : RedisInstance() \ No newline at end of file diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/VelocityMain.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/VelocityMain.kt index 309cd6c..76654da 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/VelocityMain.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/VelocityMain.kt @@ -7,33 +7,31 @@ import com.velocitypowered.api.event.proxy.ProxyInitializeEvent import com.velocitypowered.api.event.proxy.ProxyShutdownEvent import com.velocitypowered.api.plugin.PluginContainer import com.velocitypowered.api.proxy.ProxyServer -import dev.slne.surf.queue.common.SurfQueueInstance -import dev.slne.surf.surfapi.velocity.api.metrics.Metrics +import dev.slne.surf.queue.common.QueueInstance import kotlinx.coroutines.runBlocking class VelocityMain @Inject constructor( val proxy: ProxyServer, val container: PluginContainer, - val suspendingPluginContainer: SuspendingPluginContainer, - val metricsFactory: Metrics.Factory + suspendingPluginContainer: SuspendingPluginContainer, ) { init { suspendingPluginContainer.initialize(this) plugin = this runBlocking { - SurfQueueInstance.get().load() + QueueInstance.get().load() } } @Subscribe suspend fun onProxyInitialize(event: ProxyInitializeEvent) { - SurfQueueInstance.get().enable() + QueueInstance.get().enable() } @Subscribe suspend fun onProxyShutdown(event: ProxyShutdownEvent) { - SurfQueueInstance.get().disable() + QueueInstance.get().disable() } } diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/VelocitySurfQueueInstance.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/VelocitySurfQueueInstance.kt index 37d738d..b340bc5 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/VelocitySurfQueueInstance.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/VelocitySurfQueueInstance.kt @@ -1,48 +1,26 @@ package dev.slne.surf.queue.velocity import com.google.auto.service.AutoService -import dev.slne.surf.queue.common.SurfQueueInstance -import dev.slne.surf.queue.common.queue.AbstractSurfQueue +import dev.slne.surf.queue.common.QueueInstance +import dev.slne.surf.queue.common.queue.AbstractQueue +import dev.slne.surf.queue.common.queue.QueueTicker import dev.slne.surf.queue.velocity.command.queueCommand import dev.slne.surf.queue.velocity.listener.QueuePlayerListener -import dev.slne.surf.queue.velocity.metrics.QueueBstatsIntegration -import dev.slne.surf.queue.velocity.metrics.QueueMetricsLogger -import dev.slne.surf.queue.velocity.queue.QueueTickTask import dev.slne.surf.queue.velocity.queue.VelocitySurfQueue -import dev.slne.surf.surfapi.core.api.util.logger -@AutoService(SurfQueueInstance::class) -class VelocitySurfQueueInstance : SurfQueueInstance() { +@AutoService(QueueInstance::class) +class VelocitySurfQueueInstance : QueueInstance() { override val componentOwner get() = plugin.container override suspend fun enable() { super.enable() plugin.proxy.eventManager.register(plugin, QueuePlayerListener) - QueueTickTask.startTicking() queueCommand() - - try { - QueueBstatsIntegration.setup(plugin.metricsFactory) - } catch (e: Exception) { - log.atWarning() - .withCause(e) - .log("Failed to initialize bStats integration") - } - } - - override suspend fun disable() { - QueueMetricsLogger.stop() - QueueTickTask.shutdown() - - super.disable() + QueueTicker.start() } - override fun createQueue(serverName: String): AbstractSurfQueue { + override fun createQueue(serverName: String): AbstractQueue { return VelocitySurfQueue(serverName) } - - companion object { - private val log = logger() - } } \ No newline at end of file diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/QueueCommand.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/QueueCommand.kt index 00ab39a..a0d9b58 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/QueueCommand.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/QueueCommand.kt @@ -1,14 +1,10 @@ package dev.slne.surf.queue.velocity.command import dev.jorel.commandapi.kotlindsl.commandTree -import dev.slne.surf.queue.velocity.command.metrics.metricsCommand import dev.slne.surf.queue.velocity.command.pause.queuePauseCommand -import dev.slne.surf.queue.velocity.command.test.testQueueCommands import dev.slne.surf.queue.velocity.permission.SurfQueuePermissions fun queueCommand() = commandTree("squeue-velocity") { withPermission(SurfQueuePermissions.COMMAND_QUEUE) - testQueueCommands() - metricsCommand() queuePauseCommand() } \ No newline at end of file diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/test/TestQueueCommands.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/test/TestQueueCommands.kt deleted file mode 100644 index 290e202..0000000 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/command/test/TestQueueCommands.kt +++ /dev/null @@ -1,74 +0,0 @@ -package dev.slne.surf.queue.velocity.command.test - -import dev.jorel.commandapi.CommandTree -import dev.jorel.commandapi.kotlindsl.getValue -import dev.jorel.commandapi.kotlindsl.literalArgument -import dev.slne.surf.core.api.common.player.SurfPlayer -import dev.slne.surf.core.api.common.server.SurfServer -import dev.slne.surf.core.api.velocity.command.argument.surfBackendServerArgument -import dev.slne.surf.core.api.velocity.command.argument.surfPlayerArgument -import dev.slne.surf.queue.common.queue.RedisQueueService -import dev.slne.surf.queue.velocity.queue.QueueTickTask -import dev.slne.surf.surfapi.core.api.messages.adventure.sendText -import dev.slne.surf.surfapi.velocity.api.command.executors.anyExecutorSuspend - -fun CommandTree.testQueueCommands() = literalArgument("test-queue") { - - literalArgument("enqueue") { - surfPlayerArgument("player") { - surfBackendServerArgument("server") { - anyExecutorSuspend { sender, args -> - val player: SurfPlayer by args - val server: SurfServer by args - - val queue = RedisQueueService.get().get(server.name) - queue.enqueue(player.uuid) - sender.sendText { - appendSuccessPrefix() - success("Enqueued player ") - variableValue(player.lastKnownName ?: player.uuid.toString()) - success(" to queue ") - variableValue(server.name) - } - } - } - } - } - - literalArgument("tickQueues") { - anyExecutorSuspend { sender, args -> - QueueTickTask.tick() - sender.sendText { - appendSuccessPrefix() - success("Ticked queues") - } - } - } - - literalArgument("transferTask") { - literalArgument("start") { - anyExecutorSuspend { source, arguments -> - QueueTickTask.shutdown() - QueueTickTask.startTicking() - source.sendText { - appendSuccessPrefix() - success("Started transfer task") - } - } - } - - literalArgument("stop") { - anyExecutorSuspend { source, arguments -> - QueueTickTask.shutdown() - source.sendText { - appendSuccessPrefix() - success("Stopped transfer task") - } - } - } - } - - literalArgument("test-display") { - - } -} \ No newline at end of file diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/metrics/QueueBstatsIntegration.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/metrics/QueueBstatsIntegration.kt deleted file mode 100644 index 97e9c1e..0000000 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/metrics/QueueBstatsIntegration.kt +++ /dev/null @@ -1,81 +0,0 @@ -package dev.slne.surf.queue.velocity.metrics - -import dev.slne.surf.queue.common.queue.RedisQueueService -import dev.slne.surf.queue.velocity.plugin -import dev.slne.surf.queue.velocity.queue.VelocitySurfQueue -import dev.slne.surf.surfapi.core.api.util.logger -import dev.slne.surf.surfapi.velocity.api.metrics.Metrics -import kotlinx.coroutines.runBlocking -import java.util.concurrent.atomic.AtomicLong - -object QueueBstatsIntegration { - private val log = logger() - - private var lastTransfers = AtomicLong(0) - private var lastEnqueues = AtomicLong(0) - private var lastFailedTransfers = AtomicLong(0) - - fun setup(metricsFactory: Metrics.Factory) { - val metrics = metricsFactory.make(plugin, 29544) - - metrics.addCustomChart(Metrics.SimplePie("queue_count") { - try { - RedisQueueService.get().getAll().size.toString() - } catch (_: Exception) { - "0" - } - }) - - metrics.addCustomChart(Metrics.SingleLineChart("total_transfers") { - val current = QueueMetrics.totalTransfers.get() - val last = lastTransfers.getAndSet(current) - (current - last).toInt() - }) - - metrics.addCustomChart(Metrics.SingleLineChart("total_enqueues") { - val current = QueueMetrics.totalEnqueues.get() - val last = lastEnqueues.getAndSet(current) - (current - last).toInt() - }) - - metrics.addCustomChart(Metrics.SingleLineChart("total_failed_transfers") { - val current = QueueMetrics.totalFailedTransfers.get() - val last = lastFailedTransfers.getAndSet(current) - (current - last).toInt() - }) - - metrics.addCustomChart(Metrics.SingleLineChart("total_queued_players") { - try { - runBlocking { - QueueMetrics.collectQueueSizes().values.sum() - } - } catch (_: Exception) { - 0 - } - }) - - metrics.addCustomChart(Metrics.AdvancedPie("transfers_per_queue") { - try { - RedisQueueService.get().getAll() - .filterIsInstance() - .associate { it.serverName to QueueMetrics.getTransfersFor(it.serverName).toInt() } - .filterValues { it > 0 } - } catch (_: Exception) { - emptyMap() - } - }) - - metrics.addCustomChart(Metrics.AdvancedPie("queue_sizes") { - try { - runBlocking { - QueueMetrics.collectQueueSizes() - } - } catch (_: Exception) { - emptyMap() - } - }) - - log.atInfo() - .log("bStats integration initialized") - } -} \ No newline at end of file diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/metrics/QueueMetrics.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/metrics/QueueMetrics.kt deleted file mode 100644 index ca8807b..0000000 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/metrics/QueueMetrics.kt +++ /dev/null @@ -1,126 +0,0 @@ -package dev.slne.surf.queue.velocity.metrics - -import dev.slne.surf.queue.common.queue.RedisQueueService -import dev.slne.surf.queue.velocity.queue.VelocitySurfQueue -import dev.slne.surf.surfapi.core.api.util.logger -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicLong - -object QueueMetrics { - private val log = logger() - - val totalTransfers = AtomicLong(0) - val totalEnqueues = AtomicLong(0) - val totalDequeues = AtomicLong(0) - val totalFailedTransfers = AtomicLong(0) - val totalGraceExpiries = AtomicLong(0) - val totalRetryExhausted = AtomicLong(0) - val totalLockAttempts = AtomicLong(0) - val totalLockAcquired = AtomicLong(0) - val totalCleanupCycles = AtomicLong(0) - val totalCleanupRemovals = AtomicLong(0) - val totalTicks = AtomicLong(0) - - private val perQueueTransfers = ConcurrentHashMap() - private val perQueueEnqueues = ConcurrentHashMap() - private val perQueueDequeues = ConcurrentHashMap() - private val perQueueFailedTransfers = ConcurrentHashMap() - private val perQueueSkips = ConcurrentHashMap() - - fun recordTransfer(serverName: String) { - totalTransfers.incrementAndGet() - perQueueTransfers.computeIfAbsent(serverName) { AtomicLong(0) }.incrementAndGet() - } - - fun recordEnqueue(serverName: String) { - totalEnqueues.incrementAndGet() - perQueueEnqueues.computeIfAbsent(serverName) { AtomicLong(0) }.incrementAndGet() - } - - fun recordDequeue(serverName: String) { - totalDequeues.incrementAndGet() - perQueueDequeues.computeIfAbsent(serverName) { AtomicLong(0) }.incrementAndGet() - } - - fun recordFailedTransfer(serverName: String) { - totalFailedTransfers.incrementAndGet() - perQueueFailedTransfers.computeIfAbsent(serverName) { AtomicLong(0) }.incrementAndGet() - } - - fun recordSkip(serverName: String) { - perQueueSkips.computeIfAbsent(serverName) { AtomicLong(0) }.incrementAndGet() - } - - fun recordGraceExpiry() { - totalGraceExpiries.incrementAndGet() - } - - fun recordRetryExhausted() { - totalRetryExhausted.incrementAndGet() - } - - fun recordLockAttempt(acquired: Boolean) { - totalLockAttempts.incrementAndGet() - if (acquired) totalLockAcquired.incrementAndGet() - } - - fun recordCleanupCycle(removals: Int) { - totalCleanupCycles.incrementAndGet() - totalCleanupRemovals.addAndGet(removals.toLong()) - } - - fun recordTick() { - totalTicks.incrementAndGet() - } - - fun getTransfersFor(serverName: String): Long = - perQueueTransfers[serverName]?.get() ?: 0 - - fun getEnqueuesFor(serverName: String): Long = - perQueueEnqueues[serverName]?.get() ?: 0 - - fun getDequeuesFor(serverName: String): Long = - perQueueDequeues[serverName]?.get() ?: 0 - - fun getFailedTransfersFor(serverName: String): Long = - perQueueFailedTransfers[serverName]?.get() ?: 0 - - fun getSkipsFor(serverName: String): Long = - perQueueSkips[serverName]?.get() ?: 0 - - fun snapshot(): QueueMetricsSnapshot = QueueMetricsSnapshot( - totalTransfers = totalTransfers.get(), - totalEnqueues = totalEnqueues.get(), - totalDequeues = totalDequeues.get(), - totalFailedTransfers = totalFailedTransfers.get(), - totalGraceExpiries = totalGraceExpiries.get(), - totalRetryExhausted = totalRetryExhausted.get(), - totalLockAttempts = totalLockAttempts.get(), - totalLockAcquired = totalLockAcquired.get(), - totalCleanupCycles = totalCleanupCycles.get(), - totalCleanupRemovals = totalCleanupRemovals.get(), - totalTicks = totalTicks.get(), - perQueue = perQueueTransfers.keys.associateWith { serverName -> - QueueMetricsSnapshot.PerQueueMetrics( - transfers = getTransfersFor(serverName), - enqueues = getEnqueuesFor(serverName), - dequeues = getDequeuesFor(serverName), - failedTransfers = getFailedTransfersFor(serverName), - skips = getSkipsFor(serverName) - ) - } - ) - - suspend fun collectQueueSizes(): Map { - return try { - RedisQueueService.get().getAll() - .filterIsInstance() - .associate { it.serverName to it.size() } - } catch (e: Exception) { - log.atWarning() - .withCause(e) - .log("Failed to collect queue sizes for metrics") - emptyMap() - } - } -} \ No newline at end of file diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/metrics/QueueMetricsLogger.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/metrics/QueueMetricsLogger.kt deleted file mode 100644 index c73c8ca..0000000 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/metrics/QueueMetricsLogger.kt +++ /dev/null @@ -1,61 +0,0 @@ -package dev.slne.surf.queue.velocity.metrics - -import com.github.shynixn.mccoroutine.velocity.launch -import dev.slne.surf.queue.velocity.plugin -import dev.slne.surf.surfapi.core.api.util.logger -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlin.time.Duration.Companion.minutes - -object QueueMetricsLogger { - private val log = logger() - private var job: Job? = null - - fun start() { - job = plugin.container.launch { - while (isActive) { - delay(5.minutes) - try { - val snapshot = QueueMetrics.snapshot() - val queueSizes = QueueMetrics.collectQueueSizes() - - log.atInfo() - .log( - "Queue Metrics: transfers=%d, failed=%d, enqueues=%d, dequeues=%d, " + - "graceExpiries=%d, retryExhausted=%d, lockRate=%.1f%%, cleanupCycles=%d", - snapshot.totalTransfers, - snapshot.totalFailedTransfers, - snapshot.totalEnqueues, - snapshot.totalDequeues, - snapshot.totalGraceExpiries, - snapshot.totalRetryExhausted, - snapshot.lockAcquisitionRate * 100, - snapshot.totalCleanupCycles - ) - - for ((serverName, size) in queueSizes) { - val perQueue = snapshot.perQueue[serverName] - log.atInfo().log( - " Queue [%s]: size=%d, transfers=%d, failed=%d, skips=%d", - serverName, - size, - perQueue?.transfers ?: 0, - perQueue?.failedTransfers ?: 0, - perQueue?.skips ?: 0 - ) - } - } catch (e: Exception) { - log.atWarning() - .withCause(e) - .log("Failed to log metrics snapshot") - } - } - } - } - - fun stop() { - job?.cancel() - job = null - } -} \ No newline at end of file diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/metrics/QueueMetricsSnapshot.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/metrics/QueueMetricsSnapshot.kt deleted file mode 100644 index 4027105..0000000 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/metrics/QueueMetricsSnapshot.kt +++ /dev/null @@ -1,63 +0,0 @@ -package dev.slne.surf.queue.velocity.metrics - -data class QueueMetricsSnapshot( - val totalTransfers: Long, - val totalEnqueues: Long, - val totalDequeues: Long, - val totalFailedTransfers: Long, - val totalGraceExpiries: Long, - val totalRetryExhausted: Long, - val totalLockAttempts: Long, - val totalLockAcquired: Long, - val totalCleanupCycles: Long, - val totalCleanupRemovals: Long, - val totalTicks: Long, - val perQueue: Map -) { - data class PerQueueMetrics( - val transfers: Long, - val enqueues: Long, - val dequeues: Long, - val failedTransfers: Long, - val skips: Long - ) - - val lockAcquisitionRate: Double - get() = if (totalLockAttempts > 0) totalLockAcquired.toDouble() / totalLockAttempts else 0.0 - - val transferSuccessRate: Double - get() { - val total = totalTransfers + totalFailedTransfers - return if (total > 0) totalTransfers.toDouble() / total else 0.0 - } - - override fun toString(): String = buildString { - appendLine("=== Queue Metrics Snapshot ===") - appendLine( - "Transfers: $totalTransfers successful, $totalFailedTransfers failed (${ - String.format( - "%.1f", - transferSuccessRate * 100 - ) - }% success)" - ) - appendLine("Enqueues: $totalEnqueues | Dequeues: $totalDequeues") - appendLine("Grace Expiries: $totalGraceExpiries | Retry Exhausted: $totalRetryExhausted") - appendLine( - "Lock: $totalLockAcquired/$totalLockAttempts acquired (${ - String.format( - "%.1f", - lockAcquisitionRate * 100 - ) - }%)" - ) - appendLine("Cleanup: $totalCleanupCycles cycles, $totalCleanupRemovals removals") - appendLine("Ticks: $totalTicks") - if (perQueue.isNotEmpty()) { - appendLine("--- Per Queue ---") - for ((name, metrics) in perQueue) { - appendLine(" $name: transfers=${metrics.transfers}, enqueues=${metrics.enqueues}, dequeues=${metrics.dequeues}, failed=${metrics.failedTransfers}, skips=${metrics.skips}") - } - } - } -} \ No newline at end of file diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/permission/SurfQueuePermissions.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/permission/SurfQueuePermissions.kt index 3d74b3e..b2377a3 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/permission/SurfQueuePermissions.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/permission/SurfQueuePermissions.kt @@ -5,7 +5,5 @@ object SurfQueuePermissions { private const val COMMAND_PREFIX = PREFIX + "command." const val COMMAND_QUEUE = COMMAND_PREFIX + "queue" - - const val COMMAND_METRICS = COMMAND_PREFIX + "metrics" const val COMMAND_PAUSE = COMMAND_PREFIX + "pause" } \ No newline at end of file diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueDisplay.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueDisplay.kt index 2514f82..ab22798 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueDisplay.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueDisplay.kt @@ -2,8 +2,8 @@ package dev.slne.surf.queue.velocity.queue import dev.slne.surf.queue.velocity.util.toVelocityPlayer import dev.slne.surf.surfapi.core.api.messages.adventure.buildText -import it.unimi.dsi.fastutil.objects.Object2IntMap -import java.util.UUID +import it.unimi.dsi.fastutil.objects.ObjectList +import java.util.* class QueueDisplay(private val queue: VelocitySurfQueue) { @@ -13,11 +13,11 @@ class QueueDisplay(private val queue: VelocitySurfQueue) { private const val PAUSE_CHAR = '⏸' } - private var cachedUuidsWithPosition: Collection>? = null + private var cachedUuidsWithPosition: ObjectList? = null suspend fun tick() { - if (queue.getTickCount() % 3L == 0L) { - cachedUuidsWithPosition = queue.getAllUuidsWithPosition() + if (queue.tickCount % 3L == 0L) { + cachedUuidsWithPosition = queue.getAllUuidsOrderedByPosition() } updateActionBars() @@ -25,14 +25,12 @@ class QueueDisplay(private val queue: VelocitySurfQueue) { private suspend fun updateActionBars() { val uuidsWithPosition = cachedUuidsWithPosition ?: return - val spinnerIndex = (queue.getTickCount() % spinner.size) + val spinnerIndex = (queue.tickCount % spinner.size) val spinnerEnd = spinner[spinnerIndex] val spinnerStart = spinnerReversed[spinnerIndex] val paused = queue.isPaused() - for (entry in uuidsWithPosition) { - val uuid = entry.key - val position = entry.intValue + for ((index, uuid) in uuidsWithPosition.withIndex()) { val player = uuid.toVelocityPlayer() ?: continue player.sendActionBar(buildText { @@ -51,7 +49,7 @@ class QueueDisplay(private val queue: VelocitySurfQueue) { appendSpace() spacer('|') appendSpace() - variableValue("$position/${uuidsWithPosition.size}") + variableValue("${index + 1}/${uuidsWithPosition.size}") appendSpace() spacer(spinnerEnd) } diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueTickTask.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueTickTask.kt deleted file mode 100644 index 370276e..0000000 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueTickTask.kt +++ /dev/null @@ -1,53 +0,0 @@ -package dev.slne.surf.queue.velocity.queue - -import com.github.shynixn.mccoroutine.velocity.launch -import dev.slne.surf.queue.common.queue.RedisQueueService -import dev.slne.surf.queue.velocity.plugin -import dev.slne.surf.surfapi.core.api.util.logger -import kotlinx.coroutines.* -import kotlin.time.Duration.Companion.seconds - -object QueueTickTask { - - private val log = logger() - private var job: Job? = null - - private var lastFetch = 0L - - fun startTicking() { - job = plugin.container.launch { - while (isActive) { - delay(1.seconds) - tick() - } - } - } - - suspend fun shutdown() { - job?.cancelAndJoin() - job = null - } - - suspend fun tick() { - val now = System.currentTimeMillis() - if (now - lastFetch > 30_000) { - lastFetch = now - RedisQueueService.get().fetchFromRedis() - } - - coroutineScope { - for (queue in RedisQueueService.get().getAll()) { - require(queue is VelocitySurfQueue) { "Queue must be VelocitySurfQueue" } - launch { - try { - queue.tickSecond() - } catch (e: Exception) { - log.atWarning() - .withCause(e) - .log("Error during tickSecond for queue %s", queue.serverName) - } - } - } - } - } -} \ No newline at end of file diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/VelocitySurfQueue.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/VelocitySurfQueue.kt index 9f3dc5b..896c9c4 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/VelocitySurfQueue.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/VelocitySurfQueue.kt @@ -1,27 +1,12 @@ package dev.slne.surf.queue.velocity.queue -import dev.slne.surf.queue.common.queue.AbstractSurfQueue -import dev.slne.surf.queue.velocity.metrics.QueueMetrics -import dev.slne.surf.surfapi.core.api.util.logger +import dev.slne.surf.queue.common.queue.AbstractQueue +import dev.slne.surf.queue.common.queue.tick.SafeQueueTick import java.time.Instant import java.util.* -import java.util.concurrent.atomic.AtomicInteger -class VelocitySurfQueue(serverName: String) : AbstractSurfQueue(serverName) { +class VelocitySurfQueue(serverName: String) : AbstractQueue(serverName) { val display = QueueDisplay(this) - private val ticks = AtomicInteger(0) - - companion object { - private val log = logger() - } - - override fun onEnqueued() { - QueueMetrics.recordEnqueue(serverName) - } - - override fun onDequeued() { - QueueMetrics.recordDequeue(serverName) - } suspend fun markPlayerReconnected(uuid: UUID) { store.clearLastSeen(uuid) @@ -31,16 +16,8 @@ class VelocitySurfQueue(serverName: String) : AbstractSurfQueue(serverName) { store.putLastSeen(uuid, Instant.now().toEpochMilli()) } - fun getTickCount(): Int = ticks.get() - - suspend fun tickSecond() { - try { - ticks.incrementAndGet() - display.tick() - } catch (e: Exception) { - log.atWarning() - .withCause(e) - .log("Error during tickSecond for queue %s", serverName) - } + override suspend fun tick() { + super.tick() + SafeQueueTick.tickSafe(this, "display") { display.tick() } } } \ No newline at end of file diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/redis/VelocityRedisInstance.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/redis/VelocityRedisInstance.kt deleted file mode 100644 index deac7c3..0000000 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/redis/VelocityRedisInstance.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.slne.surf.queue.velocity.redis - -import com.google.auto.service.AutoService -import dev.slne.surf.queue.common.redis.RedisInstance - -@AutoService(RedisInstance::class) -class VelocityRedisInstance : RedisInstance() { - override fun register() { - super.register() - } -} \ No newline at end of file From f517a0cedb5c6d564a65a291df718b99dc861624 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sat, 11 Apr 2026 13:42:59 +0200 Subject: [PATCH 7/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(queue):=20ren?= =?UTF-8?q?ame=20VelocitySurfQueue=20to=20VelocityQueueImpl=20and=20update?= =?UTF-8?q?=20references?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rename VelocitySurfQueue class to VelocityQueueImpl for consistency - update all references to VelocitySurfQueue in related files - adjust QueueDisplay and QueuePlayerListener to use new class name --- .../dev/slne/surf/queue/api/SurfQueue.kt | 66 +++++++++++++++++++ .../surf/queue/common/queue/AbstractQueue.kt | 61 +++++++++++++++++ .../velocity/VelocitySurfQueueInstance.kt | 4 +- .../velocity/listener/QueuePlayerListener.kt | 6 +- .../surf/queue/velocity/queue/QueueDisplay.kt | 2 +- ...ocitySurfQueue.kt => VelocityQueueImpl.kt} | 2 +- 6 files changed, 134 insertions(+), 7 deletions(-) rename surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/{VelocitySurfQueue.kt => VelocityQueueImpl.kt} (90%) diff --git a/surf-queue-api/src/main/kotlin/dev/slne/surf/queue/api/SurfQueue.kt b/surf-queue-api/src/main/kotlin/dev/slne/surf/queue/api/SurfQueue.kt index 628862e..2166d8e 100644 --- a/surf-queue-api/src/main/kotlin/dev/slne/surf/queue/api/SurfQueue.kt +++ b/surf-queue-api/src/main/kotlin/dev/slne/surf/queue/api/SurfQueue.kt @@ -7,35 +7,101 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap import it.unimi.dsi.fastutil.objects.ObjectList import java.util.* +/** + * Public API interface for a server-specific player queue. + * + * Each instance represents the queue for a single target server, identified by [serverName]. + * All queue operations are suspend functions and safe to call from any coroutine context. + */ interface SurfQueue { + + /** The name of the target server this queue belongs to. */ val serverName: String + /** + * Returns the [SurfServer] instance for this queue's target server. + */ fun server() = SurfCoreApi.getServerByName(serverName) + /** + * Enqueues [uuid] using the priority resolved from LuckPerms. + * + * @return `true` if the player was newly added, `false` if already queued. + */ suspend fun enqueue(uuid: UUID): Boolean + + /** + * Enqueues [uuid] with an explicit [priority]. + * Priorities above [RedisQueueScore.MAX_PRIORITY] are capped automatically. + * + * @return `true` if the player was newly added, `false` if already queued. + */ suspend fun enqueue(uuid: UUID, priority: Int): Boolean + + /** + * Removes [uuid] from the queue. + * + * @return `true` if the player was removed, `false` if they were not queued. + */ suspend fun dequeue(uuid: UUID): Boolean + + /** + * Returns `true` if [uuid] is currently in the queue. + */ suspend fun isQueued(uuid: UUID): Boolean + + /** + * Returns the 0-based position of [uuid] in the queue, or `null` if not queued. + */ suspend fun getPosition(uuid: UUID): Int? + + /** + * Returns the total number of players currently in the queue. + */ suspend fun size(): Int + /** + * Returns `true` if the queue is paused. A paused queue stops processing transfers. + */ suspend fun isPaused(): Boolean + + /** Pauses the queue. */ suspend fun pause() + + /** Resumes the queue. */ suspend fun resume() + /** + * Returns all queued UUIDs together with their 1-based position. + * + * @deprecated Use [getAllUuidsOrderedByPosition] for better performance. + */ @Deprecated( "Use getAllUuidsOrderedByPosition for better performance", ReplaceWith("getAllUuidsOrderedByPosition()") ) suspend fun getAllUuidsWithPosition(): ObjectList> + /** + * Returns all queued UUIDs in ascending position order (position 1 first). + */ suspend fun getAllUuidsOrderedByPosition(): ObjectList @OptIn(InternalSurfQueueApi::class) companion object { + /** + * Returns the [SurfQueue] for the given [serverName], or creates a new one if it doesn't exist. + */ fun byServer(serverName: String) = SurfQueueService.instance.get(serverName) + + /** + * Returns the [SurfQueue] for the given [server], or creates a new one if it doesn't exist. + */ fun byServer(server: SurfServer) = byServer(server.name) } } +/** + * Convenience extension to retrieve the queue for this server. + */ fun SurfServer.queue() = SurfQueue.byServer(this) \ No newline at end of file diff --git a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/AbstractQueue.kt b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/AbstractQueue.kt index bd09658..e866b38 100644 --- a/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/AbstractQueue.kt +++ b/surf-queue-common/src/main/kotlin/dev/slne/surf/queue/common/queue/AbstractQueue.kt @@ -11,10 +11,39 @@ import java.time.Instant import java.util.* import java.util.concurrent.atomic.AtomicInteger +/** + * Base implementation of [SurfQueue] backed by Redis. + * + * Subclasses must supply a [serverName] and may override [onEnqueued] / [onDequeued] + * for platform-specific side effects (e.g., metrics). They may also override [tick] + * to add periodic processing, but **must** call `super.tick()`. + * + * Ordering is determined by a packed Redis score that encodes priority, enqueue + * timestamp (relative to a per-queue epoch), and a monotonic sequence number for + * tie-breaking within the same millisecond. + * + * @param serverName The name of the server this queue targets. + */ abstract class AbstractQueue(override val serverName: String) : SurfQueue { + + /** + * Redis keys used for queue storage and synchronization. + */ protected val keys = RedisQueueKeys(serverName) + + /** + * Persistent storage for queue entries and scores. + */ protected val store = RedisQueueStore(keys) + + /** + * Distributed lock manager for this queue. + */ protected val lockManager = RedisQueueLockManager(keys) + + /** + * Millisecond epoch used to make timestamps relative, reducing score magnitude. + */ protected val epochMs = store.initEpochMs() /** @@ -25,12 +54,16 @@ abstract class AbstractQueue(override val serverName: String) : SurfQueue { */ private val enqueueSequence = AtomicInteger(0) + /** Number of times [tick] has been called since creation. */ var tickCount = 0 private set companion object { private val log = logger() + /** + * Caps [priority] to [RedisQueueScore.MAX_PRIORITY] and logs a warning if it exceeds the limit. + */ fun fixPriority(uuid: UUID, priority: Int): Int { return if (priority <= RedisQueueScore.MAX_PRIORITY) { priority @@ -48,6 +81,9 @@ abstract class AbstractQueue(override val serverName: String) : SurfQueue { } } + /** + * Increments [tickCount]. Subclasses that override this **must** invoke `super.tick()`. + */ @MustBeInvokedByOverriders open suspend fun tick() { tickCount++ @@ -83,7 +119,16 @@ abstract class AbstractQueue(override val serverName: String) : SurfQueue { return added } + /** + * Called after a player is successfully enqueued. Override for side effects such as + * recording metrics. No-op by default. + */ protected open fun onEnqueued() {} + + /** + * Called after a player is successfully dequeued. Override for side effects such as + * recording metrics. No-op by default. + */ protected open fun onDequeued() {} override suspend fun dequeue(uuid: UUID): Boolean { @@ -144,8 +189,24 @@ abstract class AbstractQueue(override val serverName: String) : SurfQueue { store.setPaused(true) } + /** + * Returns the raw [QueueEntry] metadata for [uuid], or `null` if not queued. + */ suspend fun getEntryMeta(uuid: UUID): QueueEntry? = store.getMeta(uuid) + + /** + * Returns the packed [RedisQueueScore] for [uuid], or `null` if not queued. + */ suspend fun getEntryScore(uuid: UUID): RedisQueueScore? = store.getScore(uuid) + + /** + * Returns the last-seen timestamp (epoch ms) for [uuid], or `null` if not recorded. + * Used to track disconnected players within their grace period. + */ suspend fun getEntryLastSeen(uuid: UUID): Long? = store.getLastSeen(uuid) + + /** + * Returns the number of transfer retry attempts for [uuid], or `null` if not queued. + */ suspend fun getEntryRetryCount(uuid: UUID): Int? = store.getRetryCount(uuid) } \ No newline at end of file diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/VelocitySurfQueueInstance.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/VelocitySurfQueueInstance.kt index b340bc5..5ac775e 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/VelocitySurfQueueInstance.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/VelocitySurfQueueInstance.kt @@ -6,7 +6,7 @@ import dev.slne.surf.queue.common.queue.AbstractQueue import dev.slne.surf.queue.common.queue.QueueTicker import dev.slne.surf.queue.velocity.command.queueCommand import dev.slne.surf.queue.velocity.listener.QueuePlayerListener -import dev.slne.surf.queue.velocity.queue.VelocitySurfQueue +import dev.slne.surf.queue.velocity.queue.VelocityQueueImpl @AutoService(QueueInstance::class) class VelocitySurfQueueInstance : QueueInstance() { @@ -21,6 +21,6 @@ class VelocitySurfQueueInstance : QueueInstance() { } override fun createQueue(serverName: String): AbstractQueue { - return VelocitySurfQueue(serverName) + return VelocityQueueImpl(serverName) } } \ No newline at end of file diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/listener/QueuePlayerListener.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/listener/QueuePlayerListener.kt index 94db33c..62118c4 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/listener/QueuePlayerListener.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/listener/QueuePlayerListener.kt @@ -4,7 +4,7 @@ import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.connection.DisconnectEvent import com.velocitypowered.api.event.connection.PostLoginEvent import dev.slne.surf.queue.common.queue.RedisQueueService -import dev.slne.surf.queue.velocity.queue.VelocitySurfQueue +import dev.slne.surf.queue.velocity.queue.VelocityQueueImpl import dev.slne.surf.surfapi.core.api.util.logger import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch @@ -17,7 +17,7 @@ object QueuePlayerListener { val uuid = event.player.uniqueId coroutineScope { for (queue in RedisQueueService.get().getAll()) { - require(queue is VelocitySurfQueue) { "Queue must be VelocitySurfQueue" } + require(queue is VelocityQueueImpl) { "Queue must be VelocityQueueImpl" } launch { try { queue.markPlayerReconnected(uuid) @@ -37,7 +37,7 @@ object QueuePlayerListener { val uuid = event.player.uniqueId coroutineScope { for (queue in RedisQueueService.get().getAll()) { - require(queue is VelocitySurfQueue) + require(queue is VelocityQueueImpl) launch { try { queue.markPlayerDisconnected(uuid) diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueDisplay.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueDisplay.kt index ab22798..ab3aee9 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueDisplay.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/QueueDisplay.kt @@ -5,7 +5,7 @@ import dev.slne.surf.surfapi.core.api.messages.adventure.buildText import it.unimi.dsi.fastutil.objects.ObjectList import java.util.* -class QueueDisplay(private val queue: VelocitySurfQueue) { +class QueueDisplay(private val queue: VelocityQueueImpl) { companion object { private val spinner = arrayOf("∙∙∙", "●∙∙", "∙ ●∙", "∙∙ ●", "∙∙∙") diff --git a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/VelocitySurfQueue.kt b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/VelocityQueueImpl.kt similarity index 90% rename from surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/VelocitySurfQueue.kt rename to surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/VelocityQueueImpl.kt index 896c9c4..8d9f1ce 100644 --- a/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/VelocitySurfQueue.kt +++ b/surf-queue-velocity/src/main/kotlin/dev/slne/surf/queue/velocity/queue/VelocityQueueImpl.kt @@ -5,7 +5,7 @@ import dev.slne.surf.queue.common.queue.tick.SafeQueueTick import java.time.Instant import java.util.* -class VelocitySurfQueue(serverName: String) : AbstractQueue(serverName) { +class VelocityQueueImpl(serverName: String) : AbstractQueue(serverName) { val display = QueueDisplay(this) suspend fun markPlayerReconnected(uuid: UUID) {