Status tracking rewrite + code cleanup (#116)

This commit is contained in:
Nova Fox
2021-10-12 15:10:40 -05:00
committed by GitHub
parent a9b5b04f8d
commit 149b73b73a
34 changed files with 486 additions and 570 deletions
@@ -1,195 +0,0 @@
package org.dreamexposure.discal.core.object.network.discal;
import org.dreamexposure.discal.Application;
import org.dreamexposure.discal.GitProperty;
import org.dreamexposure.discal.core.database.DatabaseManager;
import org.dreamexposure.discal.core.utils.JsonUtil;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import static org.dreamexposure.discal.core.utils.GlobalVal.getSTATUS;
/**
* @author NovaFox161
* Date Created: 9/8/2018
* For Project: DisCal-Discord-Bot
* Author Website: https://www.novamaday.com
* Company Website: https://www.dreamexposure.org
* Contact: nova@dreamexposure.org
*/
@SuppressWarnings("Duplicates")
@Component
public class NetworkInfo {
private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());
private final List<ConnectedClient> clients = new CopyOnWriteArrayList<>();
private int calCount;
private int announcementCount;
private int guildCount;
private String uptime;
private UUID instanceId;
public Mono<Void> update() {
return DatabaseManager.INSTANCE.getCalendarCount()
.doOnNext(i -> this.calCount = i)
.then(DatabaseManager.INSTANCE.getAnnouncementCount())
.doOnNext(i -> this.announcementCount = i)
.then();
}
//Getters
public List<ConnectedClient> getClients() {
return new CopyOnWriteArrayList<>(this.clients);
}
public boolean doesClientExist(final int clientIndex) {
for (final ConnectedClient cc : this.clients) {
if (cc.getClientIndex() == clientIndex)
return true;
}
return false;
}
public ConnectedClient getClient(final int clientIndex) {
for (final ConnectedClient cc : this.clients) {
if (cc.getClientIndex() == clientIndex)
return cc;
}
return null;
}
public void addClient(final ConnectedClient client) {
this.clients.add(client);
this.clients.sort(Comparator.comparingInt(ConnectedClient::getClientIndex));
LOGGER.info(getSTATUS(), "Client connected to network | Index: " + client.getClientIndex());
}
public void updateClient(ConnectedClient client) {
if (this.doesClientExist(client.getClientIndex()))
this.clients.remove(this.getClient(client.getClientIndex()));
this.clients.add(client);
this.clients.sort(Comparator.comparingInt(ConnectedClient::getClientIndex));
}
public void removeClient(final int clientIndex) {
if (this.doesClientExist(clientIndex)) {
this.clients.remove(this.getClient(clientIndex));
LOGGER.warn(getSTATUS(), "Client disconnected from network | Index: " + clientIndex);
}
}
public void removeClient(final int clientIndex, final String reason) {
if (this.doesClientExist(clientIndex)) {
this.clients.remove(this.getClient(clientIndex));
LOGGER.warn(getSTATUS(), "Client disconnected from network | Index: " + clientIndex + " | Reason: " + reason);
}
}
public String getVersion() {
return GitProperty.DISCAL_VERSION.getValue();
}
public String getD4JVersion() {
return GitProperty.DISCAL_VERSION_D4J.getValue();
}
public int getTotalGuildCount() {
int count = 0;
for (final ConnectedClient cc : this.clients) {
count += cc.getConnectedServers();
}
this.guildCount = count;
return this.guildCount;
}
public int getClientCount() {
return this.clients.size();
}
public int getExpectedClientCount() {
if (!clients.isEmpty())
return clients.get(0).getExpectedClientCount();
else
return -1;
}
public int getCalendarCount() {
return this.calCount;
}
public int getAnnouncementCount() {
return this.announcementCount;
}
public String getUptime() {
return this.uptime;
}
public UUID getInstanceId() {
return this.instanceId;
}
//Setters
public void setCalCount(final int calCount) {
this.calCount = calCount;
}
public void setAnnouncementCount(final int announcementCount) {
this.announcementCount = announcementCount;
}
public void setInstanceId(final UUID instanceId) {
this.instanceId = instanceId;
}
public JSONObject toJson() {
final JSONObject json = new JSONObject();
json.put("api_version", this.getVersion());
json.put("d4j_version", this.getD4JVersion());
json.put("api_uptime", Application.getHumanReadableUptime());
json.put("api_instance_id", this.instanceId);
json.put("announcements", this.getAnnouncementCount());
json.put("total_guilds", this.getTotalGuildCount());
json.put("calendars", this.getCalendarCount());
final JSONArray jClients = new JSONArray();
for (final ConnectedClient c : this.clients)
jClients.put(JsonUtil.INSTANCE.encodeToJSON(ConnectedClient.class, c));
json.put("clients", jClients);
return json;
}
public NetworkInfo fromJson(final JSONObject json) {
this.uptime = json.getString("api_uptime");
this.instanceId = UUID.fromString(json.getString("api_instance_id"));
this.announcementCount = json.getInt("announcements");
this.guildCount = json.getInt("total_guilds");
this.calCount = json.getInt("calendars");
final JSONArray jClients = json.getJSONArray("clients");
for (int i = 0; i < jClients.length(); i++)
this.clients.add(JsonUtil.INSTANCE.decodeFromJSON(ConnectedClient.class, jClients.getJSONObject(i)));
return this;
}
}
@@ -1 +0,0 @@
package org.dreamexposure.discal.core.object.network.discal;
@@ -1 +0,0 @@
package org.dreamexposure.discal.core.object.network;
@@ -1 +0,0 @@
package org.dreamexposure.discal.core.object;
@@ -7,13 +7,13 @@ import org.springframework.boot.autoconfigure.session.SessionAutoConfiguration
import java.lang.management.ManagementFactory
import java.time.Duration
import java.util.*
import kotlin.math.roundToInt
@SpringBootApplication(exclude = [SessionAutoConfiguration::class, R2dbcAutoConfiguration::class])
class Application {
companion object {
val instanceId: UUID = UUID.randomUUID()
@JvmStatic
fun getShardIndex(): String {
/*
This fucking sucks. So k8s doesn't expose the pod ordinal for a pod in a stateful set
@@ -38,22 +38,26 @@ class Application {
}
}
@JvmStatic
fun getShardCount(): Int {
val shardCount = System.getenv("SHARD_COUNT")
return shardCount?.toInt() ?: //Fall back to config
BotSettings.SHARD_COUNT.get().toInt()
}
@JvmStatic
fun getHumanReadableUptime(): String {
fun getUptime(): Duration {
val mxBean = ManagementFactory.getRuntimeMXBean()
val rawDuration = System.currentTimeMillis() - mxBean.startTime
val duration = Duration.ofMillis(rawDuration)
return Duration.ofMillis(rawDuration)
}
return "%d days, %d hours, %d minutes, %d seconds%n"
.format(duration.toDays(), duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart())
fun getMemoryUsedInMb(): Double {
val totalMemory = Runtime.getRuntime().totalMemory()
val freeMemory = Runtime.getRuntime().freeMemory()
val raw = (totalMemory - freeMemory) / (1024 * 1024).toDouble()
return (raw * 100).roundToInt().toDouble() / 100
}
}
}
@@ -27,7 +27,6 @@ import org.dreamexposure.discal.core.enums.time.TimeFormat
import org.dreamexposure.discal.core.extensions.asStringList
import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT
import org.dreamexposure.novautils.database.DatabaseSettings
import org.intellij.lang.annotations.Language
import reactor.core.publisher.Mono
import reactor.util.retry.Retry
@@ -37,15 +36,6 @@ import java.util.*
import java.util.function.Function
object DatabaseManager {
private val settings: DatabaseSettings = DatabaseSettings(
BotSettings.SQL_HOST.get(),
BotSettings.SQL_PORT.get(),
BotSettings.SQL_DB.get(),
BotSettings.SQL_USER.get(),
BotSettings.SQL_PASS.get(),
BotSettings.SQL_PREFIX.get()
)
private val pool: ConnectionPool
init {
@@ -53,11 +43,11 @@ object DatabaseManager {
builder()
.option(DRIVER, "pool")
.option(PROTOCOL, "mysql")
.option(HOST, settings.hostname)
.option(PORT, settings.port.toInt())
.option(USER, settings.user)
.option(PASSWORD, settings.password)
.option(DATABASE, settings.database)
.option(HOST, BotSettings.SQL_HOST.get())
.option(PORT, BotSettings.SQL_PORT.get().toInt())
.option(USER, BotSettings.SQL_USER.get())
.option(PASSWORD, BotSettings.SQL_PASS.get())
.option(DATABASE, BotSettings.SQL_DB.get())
.build()
)
@@ -0,0 +1,6 @@
package org.dreamexposure.discal.core.extensions
import java.time.Duration
fun Duration.getHumanReadable() = "%d days, %d hours, %d minutes, %d seconds%n"
.format(toDays(), toHoursPart(), toMinutesPart(), toSecondsPart())
@@ -4,35 +4,35 @@ import discord4j.common.util.Snowflake
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import org.dreamexposure.discal.core.crypto.KeyGenerator
import org.dreamexposure.discal.core.enums.calendar.CalendarHost
import org.dreamexposure.discal.core.serializers.SnowflakeAsStringSerializer
import org.dreamexposure.novautils.crypto.KeyGenerator
import java.time.Instant
@Serializable
data class CalendarData(
@Serializable(with = SnowflakeAsStringSerializer::class)
@Serializable(with = SnowflakeAsStringSerializer::class)
@SerialName("guild_id")
val guildId: Snowflake = Snowflake.of(0),
@SerialName("calendar_number")
@SerialName("calendar_number")
val calendarNumber: Int = 1,
val host: CalendarHost,
@SerialName("calendar_id")
val host: CalendarHost,
@SerialName("calendar_id")
val calendarId: String = "primary",
@SerialName("calendar_address")
@SerialName("calendar_address")
val calendarAddress: String = "primary",
val external: Boolean = false,
val external: Boolean = false,
//secure values that should not be serialized
@Transient
@Transient
val credentialId: Int = 0,
@Transient
@Transient
var privateKey: String = KeyGenerator.csRandomAlphaNumericString(16),
@Transient
@Transient
var encryptedAccessToken: String = "N/a",
@Transient
@Transient
var encryptedRefreshToken: String = "N/a",
@Transient
@Transient
var expiresAt: Instant = Instant.now()
): Comparable<CalendarData> {
constructor(guildId: Snowflake, calendarNumber: Int, host: CalendarHost, calendarId: String,
@@ -0,0 +1,39 @@
package org.dreamexposure.discal.core.`object`.network.discal
import discord4j.core.GatewayDiscordClient
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.dreamexposure.discal.Application
import reactor.core.publisher.Mono
@Suppress("DataClassPrivateConstructor")
@Serializable
data class BotInstanceData private constructor(
@SerialName("instance")
val instanceData: InstanceData,
@SerialName("shard_index")
val shardIndex: Int,
@SerialName("shard_count")
val shardCount: Int,
val guilds: Int = 0,
) {
companion object {
fun load(client: GatewayDiscordClient?): Mono<BotInstanceData> {
return Mono.justOrEmpty(client)
.flatMap { it.guilds.count() }
.map(Long::toInt)
.defaultIfEmpty(0)
.map { guildCount ->
BotInstanceData(
instanceData = InstanceData(),
shardIndex = Application.getShardIndex().toInt(),
shardCount = Application.getShardCount(),
guilds = guildCount
)
}
}
}
}
@@ -1,42 +0,0 @@
package org.dreamexposure.discal.core.`object`.network.discal
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import org.dreamexposure.discal.GitProperty
import java.text.SimpleDateFormat
import java.util.*
@Serializable
data class ConnectedClient(
@SerialName("index")
val clientIndex: Int = -1,
@SerialName("expected")
val expectedClientCount: Int = -1,
val version: String = GitProperty.DISCAL_VERSION.value,
@SerialName("d4j_version")
val d4jVersion: String = GitProperty.DISCAL_VERSION_D4J.value,
@SerialName("guilds")
val connectedServers: Int = 0,
@SerialName("keep_alive")
val lastKeepAlive: Long = System.currentTimeMillis(),
val uptime: String = "ERROR",
@SerialName("memory")
val memUsed: Double = 0.0,
@Transient
val instanceId: UUID = UUID.randomUUID()
) {
fun getLastKeepAliveHumanReadable(): String {
val sdf = SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
return sdf.format(Date(this.lastKeepAlive))
}
}
@@ -0,0 +1,42 @@
package org.dreamexposure.discal.core.`object`.network.discal
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.dreamexposure.discal.Application
import org.dreamexposure.discal.GitProperty
import org.dreamexposure.discal.core.serializers.DurationAsStringSerializer
import org.dreamexposure.discal.core.serializers.InstantAsStringSerializer
import java.time.Duration
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@Suppress("DataClassPrivateConstructor")
@Serializable
data class InstanceData(
@SerialName("instance_id")
val instanceId: String = Application.instanceId.toString(),
val version: String = GitProperty.DISCAL_VERSION.value,
@SerialName("d4j_version")
val d4jVersion: String = GitProperty.DISCAL_VERSION_D4J.value,
@Serializable(with = DurationAsStringSerializer::class)
val uptime: Duration = Application.getUptime(),
@SerialName("last_heartbeat")
@Serializable(with = InstantAsStringSerializer::class)
val lastHeartbeat: Instant = Instant.now(),
val memory: Double = Application.getMemoryUsedInMb(),
) {
@Suppress("unused") //Used in thymeleaf status page
fun getHumanReadableHeartbeat(): String {
val formatter = DateTimeFormatter
.ofPattern("yyyy/MM/dd HH:mm:ss")
.withZone(ZoneId.of("UTC"))
return "$formatter.format(lastHeartbeat) UTC"
}
}
@@ -0,0 +1,41 @@
package org.dreamexposure.discal.core.`object`.network.discal
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.util.concurrent.CopyOnWriteArrayList
@Serializable
data class NetworkData(
@SerialName("total_calendars")
var totalCalendars: Int = 0,
@SerialName("total_announcements")
var totalAnnouncements: Int = 0,
@SerialName("api_status")
var apiStatus: InstanceData? = null,
@SerialName("website_status")
var websiteStatus: InstanceData? = null,
@SerialName("bot_status")
val botStatus: MutableList<BotInstanceData> = CopyOnWriteArrayList()
) {
@SerialName("expected_shard_count")
val expectedShardCount: Int
get() {
return if (botStatus.isNotEmpty()) botStatus[0].shardCount
else -1
}
@SerialName("total_guilds")
val totalGuilds: Int
get() {
var guilds = 0
botStatus.forEach { guilds += it.guilds }
return guilds
}
@Suppress("unused") //Used in thymeleaf status page
fun getCurrentShardCount() = botStatus.size
}
@@ -2,30 +2,21 @@ package org.dreamexposure.discal.core.`object`.rest
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.dreamexposure.discal.core.serializers.UUIDasStringSerializer
import java.util.*
import org.dreamexposure.discal.core.`object`.network.discal.BotInstanceData
import org.dreamexposure.discal.core.`object`.network.discal.InstanceData
//TODO: Redo
@Serializable
data class HeartbeatRequest(
@SerialName("instance_id")
@Serializable(with = UUIDasStringSerializer::class)
val instanceId: UUID,
val type: HeartbeatType,
@SerialName("client_index")
val clientIndex: String,
@SerialName("instance")
val instanceData: InstanceData? = null,
@SerialName("expected_clients")
val expectedClients: Int,
@SerialName("guilds")
val guildCount: Int,
val memory: Double,
val uptime: String,
val version: String,
@SerialName("d4j_version")
val d4jVersion: String,
@SerialName("bot_instance")
val botInstanceData: BotInstanceData? = null
)
enum class HeartbeatType {
BOT, WEBSITE
}
@@ -0,0 +1,18 @@
package org.dreamexposure.discal.core.serializers
import discord4j.common.util.Snowflake
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.time.Duration
object DurationAsStringSerializer : KSerializer<Duration> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Duration", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Duration) = encoder.encodeString(value.toMillis().toString())
override fun deserialize(decoder: Decoder): Duration = Duration.ofMillis(decoder.decodeString().toLong())
}
@@ -0,0 +1,17 @@
package org.dreamexposure.discal.core.serializers
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.time.Instant
object InstantAsStringSerializer : KSerializer<Instant> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Duration", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): Instant = Instant.parse(decoder.decodeString())
}