aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/commands/misc/PronounsCommand.java2
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java5
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/ApiCache.kt105
-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()
+ }
+ }
}