Skip to content

Commit 4e9372b

Browse files
committed
native-image compilation, disable member cache
1 parent 173044b commit 4e9372b

29 files changed

+601
-4838
lines changed

.github/workflows/build.yml

+6-6
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ jobs:
2727
if: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' }}
2828
steps:
2929
- uses: actions/checkout@v4
30-
- name: Set up JDK 17
31-
uses: actions/setup-java@v4
30+
- name: Set up JDK 21
31+
uses: graalvm/setup-graalvm@v1
3232
with:
33-
java-version: '17'
34-
distribution: 'temurin'
35-
- name: Build JAR
36-
run: ./gradlew shadowJar
33+
java-version: '21'
34+
distribution: 'graalvm-community'
35+
- name: Build native-image
36+
run: ./gradlew nativeCompile -Pprod
3737
- name: Build Docker image
3838
run: docker build -t javabot .
3939
- name: Tag docker image

Dockerfile

+15-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1-
FROM eclipse-temurin:21-jre
2-
RUN mkdir /work
3-
COPY build/libs/JavaBot-1.0.0-SNAPSHOT-all.jar /work/bot.jar
1+
FROM alpine:latest
2+
RUN apk add --no-cache libsm libxrender libxext libxtst libxi gcompat ttf-dejavu
3+
4+
COPY build/native/nativeCompile /work
45
WORKDIR /work
6+
57
RUN chown 1000:1000 /work
8+
USER 1000
9+
ENV HOME=/work
10+
11+
# https://github.com/openjdk/jdk/pull/20169
12+
# need to create fake JAVA_HOME
13+
RUN mkdir -p /tmp/JAVA_HOME/conf/fonts
14+
RUN mkdir /tmp/JAVA_HOME/lib
15+
16+
617
VOLUME "/work/config"
718
VOLUME "/work/logs"
819
VOLUME "/work/db"
920
VOLUME "/work/purgeArchives"
10-
USER 1000
11-
ENTRYPOINT [ "java", "-jar", "bot.jar" ]
21+
ENTRYPOINT [ "./javabot", "-Djava.home=/tmp/JAVA_HOME" ]

build.gradle.kts

+30-3
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.*
44
plugins {
55
java
66
id("com.github.johnrengelman.shadow") version "7.1.2"
7-
id("org.springframework.boot") version "3.2.0"
8-
id("io.spring.dependency-management") version "1.0.15.RELEASE"
7+
id("org.springframework.boot") version "3.3.5"
8+
id("io.spring.dependency-management") version "1.1.6"
9+
id("org.graalvm.buildtools.native") version "0.10.3"
910
checkstyle
1011
}
1112

@@ -64,6 +65,9 @@ dependencies {
6465
// Spring
6566
implementation("org.springframework.boot:spring-boot-starter-web")
6667
implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
68+
69+
//required for registering native hints
70+
implementation("org.jetbrains.kotlin:kotlin-reflect")
6771
}
6872

