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) {