aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/io
diff options
context:
space:
mode:
authornea <nea@nea.moe>2023-02-12 21:31:18 +0100
committernea <nea@nea.moe>2023-02-12 21:31:18 +0100
commitb88f4a94186c83fa258d27c650f5045e823b4f77 (patch)
tree277f9001cbe6141ff27d83b815337164aae2be06 /src/main/kotlin/io
parentecdc1e8639ca930a5fb23732b500894f54914428 (diff)
downloadNotEnoughUpdates-fix/apispam.tar.gz
NotEnoughUpdates-fix/apispam.tar.bz2
NotEnoughUpdates-fix/apispam.zip
Disk cache for the APIfix/apispam
Diffstat (limited to 'src/main/kotlin/io')
-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.kt47
2 files changed, 138 insertions, 14 deletions
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/kotlin/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.kt
new file mode 100644
index 00000000..bb0bc8b4
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.util
+
+import net.minecraft.client.Minecraft
+import java.util.concurrent.Executor
+import java.util.concurrent.ForkJoinPool
+
+object MinecraftExecutor {
+
+ @JvmField
+ val OnThread = Executor {
+ val mc = Minecraft.getMinecraft()
+ if (mc.isCallingFromMinecraftThread) {
+ it.run()
+ } else {
+ Minecraft.getMinecraft().addScheduledTask(it)
+ }
+ }
+
+ @JvmField
+ val OffThread = Executor {
+ val mc = Minecraft.getMinecraft()
+ if (mc.isCallingFromMinecraftThread) {
+ ForkJoinPool.commonPool().execute(it)
+ } else {
+ it.run()
+ }
+ }
+}