6973
configurations {
@@ -101,4 +105,27 @@ tasks.withType<ShadowJar> {
101105
checkstyle {
102106
toolVersion = "9.1"
103107
configDirectory.set(File("checkstyle"))
104-
}
108+
}
109+
110+
tasks.withType<Checkstyle>() {
111+
exclude("**/generated/**")
112+
}
113+
114+
tasks.checkstyleAot {
115+
isEnabled = false
116+
}
117+
tasks.processTestAot {
118+
isEnabled = false
119+
}
120+
121+
graalvmNative {
122+
binaries {
123+
named("main") {
124+
if (hasProperty("prod")) {
125+
buildArgs.add("-O3")
126+
} else {
127+
quickBuild.set(true)
128+
}
129+
}
130+
}
131+
}

settings.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
rootProject.name = "JavaBot"
1+
rootProject.name = "javabot"
22

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package net.discordjug.javabot;
2+
3+
import java.nio.channels.Channel;
4+
5+
import club.minnced.discord.webhook.send.WebhookEmbed;
6+
import net.discordjug.javabot.data.config.BotConfig;
7+
import net.discordjug.javabot.data.config.GuildConfig;
8+
import net.discordjug.javabot.data.config.GuildConfigItem;
9+
import net.discordjug.javabot.data.config.SystemsConfig;
10+
import net.discordjug.javabot.data.config.guild.HelpConfig;
11+
import net.discordjug.javabot.data.config.guild.MessageCacheConfig;
12+
import net.discordjug.javabot.data.config.guild.MetricsConfig;
13+
import net.discordjug.javabot.data.config.guild.ModerationConfig;
14+
import net.discordjug.javabot.data.config.guild.QOTWConfig;
15+
import net.discordjug.javabot.data.config.guild.ServerLockConfig;
16+
import net.discordjug.javabot.data.config.guild.StarboardConfig;
17+
import net.dv8tion.jda.api.entities.Guild;
18+
import net.dv8tion.jda.api.entities.Member;
19+
import net.dv8tion.jda.api.entities.Role;
20+
import net.dv8tion.jda.api.entities.ScheduledEvent;
21+
import net.dv8tion.jda.api.entities.ThreadMember;
22+
import net.dv8tion.jda.api.entities.User;
23+
import net.dv8tion.jda.api.entities.channel.forums.ForumTag;
24+
import net.dv8tion.jda.api.entities.emoji.RichCustomEmoji;
25+
import net.dv8tion.jda.api.entities.sticker.GuildSticker;
26+
import net.dv8tion.jda.api.hooks.ListenerAdapter;
27+
import net.dv8tion.jda.api.managers.AudioManager;
28+
import net.dv8tion.jda.internal.entities.MemberPresenceImpl;
29+
import net.dv8tion.jda.internal.requests.restaction.PermOverrideData;
30+
import org.h2.server.TcpServer;
31+
import org.springframework.aot.hint.MemberCategory;
32+
import org.springframework.aot.hint.RuntimeHints;
33+
import org.springframework.aot.hint.RuntimeHintsRegistrar;
34+
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
35+
import org.springframework.core.io.ClassPathResource;
36+
37+
/**
38+
* Configure classes and resources to be accessible from native-image.
39+
*/
40+
@RegisterReflectionForBinding({
41+
//register config classes for reflection
42+
BotConfig.class, GuildConfig.class, GuildConfigItem.class,SystemsConfig.class,
43+
HelpConfig.class, MessageCacheConfig.class, MetricsConfig.class, ModerationConfig.class, QOTWConfig.class, ServerLockConfig.class, StarboardConfig.class,
44+
45+
//ensure JDA can create necessary caches
46+
User[].class, Guild[].class, Member[].class, Role[].class, Channel[].class, AudioManager[].class, ScheduledEvent[].class, ThreadMember[].class, ForumTag[].class, RichCustomEmoji[].class, GuildSticker[].class, MemberPresenceImpl[].class,
47+
//needs to be serialized for channel managers etc
48+
PermOverrideData.class,
49+
//ensure that webhook embed authors can be serialized
50+
WebhookEmbed.EmbedAuthor.class
51+
})
52+
public class RuntimeHintsConfiguration implements RuntimeHintsRegistrar {
53+
54+
@Override
55+
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
56+
57+
//ensure resources are available in native-image
58+
hints.resources().registerPattern("assets/**");
59+
hints.resources().registerPattern("database/**");
60+
hints.resources().registerPattern("help_guidelines/**");
61+
hints.resources().registerPattern("help_overview/**");
62+
hints.resources().registerResource(new ClassPathResource("quartz.properties"));
63+
64+
//allow H2 to create the TCP server (necessary for starting the DB)
65+
hints.reflection().registerType(TcpServer.class, MemberCategory.INVOKE_PUBLIC_METHODS);
66+
67+
// JDA needs to be able to access listener methods
68+
hints.reflection().registerType(ListenerAdapter.class, MemberCategory.INVOKE_PUBLIC_METHODS);
69+
70+
// caffeine
71+
hints.reflection().registerTypeIfPresent(getClass().getClassLoader(), "com.github.benmanes.caffeine.cache.SSW", MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
72+
}
73+
}

src/main/java/net/discordjug/javabot/SpringConfig.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import org.springframework.context.ApplicationContext;
1111
import org.springframework.context.annotation.Bean;
1212
import org.springframework.context.annotation.Configuration;
13-
13+
import org.springframework.context.annotation.ImportRuntimeHints;
1414
import xyz.dynxsty.dih4jda.DIH4JDA;
1515
import xyz.dynxsty.dih4jda.DIH4JDABuilder;
1616
import xyz.dynxsty.dih4jda.exceptions.DIH4JDAException;
@@ -34,6 +34,7 @@
3434
* This class holds all configuration settings and {@link Bean}s.
3535
*/
3636
@Configuration
37+
@ImportRuntimeHints(RuntimeHintsConfiguration.class)
3738
@RequiredArgsConstructor
3839
public class SpringConfig {
3940
@Bean
@@ -71,7 +72,7 @@ JDA jda(BotConfig botConfig, ApplicationContext ctx) {
7172
return JDABuilder.createDefault(botConfig.getSystems().getJdaBotToken())
7273
.setStatus(OnlineStatus.DO_NOT_DISTURB)
7374
.setChunkingFilter(ChunkingFilter.ALL)
74-
.setMemberCachePolicy(MemberCachePolicy.ALL)
75+
.setMemberCachePolicy(MemberCachePolicy.NONE)
7576
.enableCache(CacheFlag.ACTIVITY)
7677
.enableIntents(GatewayIntent.GUILD_MEMBERS, GatewayIntent.GUILD_PRESENCES, GatewayIntent.MESSAGE_CONTENT)
7778
.addEventListeners(listeners.toArray())

src/main/java/net/discordjug/javabot/api/TomcatConfig.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import org.springframework.context.annotation.Bean;
88
import org.springframework.context.annotation.Configuration;
99

10+
import java.net.InetAddress;
11+
1012
import net.discordjug.javabot.data.config.SystemsConfig;
1113

1214

@@ -18,6 +20,7 @@
1820
public class TomcatConfig {
1921

2022
private final int ajpPort;
23+
private final InetAddress ajpAddress;
2124
private final boolean tomcatAjpEnabled;
2225
private final SystemsConfig systemsConfig;
2326

@@ -26,11 +29,13 @@ public class TomcatConfig {
2629
* @param ajpPort The port to run AJP under
2730
* @param tomcatAjpEnabled <code>true</code> if AJP is enabled, else <code>false</code>
2831
* @param systemsConfig an object representing the configuration of various systems
32+
* @param ajpAddress the listen address for AJP
2933
*/
30-
public TomcatConfig(@Value("${tomcat.ajp.port}") int ajpPort, @Value("${tomcat.ajp.enabled}") boolean tomcatAjpEnabled, SystemsConfig systemsConfig) {
34+
public TomcatConfig(@Value("${tomcat.ajp.port}") int ajpPort, @Value("${tomcat.ajp.enabled}") boolean tomcatAjpEnabled, @Value("${tomcat.ajp.address}") InetAddress ajpAddress, SystemsConfig systemsConfig) {
3135
this.ajpPort = ajpPort;
3236
this.tomcatAjpEnabled = tomcatAjpEnabled;
3337
this.systemsConfig = systemsConfig;
38+
this.ajpAddress = ajpAddress;
3439
}
3540

3641
/**
@@ -46,6 +51,7 @@ TomcatServletWebServerFactory servletContainer() {
4651
Connector ajpConnector = new Connector("org.apache.coyote.ajp.AjpNioProtocol");
4752
AjpNioProtocol protocol= (AjpNioProtocol) ajpConnector.getProtocolHandler();
4853
protocol.setSecret(systemsConfig.getApiConfig().getAjpSecret());
54+
protocol.setAddress(ajpAddress);
4955
ajpConnector.setPort(ajpPort);
5056
ajpConnector.setSecure(true);
5157
tomcat.addAdditionalTomcatConnectors(ajpConnector);

src/main/java/net/discordjug/javabot/api/routes/leaderboard/qotw/QOTWLeaderboardController.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@ public ResponseEntity<List<QOTWUserData>> getQOTWLeaderboard(
6969
if (members == null || members.isEmpty()) {
7070
List<QOTWAccount> topAccounts = pointsService.getTopAccounts(PAGE_AMOUNT, page);
7171
members = topAccounts.stream()
72-
.map(account -> new Pair<>(account, jda.retrieveUserById(account.getUserId()).complete()))
73-
.filter(pair -> guild.isMember(pair.second()))
74-
.map(pair -> createAPIAccount(pair.first(), pair.second(), topAccounts, page))
72+
.map(account -> new Pair<>(account, guild.retrieveMemberById(account.getUserId()).complete()))
73+
.filter(pair -> pair.second() != null)
74+
.map(pair -> createAPIAccount(pair.first(), pair.second().getUser(), topAccounts, page))
7575
.toList();
7676
getCache().put(new Pair<>(guild.getIdLong(), page), members);
7777
}

src/main/java/net/discordjug/javabot/listener/JobChannelCloseOldPostsListener.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import net.discordjug.javabot.data.config.guild.ModerationConfig;
1010
import net.discordjug.javabot.util.InteractionUtils;
1111
import net.dv8tion.jda.api.EmbedBuilder;
12+
import net.dv8tion.jda.api.entities.UserSnowflake;
1213
import net.dv8tion.jda.api.entities.channel.ChannelType;
1314
import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
1415
import net.dv8tion.jda.api.events.channel.ChannelCreateEvent;
@@ -53,7 +54,7 @@ public void onChannelCreate(ChannelCreateEvent event) {
5354
.setTitle("Post closed")
5455
.setDescription("This post has been blocked because you have created other recent posts.\nPlease do not spam posts.")
5556
.build())
56-
.setContent(post.getOwner().getAsMention())
57+
.setContent(UserSnowflake.fromId(post.getOwnerIdLong()).getAsMention())
5758
.flatMap(msg -> post.getManager().setArchived(true).setLocked(true))
5859
.queue();
5960
return;

src/main/java/net/discordjug/javabot/systems/help/HelpForumUpdater.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,8 @@ private void checkForumPost(@NotNull ThreadChannel post, HelpConfig config) {
8181
private void sendDMDormantInfoIfEnabled(ThreadChannel post, HelpConfig config) {
8282
if(Boolean.parseBoolean(preferenceService.getOrCreate(post.getOwnerIdLong(), Preference.PRIVATE_DORMANT_NOTIFICATIONS).getState())) {
8383
post
84-
.getOwner()
85-
.getUser()
86-
.openPrivateChannel()
84+
.getJDA()
85+
.openPrivateChannelById(post.getOwnerIdLong())
8786
.flatMap(c -> c.sendMessageEmbeds(createDMDormantInfo(post, config)))
8887
.queue();
8988
}

src/main/java/net/discordjug/javabot/systems/help/HelpListener.java

+4-5
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
2323
import net.dv8tion.jda.api.hooks.ListenerAdapter;
2424
import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback;
25-
import net.dv8tion.jda.api.interactions.components.ActionComponent;
2625
import net.dv8tion.jda.api.interactions.components.ActionRow;
2726
import net.dv8tion.jda.api.interactions.components.buttons.Button;
2827

@@ -254,7 +253,7 @@ private void handleHelpThanksInteraction(@NotNull ButtonInteractionEvent event,
254253
return;
255254
}
256255
switch (id[2]) {
257-
case "done" -> handleThanksCloseButton(event, manager, post);
256+
case "done" -> handleThanksCloseButton(event, manager, post, "");
258257
case "cancel" -> event.deferEdit().flatMap(h -> event.getMessage().delete()).queue();
259258
default -> {
260259
List<Button> thankButtons = event.getMessage()
@@ -266,15 +265,15 @@ private void handleHelpThanksInteraction(@NotNull ButtonInteractionEvent event,
266265
.toList();
267266
if (thankButtons.stream().filter(Button::isDisabled).count() ==
268267
thankButtons.size() - 1) {
269-
handleThanksCloseButton(event, manager, post);
268+
handleThanksCloseButton(event, manager, post, event.getButton().getId());
270269
} else {
271270
event.editButton(event.getButton().asDisabled()).queue();
272271
}
273272
}
274273
}
275274
}
276275

277-
private void handleThanksCloseButton(@NotNull ButtonInteractionEvent event, HelpManager manager, ThreadChannel post) {
276+
private void handleThanksCloseButton(@NotNull ButtonInteractionEvent event, HelpManager manager, ThreadChannel post, String additionalButtonId) {
278277
List<Button> buttons = event.getMessage().getButtons();
279278
// close post
280279
manager.close(event, false, null);
@@ -283,8 +282,8 @@ private void handleThanksCloseButton(@NotNull ButtonInteractionEvent event, Help
283282
experienceService.addMessageBasedHelpXP(post, true);
284283
// thank all helpers
285284
buttons.stream()
286-
.filter(ActionComponent::isDisabled)
287285
.filter(b -> b.getId() != null)
286+
.filter(b -> b.isDisabled() || (b.getId().equals(additionalButtonId)))
288287
.forEach(b -> manager.thankHelper(
289288
event.getGuild(),
290289
post,

src/main/java/net/discordjug/javabot/systems/help/HelpManager.java

+14-11
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,11 @@ public void close(IReplyCallback callback, boolean withHelpers, @Nullable String
117117
.queue(s -> postThread.getManager().setLocked(true).setArchived(true).queue());
118118
if (callback.getMember().getIdLong() != postThread.getOwnerIdLong() &&
119119
Boolean.parseBoolean(preferenceService.getOrCreate(postThread.getOwnerIdLong(), Preference.PRIVATE_CLOSE_NOTIFICATIONS).getState())) {
120-
121-
postThread.getOwner().getUser().openPrivateChannel()
122-
.flatMap(c -> createDMCloseInfoEmbed(callback.getMember(), postThread, reason, c))
123-
.queue(success -> {}, failure -> {});
120+
postThread
121+
.getJDA()
122+
.openPrivateChannelById(postThread.getOwnerIdLong())
123+
.flatMap(c -> createDMCloseInfoEmbed(callback.getMember(), postThread, reason, c))
124+
.queue(success -> {}, failure -> {});
124125

125126
botConfig.get(callback.getGuild())
126127
.getModerationConfig()
@@ -191,13 +192,15 @@ public void thankHelper(@NotNull Guild guild, ThreadChannel postThread, long hel
191192
service.performTransaction(helper.getIdLong(), config.getThankedExperience(), guild, postThread.getIdLong());
192193
} catch (SQLException e) {
193194
ExceptionLogger.capture(e, getClass().getSimpleName());
194-
botConfig.get(guild).getModerationConfig().getLogChannel().sendMessageFormat(
195-
"Could not record user %s thanking %s for help in post %s: %s",
196-
UserUtils.getUserTag(postThread.getOwner().getUser()),
197-
UserUtils.getUserTag(helper),
198-
postThread.getAsMention(),
199-
e.getMessage()
200-
).queue();
195+
guild.getJDA().retrieveUserById(postThread.getOwnerIdLong()).queue(owner -> {
196+
botConfig.get(guild).getModerationConfig().getLogChannel().sendMessageFormat(
197+
"Could not record user %s thanking %s for help in post %s: %s",
198+
UserUtils.getUserTag(owner),
199+
UserUtils.getUserTag(helper),
200+
postThread.getAsMention(),
201+
e.getMessage()
202+
).queue();
203+
});
201204
}
202205
});
203206
}

0 commit comments

Comments
 (0)