Skip to content

Commit 263f9ad

Browse files
authored
Add hot restarting (#232)
1 parent 6ea0283 commit 263f9ad

File tree

30 files changed

+989
-8
lines changed

30 files changed

+989
-8
lines changed

BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/method/accessors/MethodAccessorFactoryProvider.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import kotlin.reflect.KFunction
1111

1212
internal object MethodAccessorFactoryProvider {
1313

14-
private lateinit var accessorFactory: MethodAccessorFactory
14+
private lateinit var accessorFactory: CachingMethodAccessorFactory
1515
private val staticAccessors: MutableMap<KFunction<*>, MethodAccessor<*>> = hashMapOf()
1616

1717
internal fun getAccessorFactory(): MethodAccessorFactory {
@@ -26,6 +26,12 @@ internal object MethodAccessorFactoryProvider {
2626
return accessorFactory
2727
}
2828

29+
internal fun clearCache() {
30+
if (::accessorFactory.isInitialized) {
31+
accessorFactory.clearCache()
32+
}
33+
}
34+
2935
@OptIn(ExperimentalMethodAccessorsApi::class)
3036
private fun loadAccessorFactory(): MethodAccessorFactory {
3137
if (MethodAccessorsConfig.preferClassFileAccessors) {

BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/core/service/AbstractBotCommandsBootstrap.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import io.github.freya022.botcommands.api.core.events.PreLoadEvent
99
import io.github.freya022.botcommands.api.core.objectLogger
1010
import io.github.freya022.botcommands.api.core.service.getService
1111
import io.github.freya022.botcommands.internal.core.BContextImpl
12+
import io.github.freya022.botcommands.internal.core.method.accessors.MethodAccessorFactoryProvider
13+
import io.github.freya022.botcommands.internal.emojis.AppEmojisLoader
1214
import io.github.freya022.botcommands.internal.utils.ReflectionMetadata
1315
import kotlinx.coroutines.runBlocking
1416
import kotlin.time.DurationUnit
@@ -18,6 +20,9 @@ abstract class AbstractBotCommandsBootstrap(protected val config: BConfig) : Bot
1820
protected val logger = objectLogger()
1921

2022
protected fun init() {
23+
MethodAccessorFactoryProvider.clearCache()
24+
AppEmojisLoader.clear()
25+
2126
measure("Scanned reflection metadata") {
2227
ReflectionMetadata.runScan(config, this)
2328
}

BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/emojis/AppEmojiContainerProcessor.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import io.github.freya022.botcommands.api.core.service.ClassGraphProcessor
55
import io.github.freya022.botcommands.api.core.service.ServiceContainer
66
import io.github.freya022.botcommands.api.core.utils.findAnnotationRecursive
77
import io.github.freya022.botcommands.api.emojis.annotations.AppEmojiContainer
8-
import org.jetbrains.annotations.TestOnly
98
import kotlin.reflect.KClass
109

1110
internal object AppEmojiContainerProcessor : ClassGraphProcessor {
@@ -23,8 +22,7 @@ internal object AppEmojiContainerProcessor : ClassGraphProcessor {
2322
}
2423
}
2524

26-
@TestOnly
2725
internal fun clear() {
2826
emojiClasses.clear()
2927
}
30-
}
28+
}

BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/emojis/AppEmojisLoader.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import net.dv8tion.jda.api.JDA
2323
import net.dv8tion.jda.api.entities.Icon
2424
import net.dv8tion.jda.api.entities.emoji.ApplicationEmoji
2525
import net.dv8tion.jda.internal.utils.Checks
26-
import org.jetbrains.annotations.TestOnly
2726
import kotlin.math.abs
2827
import kotlin.reflect.KProperty
2928
import kotlin.reflect.full.declaredMemberProperties
@@ -235,7 +234,6 @@ internal class AppEmojisLoader internal constructor(
235234
private val toLoad = arrayListOf<LoadRequest>()
236235
private val loadedEmojis = hashMapOf<String, ApplicationEmoji>()
237236

238-
@TestOnly
239237
internal fun clear() {
240238
loaded = false
241239
toLoadEmojiNames.clear()

BotCommands-method-accessors/core/src/main/kotlin/dev/freya02/botcommands/method/accessors/internal/CachingMethodAccessorFactory.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ class CachingMethodAccessorFactory(private val delegate: MethodAccessorFactory)
1313
private val cache = WeakHashMap<Executable, MethodAccessor<*>>()
1414
private val lock = ReentrantLock()
1515

16+
fun clearCache() {
17+
cache.clear()
18+
}
19+
1620
override fun <R> create(
1721
instance: Any?,
1822
function: KFunction<R>,

BotCommands-restarter/README.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
[bc-module-maven-central-shield]: https://img.shields.io/maven-central/v/io.github.freya022/BotCommands-restarter?label=Maven%20central&logo=apachemaven&versionPrefix=3
2+
[bc-module-maven-central-link]: https://central.sonatype.com/artifact/io.github.freya022/BotCommands-restarter
3+
4+
# BotCommands module - Hot restarter
5+
When you build changes of your code, this modules restarts your app automatically, in the same JVM,
6+
leading to much faster restarts, as it doesn't need to recompile most of the code.
7+
8+
> [!WARNING]
9+
> If you are using Spring, use [`spring-boot-devtools`](https://docs.spring.io/spring-boot/reference/using/devtools.html) instead.
10+
11+
## Installing
12+
[![BotCommands-restarter on maven central][bc-module-maven-central-shield] ][bc-module-maven-central-link]
13+
14+
### Maven
15+
```xml
16+
<dependencies>
17+
<dependency>
18+
<groupId>io.github.freya022</groupId>
19+
<artifactId>BotCommands-restarter</artifactId>
20+
<version>VERSION</version>
21+
</dependency>
22+
</dependencies>
23+
```
24+
25+
### Gradle
26+
```gradle
27+
repositories {
28+
mavenCentral()
29+
}
30+
31+
dependencies {
32+
implementation("io.github.freya022:BotCommands-restarter:VERSION")
33+
}
34+
```
35+
36+
### Snapshots
37+
38+
To use the latest, unreleased changes, see [SNAPSHOTS.md](../SNAPSHOTS.md).
39+
40+
## Usage
41+
You can enable the feature by doing so, after which, every build will restart your application.
42+
43+
> [!NOTE]
44+
> You should minimize the amount of code executed before calling `BotCommandsRestarter.initialize`,
45+
> as it will run twice on startup, then everytime it is restarted.
46+
47+
> [!IMPORTANT]
48+
> You must only use this feature during development, here are a few ways to do so:
49+
> - Using a program argument like `--dev` then reading it from `args`
50+
> - Using a configuration file with a `IS_DEV` property
51+
> - Using an environment variable
52+
53+
### Kotlin
54+
```kotlin
55+
fun main(args: Array<out String>) {
56+
// You should enable this only during development
57+
@OptIn(ExperimentalRestartApi::class)
58+
BotCommandsRestarter.initialize(args) {
59+
// Optional configuration
60+
}
61+
62+
// ...
63+
BotCommands.create {
64+
// ...
65+
}
66+
}
67+
```
68+
69+
### Java
70+
```java
71+
void main(String[] args) {
72+
// You should enable this only during development
73+
BotCommandsRestarter.initialize(args, builder -> {
74+
// Optional configuration
75+
});
76+
77+
// ...
78+
BotCommands.create(config -> {
79+
// ...
80+
});
81+
}
82+
```
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import dev.freya02.botcommands.plugins.configureJarArtifact
2+
3+
plugins {
4+
id("repositories-conventions")
5+
id("kotlin-conventions")
6+
id("publish-conventions")
7+
id("dokka-conventions")
8+
}
9+
10+
dependencies {
11+
api(projects.botCommandsCore)
12+
13+
// Logging
14+
implementation(libs.kotlin.logging)
15+
}
16+
17+
kotlin {
18+
compilerOptions {
19+
freeCompilerArgs.addAll(
20+
"-opt-in=dev.freya02.botcommands.restarter.api.annotations.ExperimentalRestartApi",
21+
)
22+
}
23+
}
24+
25+
publishedProjectEnvironment {
26+
configureJarArtifact(
27+
artifactId = "BotCommands-restarter",
28+
description = "Enables restarting your bot on the same JVM during development.",
29+
url = "https://github.com/freya022/BotCommands/tree/3.X/BotCommands-restarter",
30+
)
31+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package dev.freya02.botcommands.restarter.api
2+
3+
import dev.freya02.botcommands.restarter.api.BotCommandsRestarter.initialize
4+
import dev.freya02.botcommands.restarter.api.annotations.ExperimentalRestartApi
5+
import dev.freya02.botcommands.restarter.api.config.RestarterConfigBuilder
6+
import dev.freya02.botcommands.restarter.api.exceptions.ImmediateRestartException
7+
import dev.freya02.botcommands.restarter.internal.Restarter
8+
import io.github.freya022.botcommands.api.ReceiverConsumer
9+
10+
/**
11+
* Entry point for the "hot restart" feature.
12+
*
13+
* @see initialize
14+
*/
15+
@ExperimentalRestartApi
16+
object BotCommandsRestarter {
17+
18+
/**
19+
* Enables hot restarting for this application. All changes to your code will restart the application.
20+
*
21+
* This method will intentionally throw [ImmediateRestartException] on the first run,
22+
* if it is caught, you must rethrow it.
23+
*
24+
* It is recommended to run this function as soon as possible to avoid running the same code twice on startup.
25+
*
26+
* **Note:** If you configure your logger programmatically, it must be done before calling this function.
27+
*
28+
* @param args The program arguments, they will be passed back to the main function upon restarting
29+
* @param configBuilder To further configure the feature, this will only run once per application
30+
*/
31+
@JvmStatic
32+
@JvmOverloads
33+
fun initialize(args: Array<out String>, configBuilder: ReceiverConsumer<RestarterConfigBuilder> = {}) {
34+
if (!Restarter.isInitialized) {
35+
// 1st restart
36+
val config = RestarterConfigBuilder.create(args)
37+
.apply(configBuilder)
38+
.build()
39+
Restarter.initialize(config)
40+
}
41+
42+
// After 1st restart
43+
}
44+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package dev.freya02.botcommands.restarter.api.annotations
2+
3+
import kotlin.annotation.AnnotationTarget.*
4+
5+
/**
6+
* Opt-in marker annotation for "hot restart" APIs that are considered experimental and are not subject to compatibility guarantees:
7+
* The behavior of such API may be changed or the API may be removed completely in any further release.
8+
*
9+
* Please create an issue or join the Discord server if you encounter a problem or want to submit feedback.
10+
*
11+
* Any usage of a declaration annotated with `@ExperimentalRestartApi` must be accepted either by
12+
* annotating that usage with the [@OptIn][OptIn] annotation, e.g. `@OptIn(ExperimentalRestartApi::class)`,
13+
* or by using the compiler argument `-opt-in=dev.freya02.botcommands.restarter.api.ExperimentalRestartApi`.
14+
*/
15+
@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
16+
@Retention(AnnotationRetention.BINARY)
17+
@Target(
18+
CLASS,
19+
ANNOTATION_CLASS,
20+
PROPERTY,
21+
FIELD,
22+
LOCAL_VARIABLE,
23+
VALUE_PARAMETER,
24+
CONSTRUCTOR,
25+
FUNCTION,
26+
PROPERTY_GETTER,
27+
PROPERTY_SETTER,
28+
TYPEALIAS
29+
)
30+
@MustBeDocumented
31+
annotation class ExperimentalRestartApi
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package dev.freya02.botcommands.restarter.api.config
2+
3+
import dev.freya02.botcommands.restarter.api.BotCommandsRestarter
4+
import dev.freya02.botcommands.restarter.api.annotations.ExperimentalRestartApi
5+
import io.github.freya022.botcommands.internal.core.config.ConfigDSL
6+
import java.time.Duration as JavaDuration
7+
import kotlin.time.Duration
8+
import kotlin.time.Duration.Companion.seconds
9+
import kotlin.time.toJavaDuration
10+
import kotlin.time.toKotlinDuration
11+
12+
@ExperimentalRestartApi
13+
interface RestarterConfig {
14+
15+
/**
16+
* The program arguments passed to the main function upon restarting.
17+
*/
18+
val startArgs: List<String>
19+
20+
/**
21+
* The time to wait before assuming all changes were compiled,
22+
* so the application can be restarted with the new changes.
23+
*
24+
* Default: 1 second
25+
*/
26+
val restartDelay: Duration
27+
28+
/**
29+
* Returns the time to wait before assuming all changes were compiled,
30+
* so the application can be restarted with the new changes.
31+
*
32+
* Default: 1 second
33+
*/
34+
fun getRestartDelay(): JavaDuration = restartDelay.toJavaDuration()
35+
}
36+
37+
/**
38+
* Builder of [RestarterConfig].
39+
*
40+
* @see [BotCommandsRestarter.initialize]
41+
*/
42+
@ConfigDSL
43+
@ExperimentalRestartApi
44+
class RestarterConfigBuilder private constructor(
45+
override val startArgs: List<String>,
46+
) : RestarterConfig {
47+
48+
override var restartDelay: Duration = 1.seconds
49+
50+
/**
51+
* Sets the time to wait before assuming all changes were compiled,
52+
* so the application can be restarted with the new changes.
53+
*
54+
* Default: 1 second
55+
*/
56+
fun setRestartDelay(delay: JavaDuration): RestarterConfigBuilder {
57+
this.restartDelay = delay.toKotlinDuration()
58+
return this
59+
}
60+
61+
@JvmSynthetic
62+
internal fun build(): RestarterConfig = object : RestarterConfig {
63+
override val startArgs = this@RestarterConfigBuilder.startArgs
64+
override val restartDelay = this@RestarterConfigBuilder.restartDelay
65+
}
66+
67+
internal companion object {
68+
69+
@JvmSynthetic
70+
internal fun create(args: Array<out String>): RestarterConfigBuilder {
71+
return RestarterConfigBuilder(args.toList())
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)