Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
30fe5b3
At least this works
freya022 May 30, 2025
5198bbc
Set Kotlin jvmTarget
freya022 May 30, 2025
6f33b59
Add RestartListener, remove unused code
freya022 May 30, 2025
74cf508
Publish source JAR, add kotlin-logging
freya022 May 30, 2025
10603d5
Rewrite Restarter + LeakSafeExecutor
freya022 May 30, 2025
ee7e1cc
Rewrite SilentExitExceptionHandler
freya022 May 30, 2025
720d171
Add classpath watching classes, small refactor
freya022 May 31, 2025
2341c48
Add ClasspathListener
freya022 May 31, 2025
f717a39
Clean up
freya022 May 31, 2025
48bff1b
Take List of arguments instead of Array
freya022 May 31, 2025
f50ae09
Remove SourceDirectories argument, replace RestartClassLoader by URLC…
freya022 May 31, 2025
8bb9f29
Don't read class data when making a snapshot
freya022 May 31, 2025
e03ffc8
Add services to self-manage this extension
freya022 May 31, 2025
df0098f
Update names
freya022 May 31, 2025
cc72423
Override ClassLoader used by ClassGraph after restart
freya022 May 31, 2025
8ef3fe6
Use BC snapshot
freya022 May 31, 2025
658a29e
Move to module
freya022 Jun 1, 2025
242fab9
Use Kotlin 2.2.0-RC
freya022 Jun 3, 2025
84d9eea
Add JDA cache agent, transform JDABuilder and JDAService with basic t…
freya022 Jun 3, 2025
aa622be
Clean up build.gradle.kts
freya022 Jun 3, 2025
25e3b4b
Start adding builder caching, getting a cache key
freya022 Jun 3, 2025
f528397
Hook JDABuilder#build to return a cached instance
freya022 Jun 4, 2025
7bced2a
Add notes
freya022 Jun 4, 2025
b32a5c5
Hook JDAImpl#shutdown() to store the shutdown code
freya022 Jun 4, 2025
40105b5
Move ClassFileTransformer(s) to package, use naming scheme of the JDK…
freya022 Jun 4, 2025
8a39798
Refactor out JDABuilder configuration, store more data
freya022 Jun 4, 2025
1b398f0
Use multi-dollar interpolation
freya022 Jun 5, 2025
0ad8e21
Consider the method name when matching JDABuilder setter hooks
freya022 Jun 5, 2025
0edc619
Replace the event manager after JDABuilder#build with ours
freya022 Jun 5, 2025
0a4ea24
Implement JDABuilderConfiguration#isSameAs
freya022 Jun 5, 2025
f88156b
Support JDABuilder#setEventPassthrough
freya022 Jun 5, 2025
b4dfc99
Send StatusChangeEvent, GuildReadyEvent, ReadyEvent after reusing an …
freya022 Jun 5, 2025
1c7fb02
Fix test
freya022 Jun 5, 2025
cdf53c3
Small changes
freya022 Jun 5, 2025
73a2771
Use RestartClassLoader as a specific type of URLClassLoader
freya022 Jun 5, 2025
dd1e6ef
Log unsupported JDABuilder methods
freya022 Jun 5, 2025
99bfd76
Ignore JDABuilder#build()
freya022 Jun 5, 2025
d55e004
Immediately return in JDA#awaitShutdown
freya022 Jun 5, 2025
0cb83b8
Support more JDABuilder methods
freya022 Jun 5, 2025
7c6c675
Set artifact IDs for publishing
freya022 Jun 5, 2025
336c2e6
JDA cache: Remove dependency on base restarter
freya022 Jun 6, 2025
6052336
Restarter: Remove RestarterClassGraphConfigurer
freya022 Jun 6, 2025
ceea55d
Restarter: Shutdown immediately, await using configured timeout
freya022 Jun 6, 2025
e81f9d0
JDA cache: Immediately run JDA#shutdown if the JVM is shutting down
freya022 Jun 6, 2025
24c08e2
JDA cache: Redirect JDA#shutdownNow to JDABuilderSession#onShutdown
freya022 Jun 6, 2025
9f2c3cb
Restarter: Replace SourceDirectories + ClasspathListener with Classpa…
freya022 Jun 6, 2025
b01e971
JDA cache: fix test
freya022 Jun 6, 2025
73917d1
JDA cache: Store cache key in JDAImpl instead of the full instance
freya022 Jun 6, 2025
c51024b
JDA cache: Rewrite shutdown() calls in JDAImpl#shutdownNow() to doShu…
freya022 Jun 6, 2025
8a6b7cd
JDA cache: Rename transformer classes of constructors
freya022 Jun 6, 2025
2d45357
JDA cache: Defer BContextImpl#scheduleShutdownSignal
freya022 Jun 6, 2025
da1e9af
JDA cache: Clean up JDACache
freya022 Jun 6, 2025
8962737
JDA cache: Remove note
freya022 Jun 6, 2025
6835e5c
JDA cache: When JDA shuts down, check JVM status first, don't run sch…
freya022 Jun 6, 2025
6c20eea
JDA cache: Shutdown if the configuration has unsupported values
freya022 Jun 6, 2025
268b29e
JDA cache: Consider configurations not equal if one of them has unsup…
freya022 Jun 6, 2025
b870e79
JDA cache: Support JDABuilder#setEnableShutdownHook
freya022 Jun 6, 2025
feb3ec0
Add test libs to convention plugin
freya022 Jun 7, 2025
53f5730
JDA cache: Add utility method to create DynamicCallSiteDesc
freya022 Jun 7, 2025
596ec4a
JDA cache: Move all class descriptors to a file
freya022 Jun 7, 2025
741423c
JDA cache: Move CodeBuilderTest to package, use common class descriptors
freya022 Jun 7, 2025
e1de668
JDA cache: Remove print
freya022 Jun 7, 2025
870489e
JDA cache: Refactor transformers
freya022 Jun 7, 2025
214e873
JDA cache: Move utils and tests to package
freya022 Jun 7, 2025
c4e8c55
JDA cache: Small reformat
freya022 Jun 7, 2025
c70b2d4
Restarter: Check for InvocationTargetException when catching Immediat…
freya022 Jun 12, 2025
85237c7
Restarter: Override ClassLoader methods
freya022 Jun 12, 2025
cf5080f
JDA cache: Don't use delegate event handler in critical section
freya022 Jun 14, 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
32 changes: 0 additions & 32 deletions build.gradle.kts

