Skip to content

Add structures to support hot restarting #232

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 31 commits into
base: 3.X
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
194ddab
Add configs
freya022 May 24, 2025
2b6b2ee
Start adding hot reloading
freya022 May 24, 2025
3f57cff
Listen to file changes using the NIO WatchService
freya022 May 24, 2025
56c7a69
Move path walkers to a util file
freya022 May 24, 2025
c7eb5d3
Start adding restart listeners, refactor
freya022 May 25, 2025
3a6f956
Rename SpringJDARestartListener to SpringJDAShutdownHandler
freya022 May 25, 2025
f379efd
Moved restarter to module, daemon internal coroutine dispatchers
freya022 May 28, 2025
78236ad
notes
freya022 May 30, 2025
1df8f4c
Move RequiresDefaultInjection to API
freya022 May 31, 2025
1b4c49b
Update names
freya022 May 31, 2025
3d87a88
Move main args to BConfig through entry point, leave feature enabled
freya022 May 31, 2025
8c276e7
Use service-loaded configurer to override ClassGraph's class loader
freya022 May 31, 2025
63669b5
Set restarter dependency's scope to `test`
freya022 May 31, 2025
1c0d5a6
Remove restart test files
freya022 May 31, 2025
c93b35d
Remove try/catch in test file
freya022 May 31, 2025
27e8163
Remove BConfig#beforeStart
freya022 May 31, 2025
ff7d92d
tests: Add back stacktrace-decoroutinator
freya022 May 31, 2025
7f287c0
Add notes
freya022 May 31, 2025
9761cfe
Fix name of `JDAConfiguration.shutdownTimeout` replacement property
freya022 May 31, 2025
80c4a99
Disable local Restarter build
freya022 May 31, 2025
ca747b5
Add BRestartConfig#cacheKey
freya022 Jun 3, 2025
0ba3344
Add BContext#restartConfig
freya022 Jun 3, 2025
d2013d3
Shutdown the bot immediately after receiving a ContextClosedEvent
freya022 Jun 5, 2025
81271d3
Remove ClassGraphConfigurer
freya022 Jun 5, 2025
c123c8c
Don't set a shutdown hook when using Spring
freya022 Jun 5, 2025
41b8109
Shutdown executors after all shards are shut down
freya022 Jun 5, 2025
a567d3e
Replace `Duration.INFINITE` by `ZERO` when awaiting JDA termination
freya022 Jun 5, 2025
7b23461
Check for any JDA status beyond `CONNECTING_TO_WEBSOCKET`
freya022 Jun 5, 2025
b3133de
Use an impossible deadline when the timeout is negative or infinite
freya022 Jun 6, 2025
ba842f2
Don't use shutdown() in shutdownNow()
freya022 Jun 6, 2025
9366ee3
Run BotOwnersImpl#onInjectedJDA async
freya022 Jun 6, 2025
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
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,12 @@
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
<!-- <dependency>-->
<!-- <groupId>dev.freya02</groupId>-->
<!-- <artifactId>BotCommands-Restarter</artifactId>-->
<!-- <version>${project.version}</version>-->
<!-- <scope>test</scope>-->
<!-- </dependency>-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
Expand Down
4 changes: 2 additions & 2 deletions src/examples/kotlin/io/github/freya022/bot/Main.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.freya022.bot

import ch.qos.logback.classic.ClassicConstants as LogbackConstants
import dev.reformator.stacktracedecoroutinator.jvm.DecoroutinatorJvmApi
import io.github.freya022.bot.config.Config
import io.github.freya022.bot.config.Environment
Expand All @@ -10,7 +11,6 @@ import net.dv8tion.jda.api.interactions.DiscordLocale
import java.lang.management.ManagementFactory
import kotlin.io.path.absolutePathString
import kotlin.system.exitProcess
import ch.qos.logback.classic.ClassicConstants as LogbackConstants

private val logger by lazy { KotlinLogging.logger {} } // Must not load before system property is set

