Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ repositories {
}
}

dependencies { protobuf("gg.grounds:library-grpc-contracts-player:0.1.0") }
dependencies { protobuf("gg.grounds:library-grpc-contracts-player:feat-player-heartbeat-SNAPSHOT") }
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package gg.grounds.player.presence

import gg.grounds.grpc.player.PlayerHeartbeatBatchReply
import gg.grounds.grpc.player.PlayerHeartbeatBatchRequest
import gg.grounds.grpc.player.PlayerLoginRequest
import gg.grounds.grpc.player.PlayerLogoutReply
import gg.grounds.grpc.player.PlayerLogoutRequest
Expand Down Expand Up @@ -49,6 +51,22 @@ private constructor(
}
}

fun heartbeatBatch(playerIds: Collection<UUID>): PlayerHeartbeatBatchReply {
return try {
val request =
PlayerHeartbeatBatchRequest.newBuilder()
.addAllPlayerIds(playerIds.map { it.toString() })
.build()
stub
.withDeadlineAfter(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.playerHeartbeatBatch(request)
} catch (e: StatusRuntimeException) {
errorHeartbeatBatchReply(e.status.toString())
} catch (e: RuntimeException) {
errorHeartbeatBatchReply(e.message ?: e::class.java.name)
}
}

override fun close() {
channel.shutdown()
try {
Expand All @@ -74,6 +92,14 @@ private constructor(
private fun errorLogoutReply(message: String): PlayerLogoutReply =
PlayerLogoutReply.newBuilder().setRemoved(false).setMessage(message).build()

private fun errorHeartbeatBatchReply(message: String): PlayerHeartbeatBatchReply =
PlayerHeartbeatBatchReply.newBuilder()
.setUpdated(0)
.setMissing(0)
.setSuccess(false)
.setMessage(message)
.build()

private fun isServiceUnavailable(status: Status.Code): Boolean =
status == Status.Code.UNAVAILABLE || status == Status.Code.DEADLINE_EXCEEDED

Expand Down
5 changes: 5 additions & 0 deletions velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.velocitypowered.api.plugin.annotation.DataDirectory
import com.velocitypowered.api.proxy.ProxyServer
import gg.grounds.config.MessagesConfigLoader
import gg.grounds.listener.PlayerConnectionListener
import gg.grounds.presence.PlayerHeartbeatScheduler
import gg.grounds.presence.PlayerPresenceService
import io.grpc.LoadBalancerRegistry
import io.grpc.NameResolverRegistry
Expand All @@ -33,6 +34,8 @@ constructor(
@param:DataDirectory private val dataDirectory: Path,
) {
private val playerPresenceService = PlayerPresenceService()
private val heartbeatScheduler =
PlayerHeartbeatScheduler(this, proxy, logger, playerPresenceService)

init {
logger.info("Initialized plugin (plugin=plugin-player, version={})", BuildInfo.VERSION)
Expand All @@ -55,11 +58,13 @@ constructor(
),
)

heartbeatScheduler.start()
logger.info("Configured player presence gRPC client (target={})", target)
}

@Subscribe
fun onShutdown(event: ProxyShutdownEvent) {
heartbeatScheduler.stop()
playerPresenceService.close()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package gg.grounds.presence

import com.velocitypowered.api.proxy.ProxyServer
import com.velocitypowered.api.scheduler.ScheduledTask
import java.util.concurrent.TimeUnit
import org.slf4j.Logger

class PlayerHeartbeatScheduler(
private val plugin: Any,
private val proxy: ProxyServer,
private val logger: Logger,
private val presenceService: PlayerPresenceService,
) {
private var heartbeatTask: ScheduledTask? = null

fun start() {
heartbeatTask?.cancel()
heartbeatTask = null
val heartbeatIntervalSeconds = resolveHeartbeatIntervalSeconds()
heartbeatTask =
proxy.scheduler
.buildTask(plugin, Runnable { sendHeartbeats() })
.repeat(heartbeatIntervalSeconds, TimeUnit.SECONDS)
.schedule()
logger.info(
"Configured player presence heartbeat task (intervalSeconds={})",
heartbeatIntervalSeconds,
)
}

fun stop() {
heartbeatTask?.cancel()
heartbeatTask = null
}

private fun sendHeartbeats() {
val playerIds = proxy.allPlayers.map { it.uniqueId }
if (playerIds.isEmpty()) {
return
}

val result = presenceService.heartbeatBatch(playerIds)
if (!result.success) {
logger.error(
"Player session heartbeat batch failed (playerCount={}, error={})",
playerIds.size,
result.message,
)
} else {
logger.debug(
"Player session heartbeat batch completed (playerCount={}, result=success)",
playerIds.size,
)
}
}

private fun resolveHeartbeatIntervalSeconds(): Long {
val raw = System.getenv("PLAYER_PRESENCE_HEARTBEAT_SECONDS") ?: return 30
return raw.toLongOrNull()?.takeIf { it > 0 } ?: 30
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import java.util.UUID
class PlayerPresenceService : AutoCloseable {
private lateinit var client: GrpcPlayerPresenceClient

data class HeartbeatBatchResult(val success: Boolean, val message: String)

fun configure(target: String) {
close()
client = GrpcPlayerPresenceClient.create(target)
Expand All @@ -29,6 +31,15 @@ class PlayerPresenceService : AutoCloseable {
}
}

fun heartbeatBatch(playerIds: Collection<UUID>): HeartbeatBatchResult {
return try {
val reply = client.heartbeatBatch(playerIds)
HeartbeatBatchResult(reply.success, reply.message)
} catch (e: RuntimeException) {
HeartbeatBatchResult(false, e.message ?: e::class.java.name)
}
}

override fun close() {
if (this::client.isInitialized) {
client.close()
Expand Down