This file was deleted.

13 changes: 13 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
plugins {
`kotlin-dsl`
}

repositories {
mavenCentral()
gradlePluginPortal()
}

dependencies {
// Change in version catalog too
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.0-RC")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
kotlin("jvm")
}

group = "dev.freya02"
version = "3.0.0-beta.2_DEV"

java {
toolchain {
languageVersion = JavaLanguageVersion.of(24)
}

sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21

withSourcesJar()
}

repositories {
mavenCentral()
mavenLocal()
}

dependencies {
testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.0")
}

kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_21

freeCompilerArgs.addAll("-Xjsr305=strict")
}
}
16 changes: 16 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[versions]
kotlin = "2.2.0-RC" # Also change in buildSrc
botcommands = "3.0.0-beta.2_DEV"
jda = "5.5.1"
mockk = "1.13.16"
bytebuddy = "1.17.5"
logback-classic = "1.5.18"
kotlin-logging = "7.0.3"

[libraries]
botcommands = { module = "io.github.freya022:BotCommands", version.ref = "botcommands" }
jda = { module = "net.dv8tion:JDA", version.ref = "jda" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
bytebuddy = { module = "net.bytebuddy:byte-buddy", version.ref = "bytebuddy" }
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback-classic" }
kotlin-logging = { module = "io.github.oshai:kotlin-logging-jvm", version.ref = "kotlin-logging" }
55 changes: 55 additions & 0 deletions restarter-jda-cache/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
id("BotCommands-Restarter-conventions")
`maven-publish`
}

repositories {
maven("https://jitpack.io")
}