Expand Down Expand Up @@ -39,7 +39,7 @@ object Main {

val config = Config.instance

BotCommands.create {
BotCommands.create(args) {
disableExceptionsInDMs = Environment.isDev

addPredefinedOwners(*config.ownerIds.toLongArray())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import net.dv8tion.jda.api.utils.TimeFormat
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.nanoseconds

private val deleteScope = namedDefaultScope("Rate limit message delete", 1)
private val deleteScope = namedDefaultScope("Rate limit message delete", 1, isDaemon = true)

/**
* Default [RateLimitHandler] implementation based on [rate limit scopes][RateLimitScope].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import io.github.freya022.botcommands.api.core.service.annotations.InterfacedSer
import io.github.freya022.botcommands.api.core.service.getService
import io.github.freya022.botcommands.internal.core.exceptions.ServiceException
import net.dv8tion.jda.api.JDA
import java.time.Duration as JavaDuration
import kotlin.time.Duration
import kotlin.time.toKotlinDuration

/**
* Main context for BotCommands framework.
Expand Down Expand Up @@ -43,7 +46,11 @@ interface BContext {
*
* Fires [BReadyEvent].
*/
READY
READY,

SHUTTING_DOWN,

SHUTDOWN,
}

//region Configs
Expand All @@ -62,6 +69,8 @@ interface BContext {
get() = config.appEmojisConfig
val textConfig: BTextConfig
get() = config.textConfig
val restartConfig: BRestartConfig
get() = config.restartConfig
//endregion

//region Services
Expand Down Expand Up @@ -144,6 +153,16 @@ interface BContext {
*/
fun getExceptionContent(message: String, t: Throwable?, extraContext: Map<String, Any?>): String

fun shutdown()

fun shutdownNow()

fun awaitShutdown(): Boolean = awaitShutdown(Duration.INFINITE)

fun awaitShutdown(timeout: JavaDuration): Boolean = awaitShutdown(timeout.toKotlinDuration())

fun awaitShutdown(timeout: Duration): Boolean

/**
* Returns the [TextCommandsContext] service.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ object BotCommands {
*/
@JvmStatic
@JvmName("create")
@Deprecated(message = "Replaced with create(String, ReceiverConsumer)", ReplaceWith("create(args, configConsumer)"))
fun createJava(configConsumer: ReceiverConsumer<BConfigBuilder>): BContext {
return create(configConsumer = configConsumer)
return create(emptyArray(), configConsumer = configConsumer)
}

/**
Expand All @@ -55,14 +56,48 @@ object BotCommands {
* @see BotCommands
*/
@JvmSynthetic
@Deprecated(message = "Replaced with create(String, ReceiverConsumer)", ReplaceWith("create(args, configConsumer)"))
fun create(configConsumer: BConfigBuilder.() -> Unit): BContext {
return build(BConfigBuilder().apply(configConsumer).build())
return build(BConfigBuilder(emptyList()).apply(configConsumer).build())
}

/**
* Creates a new instance of the framework.
*
* @return The context for the newly created framework instance,
* while this is returned, using it *usually* is not a good idea,
* your architecture should rely on [dependency injection](https://bc.freya02.dev/3.X/using-botcommands/dependency-injection/)
* and events instead.
*
* @see BotCommands
*/
@JvmStatic
@JvmName("create")
fun createJava(args: Array<out String>, configConsumer: ReceiverConsumer<BConfigBuilder>): BContext {
return create(args, configConsumer = configConsumer)
}

/**
* Creates a new instance of the framework.
*
* @return The context for the newly created framework instance,
* while this is returned, using it *usually* is not a good idea,
* your architecture should rely on [dependency injection](https://bc.freya02.dev/3.X/using-botcommands/dependency-injection/)
* and events instead.
*
* @see BotCommands
*/
@JvmSynthetic
fun create(args: Array<out String>, configConsumer: BConfigBuilder.() -> Unit): BContext {
return build(BConfigBuilder(args.toList()).apply(configConsumer).build())
}

private fun build(config: BConfig): BContext {
val (context, duration) = measureTimedValue {
val bootstrap = DefaultBotCommandsBootstrap(config)
bootstrap.injectAndLoadServices()
bootstrap.injectServices()
bootstrap.signalStart()
bootstrap.loadServices()
bootstrap.loadContext()
bootstrap.serviceContainer.getService<BContext>()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.github.freya022.botcommands.api.core.config

import io.github.freya022.botcommands.api.ReceiverConsumer
import io.github.freya022.botcommands.api.commands.text.annotations.Hidden
import io.github.freya022.botcommands.api.core.BotCommands
import io.github.freya022.botcommands.api.core.BotOwners
import io.github.freya022.botcommands.api.core.annotations.BEventListener
import io.github.freya022.botcommands.api.core.requests.PriorityGlobalRestRateLimiter
Expand All @@ -12,6 +13,7 @@ import io.github.freya022.botcommands.api.core.utils.loggerOf
import io.github.freya022.botcommands.api.core.utils.toImmutableList
import io.github.freya022.botcommands.api.core.utils.toImmutableSet
import io.github.freya022.botcommands.api.core.waiter.EventWaiter
import io.github.freya022.botcommands.api.restart.ExperimentalRestartApi
import io.github.freya022.botcommands.internal.core.config.ConfigDSL
import io.github.freya022.botcommands.internal.core.config.ConfigurationValue
import io.github.oshai.kotlinlogging.KotlinLogging
Expand All @@ -20,9 +22,20 @@ import net.dv8tion.jda.api.requests.GatewayIntent
import net.dv8tion.jda.api.requests.RestRateLimiter
import net.dv8tion.jda.api.utils.messages.MessageCreateData
import org.intellij.lang.annotations.Language
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

@InjectedService
interface BConfig {

/**
* The list of arguments passed to this program's entry point.
*
* This property is supplied by the entry point ([BotCommands] or Spring),
* thus it has no writable property for it.
*/
val args: List<String>

/**
* Predefined user IDs of the bot owners, allowing bypassing cooldowns, user permission checks,
* and having [hidden commands][Hidden] shown.
Expand Down Expand Up @@ -107,6 +120,14 @@ interface BConfig {

val classGraphProcessors: List<ClassGraphProcessor>

@ConfigurationValue("botcommands.core.enableShutdownHook", defaultValue = "false")
val enableShutdownHook: Boolean

// TODO java duration
// TODO this will apply only to hot restarts, move it to a BHotRestartConfig prob
@ConfigurationValue("botcommands.core.shutdownTimeout", type = "java.time.Duration", defaultValue = "10s")
val shutdownTimeout: Duration

val serviceConfig: BServiceConfig
val databaseConfig: BDatabaseConfig
val localizationConfig: BLocalizationConfig
Expand All @@ -116,10 +137,13 @@ interface BConfig {
val modalsConfig: BModalsConfig
val componentsConfig: BComponentsConfig
val coroutineScopesConfig: BCoroutineScopesConfig
val restartConfig: BRestartConfig
}

@ConfigDSL
class BConfigBuilder internal constructor() : BConfig {
class BConfigBuilder internal constructor(
override val args: List<String>,
) : BConfig {
override val packages: MutableSet<String> = HashSet()
override val classes: MutableSet<Class<*>> = HashSet()

Expand All @@ -138,6 +162,10 @@ class BConfigBuilder internal constructor() : BConfig {

override val classGraphProcessors: MutableList<ClassGraphProcessor> = arrayListOf()

override var enableShutdownHook: Boolean = true

override var shutdownTimeout: Duration = 10.seconds

override val serviceConfig = BServiceConfigBuilder()
override val databaseConfig = BDatabaseConfigBuilder()
override val localizationConfig = BLocalizationConfigBuilder()
Expand All @@ -147,6 +175,8 @@ class BConfigBuilder internal constructor() : BConfig {
override val modalsConfig = BModalsConfigBuilder()
override val componentsConfig = BComponentsConfigBuilder()
override val coroutineScopesConfig = BCoroutineScopesConfigBuilder()
@ExperimentalRestartApi
override val restartConfig = BRestartConfigBuilder()

/**
* Predefined user IDs of the bot owners, allowing bypassing cooldowns, user permission checks,
Expand Down Expand Up @@ -272,13 +302,19 @@ class BConfigBuilder internal constructor() : BConfig {
componentsConfig.apply(block)
}

@ExperimentalRestartApi
fun restart(block: ReceiverConsumer<BRestartConfigBuilder>) {
restartConfig.apply(block)
}

@JvmSynthetic
internal fun build(): BConfig {
val logger = KotlinLogging.loggerOf<BConfig>()
if (disableExceptionsInDMs)
logger.info { "Disabled sending exception in bot owners DMs" }

return object : BConfig {
override val args = this@BConfigBuilder.args.toImmutableList()
override val predefinedOwnerIds = this@BConfigBuilder.predefinedOwnerIds.toImmutableSet()
override val packages = this@BConfigBuilder.packages.toImmutableSet()
override val classes = this@BConfigBuilder.classes.toImmutableSet()
Expand All @@ -288,6 +324,8 @@ class BConfigBuilder internal constructor() : BConfig {
override val ignoredEventIntents = this@BConfigBuilder.ignoredEventIntents.toImmutableSet()
override val ignoreRestRateLimiter = this@BConfigBuilder.ignoreRestRateLimiter
override val classGraphProcessors = this@BConfigBuilder.classGraphProcessors.toImmutableList()
override val enableShutdownHook = this@BConfigBuilder.enableShutdownHook
override val shutdownTimeout = this@BConfigBuilder.shutdownTimeout
override val serviceConfig = this@BConfigBuilder.serviceConfig.build()
override val databaseConfig = this@BConfigBuilder.databaseConfig.build()
override val localizationConfig = this@BConfigBuilder.localizationConfig.build()
Expand All @@ -297,6 +335,8 @@ class BConfigBuilder internal constructor() : BConfig {
override val modalsConfig = this@BConfigBuilder.modalsConfig.build()
override val componentsConfig = this@BConfigBuilder.componentsConfig.build()
override val coroutineScopesConfig = this@BConfigBuilder.coroutineScopesConfig.build()
@ExperimentalRestartApi
override val restartConfig = this@BConfigBuilder.restartConfig.build()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.github.freya022.botcommands.api.core.config

import io.github.freya022.botcommands.api.core.service.annotations.InjectedService
import io.github.freya022.botcommands.api.restart.ExperimentalRestartApi
import io.github.freya022.botcommands.internal.core.config.ConfigDSL
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

@InjectedService
interface BRestartConfig {
val cacheKey: String?

// TODO java duration
val restartDelay: Duration
}

@ExperimentalRestartApi
@ConfigDSL
class BRestartConfigBuilder : BRestartConfig {

override var cacheKey: String? = null

override var restartDelay: Duration = 1.seconds

internal fun build() = object : BRestartConfig {
override val cacheKey = this@BRestartConfigBuilder.cacheKey
override val restartDelay = this@BRestartConfigBuilder.restartDelay
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ package io.github.freya022.botcommands.api.core.config

import io.github.freya022.botcommands.api.core.JDAService
import io.github.freya022.botcommands.internal.core.config.ConfigurationValue
import io.github.freya022.botcommands.internal.core.config.DeprecatedValue
import io.github.freya022.botcommands.internal.core.config.IgnoreDefaultValue
import net.dv8tion.jda.api.requests.GatewayIntent
import net.dv8tion.jda.api.utils.cache.CacheFlag
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.bind.Name
import org.springframework.context.event.ContextClosedEvent
import java.time.Duration as JavaDuration
import kotlin.time.Duration
import kotlin.time.toKotlinDuration
import java.time.Duration as JavaDuration

/**
* Configuration properties for [JDAService].
Expand Down Expand Up @@ -43,6 +44,7 @@ class JDAConfiguration internal constructor(
val devTools: DevTools = DevTools(),
) {

// TODO deprecate in favor of BContextImpl's shutdown hook, which can be disabled
class DevTools internal constructor(
/**
* When Spring devtools are enabled,
Expand All @@ -61,6 +63,8 @@ class JDAConfiguration internal constructor(
* Time to wait until JDA needs to be forcefully shut down,
* in other words, this is the allowed time for a graceful shutdown.
*/
@Deprecated("Replaced with botcommands.core.shutdownTimeout")
@DeprecatedValue("Replaced with botcommands.core.shutdownTimeout", replacement = "botcommands.core.shutdownTimeout")
@ConfigurationValue("jda.devtools.shutdownTimeout", type = "java.time.Duration", defaultValue = "10s")
val shutdownTimeout: Duration = shutdownTimeout.toKotlinDuration()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ internal fun <R> Database.withStatementJava(sql: String, readOnly: Boolean = fal
}

@PublishedApi
internal val dbLeakScope = namedDefaultScope("Connection leak watcher", 1)
internal val dbLeakScope = namedDefaultScope("Connection leak watcher", 1, isDaemon = true)

private val currentTransaction = ThreadLocal<Transaction>()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.github.freya022.botcommands.api.core.events

import io.github.freya022.botcommands.api.core.service.annotations.InterfacedService

@InterfacedService(acceptMultiple = true)
fun interface ApplicationStartListener {

fun onApplicationStart(event: BApplicationStartEvent)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.github.freya022.botcommands.api.core.events

import io.github.freya022.botcommands.api.core.config.BConfig

class BApplicationStartEvent internal constructor(
val config: BConfig,
val args: List<String>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.github.freya022.botcommands.api.core.events

import io.github.freya022.botcommands.api.core.BContext

class BShutdownEvent(context: BContext) : BEvent(context)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.github.freya022.botcommands.internal.core.service.annotations
package io.github.freya022.botcommands.api.core.service.annotations

import io.github.freya022.botcommands.internal.core.service.DefaultInjectionCondition
import org.springframework.context.annotation.Conditional
Expand All @@ -7,4 +7,4 @@ import org.springframework.context.annotation.Conditional
* Makes a service disabled when using Spring
*/
@Conditional(DefaultInjectionCondition::class)
internal annotation class RequiresDefaultInjection
annotation class RequiresDefaultInjection
Loading