Skip to content

Commit

Permalink
Improve clustering system
Browse files Browse the repository at this point in the history
  • Loading branch information
WinG4merBR committed Jan 9, 2025
1 parent ee6186b commit f580896
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 62 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package net.cakeyfox.serializable.data

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class ClusterStats(
val serverCount: Int
) {
@Serializable
data class TopggBotStats(
@SerialName("server_count")
val serverCount: Int
)
}

This file was deleted.

36 changes: 29 additions & 7 deletions foxy/src/main/kotlin/net/cakeyfox/foxy/FoxyInstance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext
import mu.KotlinLogging
import net.cakeyfox.artistry.ArtistryClient
import net.cakeyfox.common.Constants
import net.cakeyfox.foxy.command.FoxyCommandManager
import net.cakeyfox.foxy.command.component.FoxyComponentManager
import net.cakeyfox.foxy.listeners.GuildEventListener
Expand All @@ -16,12 +19,18 @@ import net.cakeyfox.foxy.listeners.MajorEventListener
import net.cakeyfox.foxy.utils.ActivityUpdater
import net.cakeyfox.foxy.utils.config.FoxyConfig
import net.cakeyfox.foxy.utils.FoxyUtils
import net.cakeyfox.foxy.utils.analytics.TopggStatsSender
import net.cakeyfox.foxy.utils.database.MongoDBClient
import net.dv8tion.jda.api.OnlineStatus
import net.dv8tion.jda.api.entities.Activity
import net.dv8tion.jda.api.entities.User
import net.dv8tion.jda.api.requests.GatewayIntent
import net.dv8tion.jda.api.sharding.DefaultShardManagerBuilder
import net.dv8tion.jda.api.sharding.ShardManager
import net.dv8tion.jda.api.utils.ChunkingFilter
import net.dv8tion.jda.api.utils.MemberCachePolicy
import net.dv8tion.jda.api.utils.cache.CacheFlag
import java.net.InetAddress
import java.util.concurrent.ConcurrentLinkedQueue
import kotlin.concurrent.thread
import kotlin.reflect.jvm.jvmName
Expand All @@ -34,20 +43,28 @@ class FoxyInstance(
lateinit var commandHandler: FoxyCommandManager
lateinit var artistryClient: ArtistryClient
lateinit var utils: FoxyUtils
lateinit var currentCluster: FoxyConfig.Cluster
lateinit var interactionManager: FoxyComponentManager
lateinit var environment: String
lateinit var httpClient: HttpClient
lateinit var selfUser: User
private lateinit var topggStatsSender: TopggStatsSender
private lateinit var environment: String

suspend fun start() {
val logger = KotlinLogging.logger(this::class.jvmName)
val activityUpdater = ActivityUpdater(this)
val clusterId = withContext(Dispatchers.IO) {
InetAddress.getLocalHost().hostName
}
currentCluster = config.discord.clusters.find { it.id == clusterId }
?: throw IllegalStateException("Unknown cluster $clusterId, check your config file!")

environment = config.environment
mongoClient = MongoDBClient()
commandHandler = FoxyCommandManager(this)
utils = FoxyUtils(this)
interactionManager = FoxyComponentManager(this)
environment = config.environment
artistryClient = ArtistryClient(config.artistryKey)
artistryClient = ArtistryClient(config.others.artistry.key)
httpClient = HttpClient(CIO) {
install(HttpTimeout) {
requestTimeoutMillis = 60_000
Expand All @@ -70,28 +87,33 @@ class FoxyInstance(
GuildEventListener(this),
InteractionEventListener(this)
)
.setShardsTotal(config.totalShards)
.setShards(config.minShards, config.maxShards)
.setStatus(OnlineStatus.ONLINE)
.setActivity(Activity.customStatus(Constants.DEFAULT_ACTIVITY(config.environment)))
.setShardsTotal(config.discord.totalShards)
.setShards(currentCluster.minShard, currentCluster.maxShard)
.setMemberCachePolicy(MemberCachePolicy.ALL)
.setChunkingFilter(ChunkingFilter.NONE)
.disableCache(CacheFlag.entries)
.enableCache(
CacheFlag.EMOJI,
CacheFlag.STICKER,
CacheFlag.MEMBER_OVERRIDES
)
.setToken(config.discordToken)
.setToken(config.discord.token)
.setEnableShutdownHook(false)
.build()

this.commandHandler.handle()

selfUser = shardManager.shards.first().selfUser
topggStatsSender = TopggStatsSender(this)

Runtime.getRuntime().addShutdownHook(thread(false) {
try {
logger.info { "Foxy is shutting down..." }
shardManager.shards.forEach { shard ->
shard.removeEventListener(*shard.registeredListeners.toTypedArray())
logger.info { "Shutting down shard #${shard.shardInfo.shardId}..."}
logger.info { "Shutting down shard #${shard.shardInfo.shardId}..." }
shard.shutdown()
}
httpClient.close()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ class FoxyCommandManager(private val foxy: FoxyInstance) {

suspend fun handle(): MutableList<Command> {
val allCommands = mutableListOf<Command>()
logger.info { "Starting command handling for ${foxy.shardManager.shards.size + 1} shards" }
foxy.shardManager.shards.forEach { shard ->
val action = shard.updateCommands()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,43 @@
package net.cakeyfox.foxy.command.vanilla.utils

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.cakeyfox.common.Colors
import net.cakeyfox.common.FoxyEmotes
import net.cakeyfox.foxy.command.FoxyInteractionContext
import net.cakeyfox.foxy.command.structure.FoxyCommandExecutor
import net.cakeyfox.foxy.utils.pretty
import java.net.InetAddress

class PingExecutor : FoxyCommandExecutor() {
override suspend fun execute(context: FoxyInteractionContext) {
val gatewayPing = context.jda.gatewayPing
val currentShardId = context.jda.shardInfo.shardId + 1
val totalShards = context.jda.shardInfo.shardTotal
val hostname = withContext(Dispatchers.IO) {
InetAddress.getLocalHost().hostName
}

val response = pretty(FoxyEmotes.FoxyHowdy, "**Pong!**\n") +
pretty(FoxyEmotes.FoxyWow, "**Gateway Ping:** `${gatewayPing}ms`\n") +
pretty(FoxyEmotes.FoxyThink, "**Shard ID:** `${currentShardId}/${totalShards}`\n") +
pretty(FoxyEmotes.FoxyCupcake, "**Cluster:** `$hostname`")
val minClusterShards = context.foxy.currentCluster.minShard
val maxClusterShards = context.foxy.currentCluster.maxShard

context.reply {
content = response
embed {
title = pretty(FoxyEmotes.FoxyWow, "Pong!")
thumbnail = context.foxy.selfUser.effectiveAvatarUrl
color = Colors.FOXY_DEFAULT

field {
name = pretty(FoxyEmotes.FoxyCupcake, "Gateway Ping:")
value = "`${gatewayPing}ms`"
inline = false
}

field {
name = pretty(FoxyEmotes.FoxyThink, "Shard")
value = "`${currentShardId}/${totalShards}`"
inline = false
}

field {
name = pretty(FoxyEmotes.FoxyDrinkingCoffee, "Cluster:")
value = "`${context.foxy.currentCluster.name}` **(Shard ${minClusterShards}/${maxClusterShards})**"
inline = false
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,21 @@ package net.cakeyfox.foxy.listeners

import kotlinx.coroutines.*
import mu.KotlinLogging
import net.cakeyfox.common.Constants
import net.cakeyfox.foxy.FoxyInstance
import net.cakeyfox.foxy.modules.antiraid.AntiRaidModule
import net.cakeyfox.foxy.utils.analytics.TopggStatsSender
import net.dv8tion.jda.api.OnlineStatus
import net.dv8tion.jda.api.entities.Activity
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
import net.dv8tion.jda.api.events.session.ReadyEvent
import net.dv8tion.jda.api.hooks.ListenerAdapter
import kotlin.reflect.jvm.jvmName

class MajorEventListener(private val foxy: FoxyInstance): ListenerAdapter() {
class MajorEventListener(foxy: FoxyInstance): ListenerAdapter() {
private val logger = KotlinLogging.logger(this::class.jvmName)
private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private val antiRaid = AntiRaidModule(foxy)
private val topggStats = TopggStatsSender(foxy)

override fun onReady(event: ReadyEvent) {
coroutineScope.launch {
event.jda.presence.setPresence(
OnlineStatus.ONLINE,
Activity.customStatus(Constants.DEFAULT_ACTIVITY(foxy.environment)))

logger.info { "Shard #${event.jda.shardInfo.shardId} is ready!" }

if (foxy.environment == "production") {
topggStats.send(event.jda.guildCache.size())
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class ActivityUpdater(
val foxy: FoxyInstance
) {
private val logger = KotlinLogging.logger(this::class.jvmName)
private val server = embeddedServer(Netty, port = foxy.config.activityPort) {
private val server = embeddedServer(Netty, port = foxy.config.others.activityUpdater.port) {
install(ContentNegotiation) {
json(
Json {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,137 @@
package net.cakeyfox.foxy.utils.analytics

import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.http.content.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import mu.KotlinLogging
import net.cakeyfox.foxy.FoxyInstance
import net.cakeyfox.foxy.utils.analytics.utils.StatsSender
import net.cakeyfox.serializable.data.TopggBotStats
import net.cakeyfox.serializable.data.ClusterStats
import kotlin.reflect.jvm.jvmName

class TopggStatsSender(
val foxy: FoxyInstance
): StatsSender {
) {
private val logger = KotlinLogging.logger(this::class.jvmName)
private val token = foxy.config.dblToken
private val clientId = foxy.config.applicationId
private val token = foxy.config.others.topggToken
private val clientId = foxy.config.discord.applicationId
private var statsSenderJob: Job? = null

override suspend fun send(guildCount: Long): Boolean {
init {
if (foxy.currentCluster.canPublishStats && foxy.config.environment == "production") {
startMainClusterRoutine()
} else {
startHttpServer()
}
}

private fun startMainClusterRoutine() {
logger.info { "Running TopggStatsSender on Main Cluster" }

statsSenderJob = CoroutineScope(Dispatchers.IO).launch {
while (true) {
val serverCounts = getServerCountsFromClusters()
sendStatsToTopGG(serverCounts)
delay(3600000)
}
}
}

private suspend fun getServerCountsFromClusters(): Int {
val client = HttpClient()
val clusterUrls = foxy.config.discord.clusters
.mapNotNull { if (!it.canPublishStats) it.clusterUrl else null }

if (foxy.currentCluster.maxShard == 0 && foxy.currentCluster.minShard == 0) {
return withContext(Dispatchers.IO) {
foxy.shardManager.shards
.map { it.awaitReady() }
.sumOf { it.guilds.size }
}
}

val otherClusterCounts = coroutineScope {
clusterUrls.map { url ->
async {
try {
val response = client.get(url)
Json.decodeFromString<ClusterStats>(response.bodyAsText()).serverCount
} catch (e: Exception) {
logger.error(e) { "Failed to fetch server count from $url" }
0
}
}
}.awaitAll()
}

val currentClusterCount = withContext(Dispatchers.IO) {
foxy.shardManager.shards
.map { it.awaitReady() }
.sumOf { it.guilds.size }
}

logger.info { "Current cluster has $currentClusterCount servers." }

val totalServerCount = currentClusterCount + otherClusterCounts.sum()
logger.info { "Total server count: $totalServerCount" }

return totalServerCount
}

private suspend fun sendStatsToTopGG(serverCount: Int): Boolean {
return withContext(Dispatchers.IO) {
val response = foxy.httpClient.post("https://top.gg/api/bots/$clientId/stats") {
header("Authorization", token)
accept(ContentType.Application.Json)
setBody(
TextContent(Json.encodeToString(TopggBotStats(guildCount)), ContentType.Application.Json)
TextContent(
Json.encodeToString(ClusterStats.TopggBotStats(serverCount)),
ContentType.Application.Json
)
)
}

if (response.status != HttpStatusCode.OK) {
logger.error { "Failed to send stats to top.gg: ${response.status}" }
return@withContext false
}

logger.info { "Successfully sent stats to top.gg" }
logger.info { "Sending $serverCount servers to Top.gg" }
return@withContext true
}
}

private fun startHttpServer() {
embeddedServer(Netty, port = foxy.config.others.statsSenderPort) {
install(ContentNegotiation) {
json()
}

routing {
get("/guilds") {
val serverCount = foxy.shardManager.shards.sumOf { it.guilds.size }
val response = buildJsonObject {
put("serverCount", serverCount)
}
val jsonString = Json.encodeToString(response)
call.respondText(
contentType = ContentType.Application.Json,
text = jsonString
)
}
}
}.start(wait = false)
}
}
Loading

0 comments on commit f580896

Please sign in to comment.