dependencies {
implementation(libs.kotlin.logging)
implementation(libs.botcommands)

testImplementation(libs.mockk)
testImplementation(libs.bytebuddy)
testImplementation(libs.logback.classic)
}

java {
sourceCompatibility = JavaVersion.VERSION_24
targetCompatibility = JavaVersion.VERSION_24
}

kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_24
freeCompilerArgs.add("-Xcontext-parameters")
}
}

val jar by tasks.getting(Jar::class) {
manifest {
attributes(
"Premain-Class" to "dev.freya02.botcommands.restart.jda.cache.Agent",
)
}
}

tasks.withType<Test> {
useJUnitPlatform()

jvmArgs("-javaagent:${jar.archiveFile.get().asFile.absolutePath}")
}

publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])

artifactId = "BotCommands-Restarter-JDA-Cache"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dev.freya02.botcommands.restart.jda.cache

import dev.freya02.botcommands.restart.jda.cache.transformer.BContextImplTransformer
import dev.freya02.botcommands.restart.jda.cache.transformer.JDABuilderTransformer
import dev.freya02.botcommands.restart.jda.cache.transformer.JDAImplTransformer
import dev.freya02.botcommands.restart.jda.cache.transformer.JDAServiceTransformer
import java.lang.instrument.Instrumentation

