#18 Use a proper memory cache
This commit is contained in:
parent
c42fc26a92
commit
9ed081dc92
|
@ -31,6 +31,7 @@ dependencies {
|
|||
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
|
||||
implementation("dev.fyloz.colorrecipesexplorer:database-manager:5.2.1")
|
||||
implementation("dev.fyloz:memorycache:1.0")
|
||||
implementation("io.github.microutils:kotlin-logging-jvm:2.1.21")
|
||||
implementation("io.jsonwebtoken:jjwt-api:0.11.2")
|
||||
implementation("io.jsonwebtoken:jjwt-impl:0.11.2")
|
||||
|
@ -61,6 +62,8 @@ dependencies {
|
|||
runtimeOnly("mysql:mysql-connector-java:8.0.22")
|
||||
runtimeOnly("org.postgresql:postgresql:42.2.16")
|
||||
runtimeOnly("com.microsoft.sqlserver:mssql-jdbc:9.2.1.jre11")
|
||||
|
||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:${springBootVersion}")
|
||||
}
|
||||
|
||||
springBoot {
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.ColorRecipesExplorerApplication
|
||||
import dev.fyloz.colorrecipesexplorer.DatabaseUpdaterProperties
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import dev.fyloz.colorrecipesexplorer.service.files.CachedFileSystemItem
|
||||
import dev.fyloz.memorycache.ExpiringMemoryCache
|
||||
import dev.fyloz.memorycache.MemoryCache
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(MaterialTypeProperties::class, CreProperties::class)
|
||||
class SpringConfiguration {
|
||||
class CreConfiguration(private val creProperties: CreProperties) {
|
||||
@Bean
|
||||
fun logger(): Logger = LoggerFactory.getLogger(ColorRecipesExplorerApplication::class.java)
|
||||
fun fileCache(): MemoryCache<String, CachedFileSystemItem> =
|
||||
ExpiringMemoryCache(maxAccessCount = creProperties.fileCacheMaxAccessCount)
|
||||
}
|
|
@ -5,11 +5,13 @@ import kotlin.properties.Delegates.notNull
|
|||
|
||||
const val DEFAULT_DATA_DIRECTORY = "data"
|
||||
const val DEFAULT_CONFIG_DIRECTORY = "config"
|
||||
const val DEFAULT_FILE_CACHE_MAX_ACCESS_COUNT = 10_000L
|
||||
|
||||
@ConfigurationProperties(prefix = "cre.server")
|
||||
class CreProperties {
|
||||
var dataDirectory: String = DEFAULT_DATA_DIRECTORY
|
||||
var configDirectory: String = DEFAULT_CONFIG_DIRECTORY
|
||||
var fileCacheMaxAccessCount: Long = DEFAULT_FILE_CACHE_MAX_ACCESS_COUNT
|
||||
}
|
||||
|
||||
@ConfigurationProperties(prefix = "cre.security")
|
||||
|
|
|
@ -3,6 +3,7 @@ package dev.fyloz.colorrecipesexplorer.service.files
|
|||
import dev.fyloz.colorrecipesexplorer.JavaFile
|
||||
import dev.fyloz.colorrecipesexplorer.utils.File
|
||||
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||
import dev.fyloz.memorycache.MemoryCache
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
|
@ -18,33 +19,32 @@ interface FileCache {
|
|||
|
||||
/** Gets the cached file at the given [path]. */
|
||||
fun getFile(path: FilePath): CachedFile?
|
||||
|
||||
|
||||
/** Checks if the cached file system item at the given [path] exists. */
|
||||
fun exists(path: FilePath): Boolean
|
||||
|
||||
|
||||
/** Checks if the cached directory at the given [path] exists. */
|
||||
fun directoryExists(path: FilePath): Boolean
|
||||
|
||||
|
||||
/** Checks if the cached file at the given [path] exists. */
|
||||
fun fileExists(path: FilePath): Boolean
|
||||
|
||||
/** Sets the file system item at the given [path] as existing or not. Loads the item in the cache if not already present. */
|
||||
|
||||
/** Sets the file system item at the given [path] as existing or not. Loads the item in the cache if not already present. */
|
||||
fun setExists(path: FilePath, exists: Boolean = true)
|
||||
|
||||
|
||||
/** Loads the file system item at the given [path] into the cache. */
|
||||
fun load(path: FilePath)
|
||||
|
||||
|
||||
/** Adds the file system item at the given [itemPath] to the cached directory at the given [directoryPath]. */
|
||||
fun addItemToDirectory(directoryPath: FilePath, itemPath: FilePath)
|
||||
|
||||
/** Removes the file system item at the given [itemPath] from the cached directory at the given [directoryPath]. */
|
||||
|
||||
/** Removes the file system item at the given [itemPath] from the cached directory at the given [directoryPath]. */
|
||||
fun removeItemFromDirectory(directoryPath: FilePath, itemPath: FilePath)
|
||||
}
|
||||
|
||||
@Component
|
||||
class DefaultFileCache : FileCache {
|
||||
class DefaultFileCache(private val cache: MemoryCache<String, CachedFileSystemItem>) : FileCache {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
private val cache = hashMapOf<String, CachedFileSystemItem>()
|
||||
|
||||
override operator fun contains(path: FilePath) =
|
||||
path.value in cache
|
||||
|
@ -71,7 +71,7 @@ class DefaultFileCache : FileCache {
|
|||
}
|
||||
|
||||
override fun exists(path: FilePath) =
|
||||
path in this && cache[path.value]!!.exists
|
||||
path in this && this[path]!!.exists
|
||||
|
||||
override fun directoryExists(path: FilePath) =
|
||||
exists(path) && this[path] is CachedDirectory
|
||||
|
@ -84,13 +84,13 @@ class DefaultFileCache : FileCache {
|
|||
load(path)
|
||||
}
|
||||
|
||||
this[path] = this[path]!!.clone(exists)
|
||||
this[path] = this[path]!!.clone(exists = exists)
|
||||
logger.debug("Updated FileCache state: ${path.value} exists -> $exists")
|
||||
}
|
||||
|
||||
override fun load(path: FilePath) =
|
||||
with(JavaFile(path.value).toFileSystemItem()) {
|
||||
cache[path.value] = this
|
||||
this@DefaultFileCache[path] = this
|
||||
|
||||
logger.debug("Loaded file at ${path.value} into FileCache")
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ class File(val file: JavaFile) {
|
|||
}
|
||||
|
||||
// TODO: Move to value class when mocking them with mockk works
|
||||
class FilePath(val value: String)
|
||||
data class FilePath(val value: String)
|
||||
|
||||
/** Runs the given [block] in the context of a file with the given [fullPath]. */
|
||||
fun <T> withFileAt(fullPath: FilePath, block: File.() -> T) =
|
||||
|
|
|
@ -5,7 +5,6 @@ cre.server.data-directory=data
|
|||
cre.server.config-directory=config
|
||||
cre.security.jwt-secret=CtnvGQjgZ44A1fh295gE78WWOgl8InrbwBgQsMy0
|
||||
cre.security.jwt-duration=18000000
|
||||
cre.security.aes-secret=blabla
|
||||
# Root user
|
||||
cre.security.root.id=9999
|
||||
cre.security.root.password=password
|
||||
|
|
|
@ -0,0 +1,426 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.files
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||
import dev.fyloz.memorycache.MemoryCache
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
internal class DefaultFileCacheTest {
|
||||
private val memoryCacheMock = mockk<MemoryCache<String, CachedFileSystemItem>>()
|
||||
|
||||
private val fileCache = spyk(DefaultFileCache(memoryCacheMock))
|
||||
|
||||
private val path = FilePath("unit_test_path")
|
||||
private val cachedFile = CachedFile("unit_test_file", path, true)
|
||||
private val cachedDirectory = CachedDirectory("unit_test_dictionary", path, true)
|
||||
|
||||
@AfterEach
|
||||
internal fun afterEach() {
|
||||
clearAllMocks()
|
||||
}
|
||||
|
||||
private fun setup_memoryCacheMock_set() {
|
||||
every { memoryCacheMock[any()] = any() } just runs
|
||||
}
|
||||
|
||||
@Test
|
||||
fun contains_normalBehavior_returnsTrue() {
|
||||
// Arrange
|
||||
every { any() in memoryCacheMock} returns true
|
||||
|
||||
// Act
|
||||
val contains = path in fileCache
|
||||
|
||||
// Assert
|
||||
assertTrue(contains)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun contains_pathNotCached_returnsFalse() {
|
||||
// Arrange
|
||||
every { any() in memoryCacheMock} returns false
|
||||
|
||||
// Act
|
||||
val contains = path in fileCache
|
||||
|
||||
// Assert
|
||||
assertFalse(contains)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun get_normalBehavior_returnsCachedItem() {
|
||||
// Arrange
|
||||
every { memoryCacheMock[any()] } returns cachedFile
|
||||
|
||||
// Act
|
||||
val item = fileCache[path]
|
||||
|
||||
// Assert
|
||||
assertEquals(cachedFile, item)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun get_pathNotCached_returnsNull() {
|
||||
// Arrange
|
||||
every { memoryCacheMock[any()] } returns null
|
||||
|
||||
// Act
|
||||
val item = fileCache[path]
|
||||
|
||||
// Assert
|
||||
assertNull(item)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getDirectory_normalBehavior_returnsCachedDirectory() {
|
||||
// Arrange
|
||||
every { fileCache.directoryExists(any()) } returns true
|
||||
every { fileCache[any()] } returns cachedDirectory
|
||||
|
||||
// Act
|
||||
val directory = fileCache.getDirectory(path)
|
||||
|
||||
// Assert
|
||||
assertEquals(cachedDirectory, directory)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getDirectory_directoryDoesNotExists_returnsNull() {
|
||||
// Arrange
|
||||
every { fileCache.directoryExists(any()) } returns false
|
||||
every { fileCache[any()] } returns cachedDirectory
|
||||
|
||||
// Act
|
||||
val directory = fileCache.getDirectory(path)
|
||||
|
||||
// Assert
|
||||
assertNull(directory)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getFile_normalBehavior_returnsCachedFile() {
|
||||
// Arrange
|
||||
every { fileCache[any()] } returns cachedFile
|
||||
every { fileCache.fileExists(any()) } returns true
|
||||
|
||||
// Act
|
||||
val file = fileCache.getFile(path)
|
||||
|
||||
// Assert
|
||||
assertEquals(cachedFile, file)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getFile_fileDoesNotExists_returnsNull() {
|
||||
// Arrange
|
||||
every { fileCache[any()] } returns cachedFile
|
||||
every { fileCache.fileExists(any()) } returns false
|
||||
|
||||
// Act
|
||||
val file = fileCache.getFile(path)
|
||||
|
||||
// Assert
|
||||
assertNull(file)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun exists_normalBehavior_returnsTrue() {
|
||||
// Arrange
|
||||
every { any() in fileCache } returns true
|
||||
every { fileCache[any()] } returns cachedFile
|
||||
|
||||
// Act
|
||||
val exists = fileCache.exists(path)
|
||||
|
||||
// Assert
|
||||
assertTrue(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun exists_pathNotCached_returnsFalse() {
|
||||
// Arrange
|
||||
every { any() in fileCache } returns false
|
||||
every { fileCache[any()] } returns cachedFile
|
||||
|
||||
// Act
|
||||
val exists = fileCache.exists(path)
|
||||
|
||||
// Assert
|
||||
assertFalse(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun exists_itemDoesNotExists_returnsFalse() {
|
||||
// Arrange
|
||||
val file = cachedFile.copy(exists = false)
|
||||
|
||||
every { any() in fileCache } returns true
|
||||
every { fileCache[any()] } returns file
|
||||
|
||||
// Act
|
||||
val exists = fileCache.exists(path)
|
||||
|
||||
// Assert
|
||||
assertFalse(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun directoryExists_normalBehavior_returnsTrue() {
|
||||
// Arrange
|
||||
every { fileCache.exists(any()) } returns true
|
||||
every { fileCache[any()] } returns cachedDirectory
|
||||
|
||||
// Act
|
||||
val exists = fileCache.directoryExists(path)
|
||||
|
||||
// Assert
|
||||
assertTrue(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun directoryExists_pathNotCached_returnsFalse() {
|
||||
// Arrange
|
||||
every { fileCache.exists(any()) } returns false
|
||||
every { fileCache[any()] } returns cachedDirectory
|
||||
|
||||
// Act
|
||||
val exists = fileCache.directoryExists(path)
|
||||
|
||||
// Assert
|
||||
assertFalse(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun directoryExists_cachedItemIsNotDirectory_returnsFalse() {
|
||||
// Arrange
|
||||
every { fileCache.exists(any()) } returns true
|
||||
every { fileCache[any()] } returns cachedFile
|
||||
|
||||
// Act
|
||||
val exists = fileCache.directoryExists(path)
|
||||
|
||||
// Assert
|
||||
assertFalse(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fileExists_normalBehavior_returnsTrue() {
|
||||
// Arrange
|
||||
every { fileCache.exists(any()) } returns true
|
||||
every { fileCache[any()] } returns cachedFile
|
||||
|
||||
// Act
|
||||
val exists = fileCache.fileExists(path)
|
||||
|
||||
// Assert
|
||||
assertTrue(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fileExists_pathNotCached_returnsFalse() {
|
||||
// Arrange
|
||||
every { fileCache.exists(any()) } returns false
|
||||
every { fileCache[any()] } returns cachedFile
|
||||
|
||||
// Act
|
||||
val exists = fileCache.fileExists(path)
|
||||
|
||||
// Assert
|
||||
assertFalse(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fileExists_cachedItemIsNotFile_returnsFalse() {
|
||||
// Arrange
|
||||
every { fileCache.exists(any()) } returns true
|
||||
every { fileCache[any()] } returns cachedDirectory
|
||||
|
||||
// Act
|
||||
val exists = fileCache.fileExists(path)
|
||||
|
||||
// Assert
|
||||
assertFalse(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setExists_normalBehavior_callsSetInCache() {
|
||||
// Arrange
|
||||
every { any() in fileCache } returns true
|
||||
every { fileCache[any()] } returns cachedFile
|
||||
|
||||
setup_memoryCacheMock_set()
|
||||
|
||||
val shouldExists = !cachedFile.exists
|
||||
|
||||
// Act
|
||||
fileCache.setExists(path, exists = shouldExists)
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
memoryCacheMock[path.value] = match { it.exists == shouldExists }
|
||||
}
|
||||
confirmVerified(memoryCacheMock)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setExists_pathNotCached_callsLoadPath() {
|
||||
// Arrange
|
||||
every { any() in fileCache } returns false
|
||||
every { fileCache[any()] } returns cachedFile
|
||||
|
||||
setup_memoryCacheMock_set()
|
||||
|
||||
// Act
|
||||
fileCache.setExists(path, exists = true)
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
fileCache.load(path)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun load_normalBehavior_callsSetInCache() {
|
||||
// Arrange
|
||||
|
||||
setup_memoryCacheMock_set()
|
||||
|
||||
// Act
|
||||
fileCache.load(path)
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
memoryCacheMock[path.value] = match { it.path == path }
|
||||
}
|
||||
confirmVerified(memoryCacheMock)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addItemToDirectory_normalBehavior_addsItemToDirectoryContent() {
|
||||
// Arrange
|
||||
every { fileCache.directoryExists(path) } returns true
|
||||
every { fileCache.getDirectory(path) } returns cachedDirectory
|
||||
|
||||
setup_memoryCacheMock_set()
|
||||
|
||||
val itemPath = FilePath("${path.value}/item")
|
||||
|
||||
// Act
|
||||
fileCache.addItemToDirectory(path, itemPath)
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
memoryCacheMock[path.value] = match<CachedDirectory> {
|
||||
it.content.any { item -> item.path == itemPath }
|
||||
}
|
||||
}
|
||||
confirmVerified(memoryCacheMock)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addItemToDirectory_directoryDoesNotExists_doesNothing() {
|
||||
// Arrange
|
||||
every { fileCache.directoryExists(path) } returns false
|
||||
every { fileCache.getDirectory(path) } returns cachedDirectory
|
||||
|
||||
setup_memoryCacheMock_set()
|
||||
|
||||
val itemPath = FilePath("${path.value}/item")
|
||||
|
||||
// Act
|
||||
fileCache.addItemToDirectory(path, itemPath)
|
||||
|
||||
// Assert
|
||||
verify(exactly = 0) {
|
||||
memoryCacheMock[path.value] = any()
|
||||
}
|
||||
confirmVerified(memoryCacheMock)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addItemToDirectory_notADirectory_doesNothing() {
|
||||
// Arrange
|
||||
every { fileCache.directoryExists(path) } returns true
|
||||
every { fileCache.getDirectory(path) } returns null
|
||||
|
||||
setup_memoryCacheMock_set()
|
||||
|
||||
val itemPath = FilePath("${path.value}/item")
|
||||
|
||||
// Act
|
||||
fileCache.addItemToDirectory(path, itemPath)
|
||||
|
||||
// Assert
|
||||
verify(exactly = 0) {
|
||||
memoryCacheMock[path.value] = any()
|
||||
}
|
||||
confirmVerified(memoryCacheMock)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeItemFromDirectory_normalBehavior_removesItemFromDirectoryContent() {
|
||||
// Arrange
|
||||
val itemPath = FilePath("${path.value}/item")
|
||||
val file = cachedFile.copy(path = itemPath)
|
||||
val directory = cachedDirectory.copy(content = setOf(file))
|
||||
|
||||
every { fileCache.directoryExists(path) } returns true
|
||||
every { fileCache.getDirectory(path) } returns directory
|
||||
|
||||
setup_memoryCacheMock_set()
|
||||
|
||||
// Act
|
||||
fileCache.removeItemFromDirectory(path, itemPath)
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
memoryCacheMock[path.value] = match<CachedDirectory> { it.content.isEmpty() }
|
||||
}
|
||||
confirmVerified(memoryCacheMock)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeItemFromDirectory_directoryDoesNotExists_doesNothing() {
|
||||
// Arrange
|
||||
every { fileCache.directoryExists(path) } returns false
|
||||
every { fileCache.getDirectory(path) } returns cachedDirectory
|
||||
|
||||
setup_memoryCacheMock_set()
|
||||
|
||||
val itemPath = FilePath("${path.value}/item")
|
||||
|
||||
// Act
|
||||
fileCache.removeItemFromDirectory(path, itemPath)
|
||||
|
||||
// Assert
|
||||
verify(exactly = 0) {
|
||||
memoryCacheMock[path.value] = any()
|
||||
}
|
||||
confirmVerified(memoryCacheMock)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeItemFromDirectory_notADirectory_doesNothing() {
|
||||
// Arrange
|
||||
every { fileCache.directoryExists(path) } returns true
|
||||
every { fileCache.getDirectory(path) } returns null
|
||||
|
||||
setup_memoryCacheMock_set()
|
||||
|
||||
val itemPath = FilePath("${path.value}/item")
|
||||
|
||||
// Act
|
||||
fileCache.removeItemFromDirectory(path, itemPath)
|
||||
|
||||
// Assert
|
||||
verify(exactly = 0) {
|
||||
memoryCacheMock[path.value] = any()
|
||||
}
|
||||
confirmVerified(memoryCacheMock)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue