diff options
author | nea <nea@nea.moe> | 2023-02-12 21:31:18 +0100 |
---|---|---|
committer | nea <nea@nea.moe> | 2023-02-12 21:31:18 +0100 |
commit | b88f4a94186c83fa258d27c650f5045e823b4f77 (patch) | |
tree | 277f9001cbe6141ff27d83b815337164aae2be06 /src/main/kotlin/io | |
parent | ecdc1e8639ca930a5fb23732b500894f54914428 (diff) | |
download | NotEnoughUpdates-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.kt | 105 | ||||
-rw-r--r-- | src/main/kotlin/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.kt | 47 |
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() + } + } +} |