object Agent {

@JvmStatic
fun premain(agentArgs: String?, inst: Instrumentation) {
inst.addTransformer(JDABuilderTransformer)
inst.addTransformer(JDAServiceTransformer)
inst.addTransformer(BContextImplTransformer)
inst.addTransformer(JDAImplTransformer)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package dev.freya02.botcommands.restart.jda.cache

import net.dv8tion.jda.api.events.GenericEvent
import net.dv8tion.jda.api.hooks.IEventManager
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

internal class BufferingEventManager @DynamicCall constructor(
delegate: IEventManager,
) : IEventManager {

private val lock = ReentrantLock()
private val eventBuffer: MutableList<GenericEvent> = arrayListOf()

private var delegate: IEventManager? = delegate

internal fun setDelegate(delegate: IEventManager) {
lock.withLock {
check(delegate !is BufferingEventManager) {
"Tried to delegate to a BufferingEventManager!"
}

this.delegate = delegate
eventBuffer.forEach(::handle)
}
}

internal fun detach() {
lock.withLock {
delegate = null
}
}

override fun register(listener: Any) {
lock.withLock {
val delegate = delegate ?: error("Should not happen, implement a listener queue if necessary")
delegate.register(listener)
}
}

override fun unregister(listener: Any) {
lock.withLock {
val delegate = delegate ?: error("Should not happen, implement a listener queue if necessary")
delegate.unregister(listener)
}
}

override fun handle(event: GenericEvent) {
val delegate = lock.withLock {
val delegate = delegate
if (delegate == null) {
eventBuffer += event
return
}
delegate
}

delegate.handle(event)
}

override fun getRegisteredListeners(): List<Any?> {
lock.withLock {
val delegate = delegate ?: error("Should not happen, implement a listener queue if necessary")
return delegate.registeredListeners
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dev.freya02.botcommands.restart.jda.cache

/**
* This member is used by generated code and as such is not directly referenced.
*
* This member must be `public`.
*/
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
internal annotation class DynamicCall
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package dev.freya02.botcommands.restart.jda.cache

import io.github.freya022.botcommands.api.core.utils.enumSetOf
import io.github.freya022.botcommands.api.core.utils.enumSetOfAll
import io.github.oshai.kotlinlogging.KotlinLogging
import net.dv8tion.jda.api.OnlineStatus
import net.dv8tion.jda.api.entities.Activity
import net.dv8tion.jda.api.hooks.IEventManager
import net.dv8tion.jda.api.hooks.InterfacedEventManager
import net.dv8tion.jda.api.utils.ChunkingFilter
import net.dv8tion.jda.api.utils.MemberCachePolicy
import net.dv8tion.jda.api.utils.cache.CacheFlag
import java.util.*

private val logger = KotlinLogging.logger { }

class JDABuilderConfiguration internal constructor() {

private val warnedUnsupportedValues: MutableSet<String> = hashSetOf()

var hasUnsupportedValues = false
private set

private val builderValues: MutableMap<ValueType, Any?> = hashMapOf()
private var _eventManager: IEventManager? = null
val eventManager: IEventManager get() = _eventManager ?: InterfacedEventManager()

// So we can track the initial token and intents, the constructor will be instrumented and call this method
// The user overriding the values using token/intent setters should not be an issue
@DynamicCall
fun onInit(token: String?, intents: Int) {
builderValues[ValueType.TOKEN] = token
builderValues[ValueType.INTENTS] = intents
builderValues[ValueType.CACHE_FLAGS] = enumSetOfAll<CacheFlag>()
}

@DynamicCall
fun markUnsupportedValue(signature: String) {
if (warnedUnsupportedValues.add(signature))
logger.warn { "Unsupported JDABuilder method '$signature', JDA will not be cached between restarts" }
hasUnsupportedValues = true
}

@DynamicCall
fun setStatus(status: OnlineStatus) {
builderValues[ValueType.STATUS] = status
}

@DynamicCall
fun setEventManager(eventManager: IEventManager?) {
_eventManager = eventManager
}

@DynamicCall
fun setEventPassthrough(enable: Boolean) {
builderValues[ValueType.EVENT_PASSTHROUGH] = enable
}

@DynamicCall
@Suppress("UNCHECKED_CAST")
fun enableCache(first: CacheFlag, vararg others: CacheFlag) {
(builderValues[ValueType.CACHE_FLAGS] as EnumSet<CacheFlag>) += enumSetOf(first, *others)
}

@DynamicCall
@Suppress("UNCHECKED_CAST")
fun enableCache(flags: Collection<CacheFlag>) {
(builderValues[ValueType.CACHE_FLAGS] as EnumSet<CacheFlag>) += flags
}

@DynamicCall
@Suppress("UNCHECKED_CAST")
fun disableCache(first: CacheFlag, vararg others: CacheFlag) {
(builderValues[ValueType.CACHE_FLAGS] as EnumSet<CacheFlag>) -= enumSetOf(first, *others)
}

@DynamicCall
@Suppress("UNCHECKED_CAST")
fun disableCache(flags: Collection<CacheFlag>) {
(builderValues[ValueType.CACHE_FLAGS] as EnumSet<CacheFlag>) -= flags
}

@DynamicCall
fun setMemberCachePolicy(memberCachePolicy: MemberCachePolicy?) {
builderValues[ValueType.MEMBER_CACHE_POLICY] = memberCachePolicy
}

@DynamicCall
fun setChunkingFilter(filter: ChunkingFilter?) {
builderValues[ValueType.CHUNKING_FILTER] = filter
}

@DynamicCall
fun setLargeThreshold(threshold: Int) {
builderValues[ValueType.LARGE_THRESHOLD] = threshold
}

@DynamicCall
fun setActivity(activity: Activity?) {
builderValues[ValueType.ACTIVITY] = activity
}

@DynamicCall
fun setEnableShutdownHook(enable: Boolean) {
builderValues[ValueType.ENABLE_SHUTDOWN_HOOK] = enable
}

internal infix fun isSameAs(other: JDABuilderConfiguration): Boolean {
if (hasUnsupportedValues) return false
if (other.hasUnsupportedValues) return false

return builderValues == other.builderValues
}

private enum class ValueType {
TOKEN,
INTENTS,
STATUS,
EVENT_PASSTHROUGH,
CACHE_FLAGS,
// These two are interfaces, it's fine to compare them by equality,
// their reference will be the same as they are from the app class loader,
// so if two runs uses MemberCachePolicy#VOICE, it'll still be compatible
MEMBER_CACHE_POLICY,
CHUNKING_FILTER,
LARGE_THRESHOLD,
ACTIVITY,
ENABLE_SHUTDOWN_HOOK,
}
}
Loading