diff options
-rw-r--r-- | src/main/java/io/github/moulberry/notenoughupdates/commands/misc/PronounsCommand.java | 2 | ||||
-rw-r--r-- | src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java | 5 | ||||
-rw-r--r-- | src/main/kotlin/io/github/moulberry/notenoughupdates/util/ApiCache.kt | 105 | ||||
-rw-r--r-- | src/main/kotlin/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.kt (renamed from src/main/java/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.java) | 38 |
4 files changed, 119 insertions, 31 deletions
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/commands/misc/PronounsCommand.java b/src/main/java/io/github/moulberry/notenoughupdates/commands/misc/PronounsCommand.java index 5a4f1400..cf0d0c56 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/commands/misc/PronounsCommand.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/commands/misc/PronounsCommand.java @@ -88,7 +88,7 @@ public class PronounsCommand extends ClientCommandBase { "§e[NEU] Pronouns for §b" + user + " §eon §b" + platform + "§e:"), id); betterPronounChoice.render().forEach(it -> nc.printChatMessage(new ChatComponentText("§e[NEU] §a" + it))); return null; - }, MinecraftExecutor.INSTANCE); + }, MinecraftExecutor.OnThread); } } diff --git a/src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java b/src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java index 708f4677..e34324fe 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java @@ -56,6 +56,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.zip.GZIPInputStream; @@ -210,7 +211,7 @@ public class ApiUtil { conn.setDoOutput(true); OutputStream os = conn.getOutputStream(); try { - os.write(this.postData.getBytes("utf-8")); + os.write(this.postData.getBytes(StandardCharsets.UTF_8)); } finally { os.close(); } @@ -252,7 +253,7 @@ public class ApiUtil { } public <T> CompletableFuture<T> requestJson(Class<? extends T> clazz) { - return requestString().thenApply(str -> gson.fromJson(str, clazz)); + return requestString().thenApplyAsync(str -> gson.fromJson(str, clazz)); } } diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/ApiCache.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/ApiCache.kt index d8f3f0f4..c14df425 100644 --- a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/ApiCache.kt +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/ApiCache.kt @@ -23,10 +23,15 @@ import io.github.moulberry.notenoughupdates.NotEnoughUpdates import io.github.moulberry.notenoughupdates.options.customtypes.NEUDebugFlag import io.github.moulberry.notenoughupdates.util.ApiUtil.Request import org.apache.http.NameValuePair +import java.nio.file.Files +import java.nio.file.Path import java.time.Duration import java.util.* import java.util.concurrent.CompletableFuture import java.util.function.Supplier +import kotlin.io.path.deleteIfExists +import kotlin.io.path.readText +import kotlin.io.path.writeText import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.seconds import kotlin.time.ExperimentalTime @@ -43,15 +48,74 @@ object ApiCache { ) data class CacheResult( - val future: CompletableFuture<String>, + private var future: CompletableFuture<String>?, val firedAt: TimeSource.Monotonic.ValueTimeMark, - ) + private var file: Path? = null, + private var disposed: Boolean = false, + ) { + init { + future!!.thenAcceptAsync { text -> + synchronized(this) { + if (disposed) { + return@synchronized + } + future = null + val f = Files.createTempFile(cacheBaseDir, "api-cache", ".bin") + log("Writing cache to disk: $f") + f.toFile().deleteOnExit() + f.writeText(text) + file = f + } + } + } + + val isAvailable get() = file != null && !disposed + fun getCachedFuture(): CompletableFuture<String> { + synchronized(this) { + if (disposed) { + return CompletableFuture.supplyAsync { + throw IllegalStateException("Attempting to read from a disposed future at $file. Most likely caused by non synchronized access to ApiCache.cachedRequests") + } + } + val fut = future + if (fut != null) { + return fut + } else { + val text = file!!.readText() + return CompletableFuture.completedFuture(text) + } + } + } + + /** + * Should be called when removing / replacing a request from [cachedRequests]. + * Should only be called while holding a lock on [ApiCache]. + * This deletes the disk cache and smashes the internal state for it to be GCd. + * After calling this method no other method may be called on this object. + */ + internal fun dispose() { + synchronized(this) { + if (disposed) return + log("Disposing cache for $file") + disposed = true + file?.deleteIfExists() + future = null + } + } + } + + private val cacheBaseDir by lazy { + val d = Files.createTempDirectory("neu-cache") + d.toFile().deleteOnExit() + d + } private val cachedRequests = mutableMapOf<CacheKey, CacheResult>() val histogramTotalRequests: MutableMap<String, Int> = mutableMapOf() val histogramNonCachedRequests: MutableMap<String, Int> = mutableMapOf() + private val timeout = 10.seconds - private val maxCacheAge = 1.hours + private val globalMaxCacheAge = 1.hours private fun log(message: String) { NEUDebugFlag.API_CACHE.log(message) @@ -83,15 +147,17 @@ object ApiCache { } else { log("Cache hit for api request for url ${request.baseUrl} by $callingClassText.") } - } private fun evictCache() { synchronized(this) { val it = cachedRequests.iterator() while (it.hasNext()) { - if (it.next().value.firedAt.elapsedNow() >= maxCacheAge) + val next = it.next() + if (next.value.firedAt.elapsedNow() >= globalMaxCacheAge) { + next.value.dispose() it.remove() + } } } } @@ -113,6 +179,7 @@ object ApiCache { } fun recache(): CompletableFuture<String> { return futureSupplier.get().also { + cachedRequests[cacheKey]?.dispose() // Safe to dispose like this because this function is always called in a synchronized block cachedRequests[cacheKey] = CacheResult(it, TimeSource.Monotonic.markNow()) } } @@ -122,16 +189,26 @@ object ApiCache { traceApiRequest(request, "no cache found") return recache() } - if (cachedRequest.future.isDone && cachedRequest.firedAt.elapsedNow() > maxAge.toKotlinDuration()) { - traceApiRequest(request, "outdated cache") - return recache() - } - if (!cachedRequest.future.isDone && cachedRequest.firedAt.elapsedNow() > timeout) { - traceApiRequest(request, "suspiciously slow api response") - return recache() + + return if (cachedRequest.isAvailable) { + if (cachedRequest.firedAt.elapsedNow() > maxAge.toKotlinDuration()) { + traceApiRequest(request, "outdated cache") + recache() + } else { + // Using local cached request + traceApiRequest(request, null) + cachedRequest.getCachedFuture() + } + } else { + if (cachedRequest.firedAt.elapsedNow() > timeout) { + traceApiRequest(request, "suspiciously slow api response") + recache() + } else { + // Joining ongoing request + traceApiRequest(request, null) + cachedRequest.getCachedFuture() + } } - traceApiRequest(request, null) - return cachedRequest.future } } diff --git a/src/main/java/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.java b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.kt index bf973b76..bb0bc8b4 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.java +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 NotEnoughUpdates contributors + * Copyright (C) 2023 NotEnoughUpdates contributors * * This file is part of NotEnoughUpdates. * @@ -17,21 +17,31 @@ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>. */ -package io.github.moulberry.notenoughupdates.util; +package io.github.moulberry.notenoughupdates.util -import net.minecraft.client.Minecraft; -import org.jetbrains.annotations.NotNull; +import net.minecraft.client.Minecraft +import java.util.concurrent.Executor +import java.util.concurrent.ForkJoinPool -import java.util.concurrent.Executor; +object MinecraftExecutor { -public class MinecraftExecutor implements Executor { + @JvmField + val OnThread = Executor { + val mc = Minecraft.getMinecraft() + if (mc.isCallingFromMinecraftThread) { + it.run() + } else { + Minecraft.getMinecraft().addScheduledTask(it) + } + } - public static MinecraftExecutor INSTANCE = new MinecraftExecutor(); - - private MinecraftExecutor() {} - - @Override - public void execute(@NotNull Runnable runnable) { - Minecraft.getMinecraft().addScheduledTask(runnable); - } + @JvmField + val OffThread = Executor { + val mc = Minecraft.getMinecraft() + if (mc.isCallingFromMinecraftThread) { + ForkJoinPool.commonPool().execute(it) + } else { + it.run() + } + } } |