diff --git a/common/src/main/java/net/william278/papiproxybridge/api/PlaceholderAPI.java b/common/src/main/java/net/william278/papiproxybridge/api/PlaceholderAPI.java index fccae2f..c28bee8 100644 --- a/common/src/main/java/net/william278/papiproxybridge/api/PlaceholderAPI.java +++ b/common/src/main/java/net/william278/papiproxybridge/api/PlaceholderAPI.java @@ -121,7 +121,7 @@ public static void clearCache(@NotNull UUID player) { }); } - private record CacheKey(@NotNull UUID requester, @NotNull UUID formatFor) { + private record CacheKey(@NotNull UUID requester, @NotNull UUID formatFor, @NotNull String scope) { private boolean contains(@NotNull UUID player) { return requester.equals(player) || formatFor.equals(player); @@ -168,7 +168,7 @@ private CompletableFuture formatPlaceholders(@NotNull String text, @NotN if (!requester.isConnected()) { return CompletableFuture.completedFuture(text); } - final CacheKey cacheKey = new CacheKey(requester.getUniqueId(), formatFor); + final CacheKey cacheKey = new CacheKey(requester.getUniqueId(), formatFor, requester.getPlaceholderCacheScope()); if (cacheExpiry > 0 && cache.containsKey(cacheKey) && cache.get(cacheKey).containsKey(text)) { return CompletableFuture.completedFuture(cache.get(cacheKey).get(text)); } @@ -273,7 +273,7 @@ private CompletableFuture formatComponentPlaceholders(@NotNull String if (!requester.isConnected()) { return CompletableFuture.completedFuture(Component.text(text)); } - final CacheKey cacheKey = new CacheKey(requester.getUniqueId(), formatFor); + final CacheKey cacheKey = new CacheKey(requester.getUniqueId(), formatFor, requester.getPlaceholderCacheScope()); if (cacheExpiry > 0 && componentCache.containsKey(cacheKey) && componentCache.get(cacheKey).containsKey(text)) { return CompletableFuture.completedFuture(componentCache.get(cacheKey).get(text)); } diff --git a/common/src/main/java/net/william278/papiproxybridge/user/OnlineUser.java b/common/src/main/java/net/william278/papiproxybridge/user/OnlineUser.java index b3f1c38..c73e245 100644 --- a/common/src/main/java/net/william278/papiproxybridge/user/OnlineUser.java +++ b/common/src/main/java/net/william278/papiproxybridge/user/OnlineUser.java @@ -36,6 +36,18 @@ public interface OnlineUser { @NotNull UUID getUniqueId(); + /** + * Returns the runtime context in which placeholder results may be reused. + * Implementations should change this value when the same user can resolve + * placeholders against a different backend or data source. + * + * @return the placeholder cache scope + */ + @NotNull + default String getPlaceholderCacheScope() { + return ""; + } + default void sendMessage(@NotNull PAPIProxyBridge plugin, @NotNull Request request, boolean wantsJson, boolean isRequest) { final ByteArrayDataOutput messageWriter = ByteStreams.newDataOutput(); final UUID uuid = getUniqueId(); diff --git a/common/src/test/java/net/william278/papiproxybridge/api/PlaceholderAPITest.java b/common/src/test/java/net/william278/papiproxybridge/api/PlaceholderAPITest.java index 6cbc62f..1bb6d07 100644 --- a/common/src/test/java/net/william278/papiproxybridge/api/PlaceholderAPITest.java +++ b/common/src/test/java/net/william278/papiproxybridge/api/PlaceholderAPITest.java @@ -36,6 +36,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; @@ -89,6 +90,30 @@ void componentCacheUsesRequesterAndFormattingTarget() { assertEquals(3, bridge.requests.get()); } + @Test + void cacheIsIsolatedByBackendServer() { + final TestProxyUser proxyUser = new TestProxyUser(UUID.randomUUID(), "lobby"); + bridge.response = "lobby"; + assertEquals("lobby", api.formatPlaceholders("%server_name%", proxyUser).join()); + + proxyUser.serverName = "survival"; + bridge.response = "survival"; + assertEquals("survival", api.formatPlaceholders("%server_name%", proxyUser).join()); + assertEquals(2, bridge.requests.get()); + } + + @Test + void componentCacheIsIsolatedByBackendServer() { + final TestProxyUser proxyUser = new TestProxyUser(UUID.randomUUID(), "lobby"); + bridge.response = "{\"text\":\"lobby\"}"; + assertEquals(Component.text("lobby"), api.formatComponentPlaceholders("%server_name%", proxyUser).join()); + + proxyUser.serverName = "survival"; + bridge.response = "{\"text\":\"survival\"}"; + assertEquals(Component.text("survival"), api.formatComponentPlaceholders("%server_name%", proxyUser).join()); + assertEquals(2, bridge.requests.get()); + } + @Test void retriesFailedRequests() { api.setRetryTimes(1); @@ -99,11 +124,23 @@ void retriesFailedRequests() { assertEquals(2, bridge.requests.get()); } + @Test + void retriesTimedOutRequests() throws Exception { + api.setRequestTimeout(20); + api.setRetryTimes(1); + bridge.timeouts = 1; + bridge.response = "formatted"; + + assertEquals("formatted", api.formatPlaceholders("%name%", requester).get(1, TimeUnit.SECONDS)); + assertEquals(2, bridge.requests.get()); + } + private static final class TestBridge implements PAPIProxyBridge { private final TestUser user; private final AtomicInteger requests = new AtomicInteger(); private String response; private int failures; + private int timeouts; private TestBridge(TestUser user) { this.user = user; @@ -128,6 +165,9 @@ public Optional findPlayer(@NotNull UUID uuid) { public CompletableFuture createRequest(@NotNull String text, @NotNull OnlineUser requester, @NotNull UUID formatFor, boolean wantsJson, long requestTimeout) { requests.incrementAndGet(); + if (timeouts-- > 0) { + return new CompletableFuture<>(); + } if (failures-- > 0) { return CompletableFuture.failedFuture(new IllegalStateException("failed")); } @@ -182,4 +222,33 @@ private record TestUser(UUID uuid) implements OnlineUser { public void handleMessage(@NotNull PAPIProxyBridge plugin, @NotNull Request message, boolean wantsJson) { } } + + private static final class TestProxyUser implements OnlineUser { + private final UUID uuid; + private String serverName; + + private TestProxyUser(UUID uuid, String serverName) { + this.uuid = uuid; + this.serverName = serverName; + } + + @Override + public @NotNull String getUsername() { + return "proxy-user"; + } + + @Override + public @NotNull UUID getUniqueId() { + return uuid; + } + + @Override + public @NotNull String getPlaceholderCacheScope() { + return serverName; + } + + @Override + public void handleMessage(@NotNull PAPIProxyBridge plugin, @NotNull Request message, boolean wantsJson) { + } + } } diff --git a/proxy/src/main/java/net/william278/papiproxybridge/user/ProxyUser.java b/proxy/src/main/java/net/william278/papiproxybridge/user/ProxyUser.java index 6cb11c0..961da99 100644 --- a/proxy/src/main/java/net/william278/papiproxybridge/user/ProxyUser.java +++ b/proxy/src/main/java/net/william278/papiproxybridge/user/ProxyUser.java @@ -43,4 +43,10 @@ default void handleMessage(@NotNull PAPIProxyBridge plugin, @NotNull Request mes @NotNull String getServerName(); + @Override + @NotNull + default String getPlaceholderCacheScope() { + return getServerName(); + } + }