#18 Use a proper memory cache
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details

This commit is contained in:
FyloZ 2022-02-12 14:48:58 -05:00
parent c42fc26a92
commit 9ed081dc92
Signed by: william
GPG Key ID: 835378AE9AF4AE97
7 changed files with 452 additions and 22 deletions

View File

@ -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 {

View File

@ -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)
}

View File

@ -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")

View File

@ -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")
}

View File

@ -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) =

View File

@ -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

View File

@ -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)
}
}