diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/InMemoryCacheManager.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/InMemoryCacheManager.kt index be0abde5..12bdc70a 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/InMemoryCacheManager.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/InMemoryCacheManager.kt @@ -1,27 +1,40 @@ package dev.usbharu.hideout.service.ap.resource import dev.usbharu.hideout.domain.model.ap.Object +import dev.usbharu.hideout.util.LruCache import kotlinx.coroutines.delay import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.springframework.stereotype.Service +import java.time.Instant @Service class InMemoryCacheManager : CacheManager { - private val cacheKey = mutableSetOf() + private val cacheKey = LruCache(15) private val valueStore = mutableMapOf() private val keyMutex = Mutex() override suspend fun putCache(key: String, block: suspend () -> Object) { - val hasCache: Boolean + val needRunBlock: Boolean keyMutex.withLock { - hasCache = cacheKey.contains(key) - cacheKey.add(key) + cacheKey.filter { Instant.ofEpochMilli(it.value).plusSeconds(300) <= Instant.now() } + + val cached = cacheKey.get(key) + if (cached == null) { + needRunBlock = true + cacheKey[key] = Instant.now().toEpochMilli() + + valueStore.remove(key) + } else { + needRunBlock = false + } } - if (hasCache.not()) { + if (needRunBlock) { val processed = block() - valueStore[key] = processed + if (cacheKey.containsKey(key)) { + valueStore[key] = processed + } } } @@ -29,6 +42,9 @@ class InMemoryCacheManager : CacheManager { override suspend fun getOrWait(key: String): Object { while (valueStore.contains(key).not()) { + if (cacheKey.containsKey(key).not()) { + throw IllegalStateException("Invalid cache key.") + } delay(1) } return valueStore.getValue(key) diff --git a/src/main/kotlin/dev/usbharu/hideout/util/LruCache.kt b/src/main/kotlin/dev/usbharu/hideout/util/LruCache.kt new file mode 100644 index 00000000..ed0c3661 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/util/LruCache.kt @@ -0,0 +1,14 @@ +package dev.usbharu.hideout.util + +import java.io.Serial + +class LruCache(private val maxSize: Int) : LinkedHashMap(15, 0.75f, true) { + + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean = size > maxSize + + companion object { + @Serial + private const val serialVersionUID: Long = -6446947260925053191L + } + +} diff --git a/src/test/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveServiceImplTest.kt index 16b171ff..068cfe5f 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveServiceImplTest.kt @@ -1,5 +1,6 @@ package dev.usbharu.hideout.service.ap.resource +import dev.usbharu.hideout.domain.model.ap.Object import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.repository.UserRepository import io.ktor.client.* @@ -52,7 +53,7 @@ class APResourceResolveServiceImplTest { val apResourceResolveService = APResourceResolveServiceImpl(httpClient, userRepository, InMemoryCacheManager(), objectMapper) - apResourceResolveService.resolve("https", 0) + apResourceResolveService.resolve("https", 0) assertEquals(1, count) } @@ -87,10 +88,10 @@ class APResourceResolveServiceImplTest { val apResourceResolveService = APResourceResolveServiceImpl(httpClient, userRepository, InMemoryCacheManager(), objectMapper) - apResourceResolveService.resolve("https", 0) - apResourceResolveService.resolve("https", 0) - apResourceResolveService.resolve("https", 0) - apResourceResolveService.resolve("https", 0) + apResourceResolveService.resolve("https", 0) + apResourceResolveService.resolve("https", 0) + apResourceResolveService.resolve("https", 0) + apResourceResolveService.resolve("https", 0) assertEquals(1, count) } @@ -127,17 +128,17 @@ class APResourceResolveServiceImplTest { repeat(10) { awaitAll( - async { apResourceResolveService.resolve("https", 0) }, - async { apResourceResolveService.resolve("https", 0) }, - async { apResourceResolveService.resolve("https", 0) }, - async { apResourceResolveService.resolve("https", 0) }, - async { apResourceResolveService.resolve("https", 0) }, - async { apResourceResolveService.resolve("https", 0) }, - async { apResourceResolveService.resolve("https", 0) }, - async { apResourceResolveService.resolve("https", 0) }, - async { apResourceResolveService.resolve("https", 0) }, - async { apResourceResolveService.resolve("https", 0) }, - async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, ) } @@ -174,9 +175,9 @@ class APResourceResolveServiceImplTest { val apResourceResolveService = APResourceResolveServiceImpl(httpClient, userRepository, InMemoryCacheManager(), objectMapper) - apResourceResolveService.resolve("abcd", 0) - apResourceResolveService.resolve("1234", 0) - apResourceResolveService.resolve("aaaa", 0) + apResourceResolveService.resolve("abcd", 0) + apResourceResolveService.resolve("1234", 0) + apResourceResolveService.resolve("aaaa", 0) assertEquals(3, count) }