package fr.xephi.authme.datasource; import ch.jalu.datasourcecolumns.data.DataSourceValue; import ch.jalu.datasourcecolumns.data.DataSourceValueImpl; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.ThreadFactoryBuilder; import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.output.ConsoleLoggerFactory; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.properties.DatabaseSettings; import fr.xephi.authme.util.Utils; import java.lang.reflect.Method; import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; public class CacheDataSource implements DataSource { private final ConsoleLogger logger = ConsoleLoggerFactory.get(CacheDataSource.class); private final DataSource source; private final PlayerCache playerCache; private final LoadingCache> cachedAuths; private ListeningExecutorService executorService; /** * Constructor for CacheDataSource. * * @param source the source * @param playerCache the player cache */ public CacheDataSource(DataSource source, PlayerCache playerCache) { this.source = source; this.playerCache = playerCache; if (AuthMe.settings.getProperty(DatabaseSettings.USE_VIRTUAL_THREADS)) { try { Method method = Executors.class.getMethod("newVirtualThreadPerTaskExecutor"); method.setAccessible(true); ExecutorService ex = (ExecutorService) method.invoke(null); executorService = MoreExecutors.listeningDecorator(ex); logger.info("Using virtual threads for cache loader"); } catch (Exception e) { executorService = MoreExecutors.listeningDecorator( Executors.newCachedThreadPool(new ThreadFactoryBuilder() .setDaemon(true) .setNameFormat("AuthMe-CacheLoader") .build()) ); logger.info("Cannot enable virtual threads, fallback to CachedThreadPool"); } } else { executorService = MoreExecutors.listeningDecorator( Executors.newCachedThreadPool(new ThreadFactoryBuilder() .setDaemon(true) .setNameFormat("AuthMe-CacheLoader") .build()) ); } cachedAuths = CacheBuilder.newBuilder() .refreshAfterWrite(5, TimeUnit.MINUTES) .expireAfterAccess(15, TimeUnit.MINUTES) .build(new CacheLoader>() { @Override public Optional load(String key) { return Optional.ofNullable(source.getAuth(key)); } @Override public ListenableFuture> reload(final String key, Optional oldValue) { return executorService.submit(() -> load(key)); } }); } public LoadingCache> getCachedAuths() { return cachedAuths; } @Override public void reload() { source.reload(); } @Override public boolean isCached() { return true; } @Override public boolean isAuthAvailable(String user) { return getAuth(user) != null; } @Override public HashedPassword getPassword(String user) { user = user.toLowerCase(Locale.ROOT); Optional pAuthOpt = cachedAuths.getIfPresent(user); if (pAuthOpt != null && pAuthOpt.isPresent()) { return pAuthOpt.get().getPassword(); } return source.getPassword(user); } @Override public PlayerAuth getAuth(String user) { user = user.toLowerCase(Locale.ROOT); return cachedAuths.getUnchecked(user).orElse(null); } @Override public boolean saveAuth(PlayerAuth auth) { boolean result = source.saveAuth(auth); if (result) { cachedAuths.refresh(auth.getNickname()); } return result; } @Override public boolean updatePassword(PlayerAuth auth) { boolean result = source.updatePassword(auth); if (result) { cachedAuths.refresh(auth.getNickname()); } return result; } @Override public boolean updatePassword(String user, HashedPassword password) { user = user.toLowerCase(Locale.ROOT); boolean result = source.updatePassword(user, password); if (result) { cachedAuths.refresh(user); } return result; } @Override public boolean updateSession(PlayerAuth auth) { boolean result = source.updateSession(auth); if (result) { cachedAuths.refresh(auth.getNickname()); } return result; } @Override public boolean updateQuitLoc(final PlayerAuth auth) { boolean result = source.updateQuitLoc(auth); if (result) { cachedAuths.refresh(auth.getNickname()); } return result; } @Override public Set getRecordsToPurge(long until) { return source.getRecordsToPurge(until); } @Override public boolean removeAuth(String name) { name = name.toLowerCase(Locale.ROOT); boolean result = source.removeAuth(name); if (result) { cachedAuths.invalidate(name); } return result; } @Override public void closeConnection() { executorService.shutdown(); try { executorService.awaitTermination(5, TimeUnit.SECONDS); } catch (InterruptedException e) { logger.logException("Could not close executor service:", e); } cachedAuths.invalidateAll(); source.closeConnection(); } @Override public boolean updateEmail(final PlayerAuth auth) { boolean result = source.updateEmail(auth); if (result) { cachedAuths.refresh(auth.getNickname()); } return result; } @Override public List getAllAuthsByIp(String ip) { return source.getAllAuthsByIp(ip); } @Override public int countAuthsByEmail(String email) { return source.countAuthsByEmail(email); } @Override public void purgeRecords(Collection banned) { source.purgeRecords(banned); cachedAuths.invalidateAll(banned); } @Override public DataSourceType getType() { return source.getType(); } @Override public boolean isLogged(String user) { return source.isLogged(user); } @Override public void setLogged(final String user) { source.setLogged(user.toLowerCase(Locale.ROOT)); } @Override public void setUnlogged(final String user) { source.setUnlogged(user.toLowerCase(Locale.ROOT)); } @Override public boolean hasSession(final String user) { return source.hasSession(user); } @Override public void grantSession(final String user) { source.grantSession(user); } @Override public void revokeSession(final String user) { source.revokeSession(user); } @Override public void purgeLogged() { source.purgeLogged(); cachedAuths.invalidateAll(); } @Override public int getAccountsRegistered() { return source.getAccountsRegistered(); } @Override public boolean updateRealName(String user, String realName) { boolean result = source.updateRealName(user, realName); if (result) { cachedAuths.refresh(user); } return result; } @Override public DataSourceValue getEmail(String user) { return cachedAuths.getUnchecked(user) .map(auth -> DataSourceValueImpl.of(auth.getEmail())) .orElse(DataSourceValueImpl.unknownRow()); } @Override public List getAllAuths() { return source.getAllAuths(); } @Override public List getLoggedPlayersWithEmptyMail() { return playerCache.getCache().values().stream() .filter(auth -> Utils.isEmailEmpty(auth.getEmail())) .map(PlayerAuth::getRealName) .collect(Collectors.toList()); } @Override public List getRecentlyLoggedInPlayers() { return source.getRecentlyLoggedInPlayers(); } @Override public boolean setTotpKey(String user, String totpKey) { boolean result = source.setTotpKey(user, totpKey); if (result) { cachedAuths.refresh(user); } return result; } @Override public void invalidateCache(String playerName) { cachedAuths.invalidate(playerName); } @Override public void refreshCache(String playerName) { if (cachedAuths.getIfPresent(playerName) != null) { cachedAuths.refresh(playerName); } } }