Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.eternalcode.core.feature.msg;

import com.eternalcode.core.feature.msg.toggle.MsgState;
import com.eternalcode.core.feature.msg.toggle.MsgToggleRepository;
import com.eternalcode.core.injector.annotations.Inject;
import com.eternalcode.core.injector.annotations.component.Controller;
import com.eternalcode.core.placeholder.PlaceholderRegistry;
import com.eternalcode.core.placeholder.PlaceholderReplacer;
import com.eternalcode.core.placeholder.cache.AsyncPlaceholderCacheRegistry;
import com.eternalcode.core.placeholder.cache.AsyncPlaceholderCached;
import com.eternalcode.core.publish.Subscribe;
import com.eternalcode.core.publish.event.EternalInitializeEvent;
import com.eternalcode.core.translation.Translation;
import com.eternalcode.core.translation.TranslationManager;
import java.time.Duration;
import java.util.UUID;

@Controller
public class MsgPlaceholderSetup {

public static final String MSG_STATE_CACHE_KEY = "msg_state";

private final MsgService msgService;
private final MsgToggleRepository msgToggleRepository;
private final TranslationManager translationManager;
private final AsyncPlaceholderCacheRegistry cacheRegistry;

@Inject
MsgPlaceholderSetup(
MsgService msgService,
MsgToggleRepository msgToggleRepository,
TranslationManager translationManager,
AsyncPlaceholderCacheRegistry cacheRegistry
) {
this.msgService = msgService;
this.msgToggleRepository = msgToggleRepository;
this.translationManager = translationManager;
this.cacheRegistry = cacheRegistry;
}

@Subscribe(EternalInitializeEvent.class)
void setUpPlaceholders(PlaceholderRegistry placeholderRegistry) {
Translation translation = this.translationManager.getMessages();

AsyncPlaceholderCached<MsgState> stateCache = this.cacheRegistry.register(
MSG_STATE_CACHE_KEY,
this.msgToggleRepository::getPrivateChatState,
Duration.ofMinutes(10)
);

placeholderRegistry.registerPlaceholder(PlaceholderReplacer.of(
"socialspy_status",
player -> String.valueOf(this.msgService.isSpy(player.getUniqueId()))
));

placeholderRegistry.registerPlaceholder(PlaceholderReplacer.of(
"socialspy_status_formatted",
player -> {
UUID uuid = player.getUniqueId();
return this.msgService.isSpy(uuid)
? translation.msg().placeholders().socialSpyEnabled()
: translation.msg().placeholders().socialSpyDisabled();
}
));

placeholderRegistry.registerPlaceholder(PlaceholderReplacer.of(
"msg_status",
player -> {
UUID uuid = player.getUniqueId();
MsgState state = stateCache.getCached(uuid);

if (state == null) {
return translation.msg().placeholders().loading();
}

return state.name().toLowerCase();
}
));

placeholderRegistry.registerPlaceholder(PlaceholderReplacer.of(
"msg_status_formatted",
player -> {
UUID uuid = player.getUniqueId();
MsgState state = stateCache.getCached(uuid);

if (state == null) {
return translation.msg().placeholders().loading();
}

return state == MsgState.ENABLED
? translation.msg().placeholders().msgEnabled()
: translation.msg().placeholders().msgDisabled();
}
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,22 @@ public class ENMsgMessages extends OkaeriConfig implements MsgMessages {
public Notice socialSpyEnable = Notice.chat("<green>► <white>SocialSpy has been {STATE}<white>!");
public Notice socialSpyDisable = Notice.chat("<red>► <white>SocialSpy has been {STATE}<white>!");

@Comment("# Formatowanie placeholderów")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is EN translation, so we use English comments

Suggested change
@Comment("# Formatowanie placeholderów")
@Comment("# Placeholders formatting")

public ENPlaceholders placeholders = new ENPlaceholders();

@Getter
@Accessors(fluent = true)
public static class ENPlaceholders extends OkaeriConfig implements MsgMessages.Placeholders {
private String loading = "<yellow>Loading...";

private String msgEnabled = "<green>Enabled";
private String msgDisabled = "<red>Disabled";
private String socialSpyEnabled = "<green>Enabled";
private String socialSpyDisabled = "<red>Disabled";
}

public ENPlaceholders placeholders() {
return this.placeholders;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,16 @@ public interface MsgMessages {
Notice otherMessagesDisabled();
Notice otherMessagesEnabled();

Placeholders placeholders();

interface Placeholders {
String loading();

String msgEnabled();
String msgDisabled();

String socialSpyEnabled();
String socialSpyDisabled();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,21 @@ public class PLMsgMessages extends OkaeriConfig implements MsgMessages {
public Notice otherMessagesDisabled = Notice.chat("<green>► <white>Wiadomości prywatne zostały <red>wyłączone <white>dla gracza <green>{PLAYER}<white>!");
public Notice otherMessagesEnabled = Notice.chat("<green>► <white>Wiadomości prywatne zostały <green>włączone <white>dla gracza <green>{PLAYER}<white>!");

@Comment("# Formatowanie placeholderów")
public PLPlaceholders placeholders = new PLPlaceholders();

@Getter
@Accessors(fluent = true)
public static class PLPlaceholders extends OkaeriConfig implements MsgMessages.Placeholders {
private String loading = "<yellow>Ładowanie...";

private String msgEnabled = "<green>Włączone";
private String msgDisabled = "<red>Wyłączone";
private String socialSpyEnabled = "<green>Włączony";
private String socialSpyDisabled = "<red>Wyłączony";
}

public PLPlaceholders placeholders() {
return this.placeholders;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import java.util.UUID;
import java.util.concurrent.CompletableFuture;

interface MsgToggleRepository {
public interface MsgToggleRepository {

CompletableFuture<MsgState> getPrivateChatState(UUID uuid);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,40 @@
package com.eternalcode.core.feature.msg.toggle;

import com.eternalcode.core.feature.msg.MsgPlaceholderSetup;
import com.eternalcode.core.injector.annotations.Inject;
import com.eternalcode.core.injector.annotations.component.Service;
import com.eternalcode.core.placeholder.cache.AsyncPlaceholderCacheRegistry;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;

@Service
class MsgToggleServiceImpl implements MsgToggleService {

private final MsgToggleRepository msgToggleRepository;
private final ConcurrentHashMap<UUID, MsgState> cachedToggleStates;
private final AsyncPlaceholderCacheRegistry cacheRegistry;

@Inject
MsgToggleServiceImpl(MsgToggleRepository msgToggleRepository) {
this.cachedToggleStates = new ConcurrentHashMap<>();
MsgToggleServiceImpl(MsgToggleRepository msgToggleRepository, AsyncPlaceholderCacheRegistry cacheRegistry) {
this.msgToggleRepository = msgToggleRepository;

this.cacheRegistry = cacheRegistry;
}


@Override
public CompletableFuture<MsgState> getState(UUID playerUniqueId) {
if (this.cachedToggleStates.containsKey(playerUniqueId)) {
return CompletableFuture.completedFuture(this.cachedToggleStates.get(playerUniqueId));
}

return this.msgToggleRepository.getPrivateChatState(playerUniqueId);
return this.msgToggleRepository.getPrivateChatState(playerUniqueId)
.thenApply(state -> {
this.updateCache(playerUniqueId, state);
return state;
});
}

@Override
public CompletableFuture<Void> setState(UUID playerUniqueId, MsgState state) {
this.cachedToggleStates.put(playerUniqueId, state);
this.updateCache(playerUniqueId, state);

return this.msgToggleRepository.setPrivateChatState(playerUniqueId, state)
.exceptionally(throwable -> {
this.cachedToggleStates.remove(playerUniqueId);
this.invalidateCache(playerUniqueId);
return null;
});
}
Comment on lines 23 to 32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The previous implementation of this service used a ConcurrentHashMap (cachedToggleStates) to cache the toggle states of players, avoiding repeated database lookups. This refactoring has removed the cache, meaning that every call to getState() will now result in a database query via msgToggleRepository.getPrivateChatState(). This is a significant performance regression, especially if getState() is called frequently (e.g., by toggleState).

The new PlaceholderWatcher system updates placeholder caches, but it does not provide a caching layer for the service itself. It's recommended to reintroduce a caching mechanism in this service, similar to the previous implementation. The PlaceholderWatcher can then be used to update or invalidate this service-level cache as well.

Expand All @@ -48,4 +47,14 @@ public CompletableFuture<MsgState> toggleState(UUID playerUniqueId) {
.thenApply(aVoid -> newState);
});
}

private void updateCache(UUID uuid, MsgState state) {
this.cacheRegistry.<MsgState>get(MsgPlaceholderSetup.MSG_STATE_CACHE_KEY)
.ifPresent(cache -> cache.update(uuid, state));
}

private void invalidateCache(UUID uuid) {
this.cacheRegistry.<MsgState>get(MsgPlaceholderSetup.MSG_STATE_CACHE_KEY)
.ifPresent(cache -> cache.invalidate(uuid));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.eternalcode.core.placeholder.cache;

import com.eternalcode.core.injector.annotations.Inject;
import com.eternalcode.core.injector.annotations.component.Controller;
import com.eternalcode.core.publish.Subscribe;
import com.eternalcode.core.publish.event.EternalReloadEvent;
import com.eternalcode.core.publish.event.EternalShutdownEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;

@Controller
class AsyncPlaceholderCacheController implements Listener {

private final AsyncPlaceholderCacheRegistry cacheRegistry;

@Inject
AsyncPlaceholderCacheController(AsyncPlaceholderCacheRegistry cacheRegistry) {
this.cacheRegistry = cacheRegistry;
}

@EventHandler(priority = EventPriority.MONITOR)
void onPlayerQuit(PlayerQuitEvent event) {
this.cacheRegistry.invalidatePlayer(event.getPlayer().getUniqueId());
}

@Subscribe
void onDisable(EternalShutdownEvent event) {
this.cacheRegistry.invalidateAll();
}

@Subscribe
void onReload(EternalReloadEvent event) {
this.cacheRegistry.invalidateAll();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.eternalcode.core.placeholder.cache;

import com.eternalcode.core.injector.annotations.Inject;
import com.eternalcode.core.injector.annotations.component.Service;
import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

@Service
public class AsyncPlaceholderCacheRegistry {

private static final Duration DEFAULT_EXPIRE_DURATION = Duration.ofMinutes(30);

private final Map<String, AsyncPlaceholderCached<?>> caches = new ConcurrentHashMap<>();

@Inject
public AsyncPlaceholderCacheRegistry() {
}

public <T> AsyncPlaceholderCached<T> register(String key, Function<UUID, CompletableFuture<T>> loader) {
return this.register(key, loader, DEFAULT_EXPIRE_DURATION);
}

public <T> AsyncPlaceholderCached<T> register(String key, Function<UUID, CompletableFuture<T>> loader, Duration expireAfterWrite) {
AsyncPlaceholderCached<T> cache = new AsyncPlaceholderCached<>(loader, expireAfterWrite);
this.caches.put(key, cache);
return cache;
}

@SuppressWarnings("unchecked")
public <T> Optional<AsyncPlaceholderCached<T>> get(String key) {
return Optional.ofNullable((AsyncPlaceholderCached<T>) this.caches.get(key));
}

public void invalidatePlayer(UUID uuid) {
this.caches.values().forEach(cache -> cache.invalidate(uuid));
}

public void invalidateAll() {
this.caches.values().forEach(AsyncPlaceholderCached::clear);
}

public void unregister(String key) {
AsyncPlaceholderCached<?> cache = this.caches.remove(key);
if (cache != null) {
cache.clear();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.eternalcode.core.placeholder.cache;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;

public class AsyncPlaceholderCached<T> {

private final Cache<UUID, T> cache;
private final Function<UUID, CompletableFuture<T>> loader;

public AsyncPlaceholderCached(Function<UUID, CompletableFuture<T>> loader, Duration expireAfterWrite) {
this.loader = loader;
this.cache = CacheBuilder.newBuilder()
.expireAfterWrite(expireAfterWrite)
.build();
}

public T getCached(UUID uuid) {
T cached = this.cache.getIfPresent(uuid);

if (cached != null) {
return cached;
}

this.loader.apply(uuid).thenAccept(value ->
this.cache.put(uuid, value)
);

return null;
}

public void update(UUID uuid, T value) {
this.cache.put(uuid, value);
}

public void invalidate(UUID uuid) {
this.cache.invalidate(uuid);
}

public void clear() {
this.cache.invalidateAll();
}

public boolean contains(UUID uuid) {
return this.cache.getIfPresent(uuid) != null;
}
}
6 changes: 5 additions & 1 deletion eternalcore-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ dependencies {
tasks {
runServer {
minecraftVersion("1.21.8")
downloadPlugins.modrinth("luckperms", "v${Versions.LUCKPERMS}-bukkit")

downloadPlugins {
modrinth("luckperms", "v${Versions.LUCKPERMS}-bukkit")
modrinth("placeholderapi", Versions.PLACEHOLDER_API)
}
}
}