diff --git a/compose/docker-compose-dev.yml b/compose/docker-compose-dev.yml index 0595c50a73e..0d817aa4ad0 100644 --- a/compose/docker-compose-dev.yml +++ b/compose/docker-compose-dev.yml @@ -12,6 +12,12 @@ services: ports: - "${POSTGRES_PORT:-5432}:5432" + redis: + image: redis/redis-stack:6.2.4-v3 + ports: + - "6379:6379" + - "8001:8001" + elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.17.4 ports: diff --git a/ontrack-docs/src/docs/asciidoc/configuration-properties.adoc b/ontrack-docs/src/docs/asciidoc/configuration-properties.adoc index 360ad2d2a34..6e485dfdcb9 100644 --- a/ontrack-docs/src/docs/asciidoc/configuration-properties.adoc +++ b/ontrack-docs/src/docs/asciidoc/configuration-properties.adoc @@ -89,12 +89,6 @@ ontrack.config.key-store = file # Optional. If not filled in, will use a subdirectory of the working directory # ontrack.config.file-key-store.directory = -# Cache configuration -# Caffeine spec strings per cache type -# See http://static.javadoc.io/com.github.ben-manes.caffeine/caffeine/2.6.0/com/github/benmanes/caffeine/cache/CaffeineSpec.html -# For example, for the `properties` cache: -ontrack.config.cache.specs.properties = maximumSize=1000,expireAfterWrite=1d,recordStats - ################################# # CasC properties ################################# diff --git a/ontrack-extension-api/src/main/java/net/nemerosa/ontrack/extension/api/CacheConfigExtension.kt b/ontrack-extension-api/src/main/java/net/nemerosa/ontrack/extension/api/CacheConfigExtension.kt index 83be8a3c04a..67b743b76b9 100644 --- a/ontrack-extension-api/src/main/java/net/nemerosa/ontrack/extension/api/CacheConfigExtension.kt +++ b/ontrack-extension-api/src/main/java/net/nemerosa/ontrack/extension/api/CacheConfigExtension.kt @@ -6,9 +6,6 @@ package net.nemerosa.ontrack.extension.api interface CacheConfigExtension { /** * List of configurable caches. - * - * Returns a map whose key is the cache name, and the value is a - * `com.github.benmanes.caffeine.cache.CaffeineSpec` string specification. */ - val caches: Map + val caches: Map } \ No newline at end of file diff --git a/ontrack-extension-api/src/main/java/net/nemerosa/ontrack/extension/api/CacheConfigExtensionData.kt b/ontrack-extension-api/src/main/java/net/nemerosa/ontrack/extension/api/CacheConfigExtensionData.kt new file mode 100644 index 00000000000..1df98168e85 --- /dev/null +++ b/ontrack-extension-api/src/main/java/net/nemerosa/ontrack/extension/api/CacheConfigExtensionData.kt @@ -0,0 +1,12 @@ +package net.nemerosa.ontrack.extension.api + +import java.time.Duration + +/** + * Cache definition + * + * @property ttl TTL for a given entry in a cache + */ +data class CacheConfigExtensionData( + val ttl: Duration, +) \ No newline at end of file diff --git a/ontrack-extension-git/src/main/java/net/nemerosa/ontrack/extension/git/GitCacheConfigExtension.kt b/ontrack-extension-git/src/main/java/net/nemerosa/ontrack/extension/git/GitCacheConfigExtension.kt index df930f2445b..9c020eaed2d 100644 --- a/ontrack-extension-git/src/main/java/net/nemerosa/ontrack/extension/git/GitCacheConfigExtension.kt +++ b/ontrack-extension-git/src/main/java/net/nemerosa/ontrack/extension/git/GitCacheConfigExtension.kt @@ -1,7 +1,9 @@ package net.nemerosa.ontrack.extension.git import net.nemerosa.ontrack.extension.api.CacheConfigExtension +import net.nemerosa.ontrack.extension.api.CacheConfigExtensionData import org.springframework.stereotype.Component +import java.time.Duration /** * Configuration of caching for the Git module @@ -10,7 +12,9 @@ import org.springframework.stereotype.Component class GitCacheConfigExtension( gitConfigProperties: GitConfigProperties, ) : CacheConfigExtension { - override val caches: Map = mapOf( - CACHE_GIT_CHANGE_LOG to "maximumSize=20,expireAfterWrite=10m,recordStats", + override val caches: Map = mapOf( + CACHE_GIT_CHANGE_LOG to CacheConfigExtensionData( + ttl = Duration.ofMinutes(10) + ) ) } diff --git a/ontrack-model/src/main/java/net/nemerosa/ontrack/model/support/OntrackConfigProperties.kt b/ontrack-model/src/main/java/net/nemerosa/ontrack/model/support/OntrackConfigProperties.kt index 0c27e6271e6..fdc688f296b 100644 --- a/ontrack-model/src/main/java/net/nemerosa/ontrack/model/support/OntrackConfigProperties.kt +++ b/ontrack-model/src/main/java/net/nemerosa/ontrack/model/support/OntrackConfigProperties.kt @@ -176,33 +176,6 @@ class OntrackConfigProperties { * Allows the token to be used as passwords. */ var password: Boolean = true - - /** - * Cache properties - */ - var cache = TokensCacheProperties() - } - - /** - * Token cache properties - */ - class TokensCacheProperties { - /** - * Is caching of the tokens enabled? - */ - var enabled = true - - /** - * Cache validity period - */ - @DurationUnit(ChronoUnit.MINUTES) - var validity: Duration = Duration.ofDays(30) - - /** - * Maximum number of items in the cache. Should be aligned with the - * number of sessions. Note that the objects stored in the cache are tiny. - */ - var maxCount: Long = 1_000 } companion object { diff --git a/ontrack-service/build.gradle.kts b/ontrack-service/build.gradle.kts index 015415ac7de..44c7c06589c 100644 --- a/ontrack-service/build.gradle.kts +++ b/ontrack-service/build.gradle.kts @@ -6,6 +6,7 @@ plugins { dependencies { implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-cache") + implementation("org.springframework.boot:spring-boot-starter-data-redis") implementation(project(":ontrack-model")) implementation(project(":ontrack-repository")) implementation(project(":ontrack-extension-api")) @@ -17,7 +18,6 @@ dependencies { implementation("commons-io:commons-io") implementation("org.apache.commons:commons-lang3") implementation("org.jgrapht:jgrapht-core") - implementation("com.github.ben-manes.caffeine:caffeine") implementation("org.elasticsearch.client:elasticsearch-rest-high-level-client") implementation("org.flywaydb:flyway-core") diff --git a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/CacheConfig.kt b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/CacheConfig.kt index dfd38b7b3c4..f4218738dcb 100644 --- a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/CacheConfig.kt +++ b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/CacheConfig.kt @@ -1,50 +1,8 @@ package net.nemerosa.ontrack.service -import com.github.benmanes.caffeine.cache.Caffeine -import net.nemerosa.ontrack.common.Caches -import net.nemerosa.ontrack.extension.api.CacheConfigExtension -import org.springframework.cache.CacheManager import org.springframework.cache.annotation.EnableCaching -import org.springframework.cache.caffeine.CaffeineCache -import org.springframework.cache.support.SimpleCacheManager -import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import java.util.concurrent.TimeUnit @Configuration @EnableCaching -class CacheConfig( - private val cacheConfigProperties: CacheConfigProperties, - private val cacheConfigExtensions: List -) { - @Bean - fun cacheManager(): CacheManager { - val manager = SimpleCacheManager() - - manager.setCaches( - // Built in caches - listOf( - // Cache for settings - CaffeineCache( - Caches.SETTINGS, - Caffeine.newBuilder() - .maximumSize(1) - .expireAfterWrite(10, TimeUnit.HOURS) - .build() - ) - ) + cacheConfigExtensions.flatMap { - it.caches.map { (name, spec) -> toCache(name, spec) } - } - ) - - return manager - } - - private fun toCache(name: String, defaultSpec: String) = CaffeineCache( - name, - Caffeine.from( - cacheConfigProperties.specs[name] ?: defaultSpec - ).build() - ) - -} \ No newline at end of file +class CacheConfig \ No newline at end of file diff --git a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/CacheConfigCustomizer.kt b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/CacheConfigCustomizer.kt new file mode 100644 index 00000000000..f96dc1c5ffe --- /dev/null +++ b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/CacheConfigCustomizer.kt @@ -0,0 +1,33 @@ +package net.nemerosa.ontrack.service + +import net.nemerosa.ontrack.common.Caches +import net.nemerosa.ontrack.extension.api.CacheConfigExtension +import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer +import org.springframework.data.redis.cache.RedisCacheConfiguration +import org.springframework.data.redis.cache.RedisCacheManager.RedisCacheManagerBuilder +import org.springframework.stereotype.Component +import java.time.Duration + +@Component +class CacheConfigCustomizer( + private val cacheConfigExtensions: List, +) : RedisCacheManagerBuilderCustomizer { + override fun customize(builder: RedisCacheManagerBuilder) { + // Core caches + builder.withCacheConfiguration( + Caches.SETTINGS, + RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofHours(1)) + ) + // Extensions + cacheConfigExtensions.forEach { extension -> + extension.caches.forEach { (name, config) -> + builder.withCacheConfiguration( + name, + RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(config.ttl) + ) + } + } + } +} \ No newline at end of file diff --git a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/CacheConfigProperties.kt b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/CacheConfigProperties.kt deleted file mode 100644 index 675ff7ba257..00000000000 --- a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/CacheConfigProperties.kt +++ /dev/null @@ -1,13 +0,0 @@ -package net.nemerosa.ontrack.service - -import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.stereotype.Component - -@Component -@ConfigurationProperties(prefix = "ontrack.config.cache") -class CacheConfigProperties { - /** - * Caffeine specifications - */ - var specs = mutableMapOf() -} diff --git a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/CoreCacheConfigExtension.kt b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/CoreCacheConfigExtension.kt deleted file mode 100644 index 2e5d11c62c0..00000000000 --- a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/CoreCacheConfigExtension.kt +++ /dev/null @@ -1,15 +0,0 @@ -package net.nemerosa.ontrack.service - -import net.nemerosa.ontrack.extension.api.CacheConfigExtension -import org.springframework.stereotype.Component - -/** - * List of core caches. - */ -@Component -class CoreCacheConfigExtension : CacheConfigExtension { - override val caches: Map - get() = mapOf( - "properties" to "maximumSize=1000,expireAfterWrite=1d,recordStats" - ) -} diff --git a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/security/TokensServiceImpl.kt b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/security/TokensServiceImpl.kt index d7f81394610..9c910ac2bef 100644 --- a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/security/TokensServiceImpl.kt +++ b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/security/TokensServiceImpl.kt @@ -1,7 +1,5 @@ package net.nemerosa.ontrack.service.security -import com.github.benmanes.caffeine.cache.Cache -import com.github.benmanes.caffeine.cache.Caffeine import net.nemerosa.ontrack.common.Time import net.nemerosa.ontrack.model.security.Account import net.nemerosa.ontrack.model.security.AccountManagement @@ -17,18 +15,13 @@ import java.time.Duration @Service @Transactional class TokensServiceImpl( - private val tokensRepository: TokensRepository, - private val securityService: SecurityService, - private val tokenGenerator: TokenGenerator, - private val ontrackConfigProperties: OntrackConfigProperties, - private val accountService: AccountService + private val tokensRepository: TokensRepository, + private val securityService: SecurityService, + private val tokenGenerator: TokenGenerator, + private val ontrackConfigProperties: OntrackConfigProperties, + private val accountService: AccountService, ) : TokensService { - private val cache: Cache = Caffeine.newBuilder() - .maximumSize(ontrackConfigProperties.security.tokens.cache.maxCount) - .expireAfterAccess(ontrackConfigProperties.security.tokens.cache.validity) - .build() - override val currentToken: Token? get() { // Gets the current account @@ -42,9 +35,9 @@ class TokensServiceImpl( override fun generateNewToken(): Token { // Gets the current account val account = securityService.currentAccount?.account - ?: throw TokenGenerationNoAccountException() + ?: throw TokenGenerationNoAccountException() // Generates a new token - return securityService.asAdmin { generateToken(account.id(), null, false) } + return securityService.asAdmin { generateToken(account.id(), null, false) } } override fun generateToken(accountId: Int, validity: Duration?, forceUnlimited: Boolean): Token { @@ -71,9 +64,7 @@ class TokensServiceImpl( val account = securityService.currentAccount?.account // Revokes its token account?.apply { - val token = tokensRepository.invalidate(id()) - // Removes any cache token - token?.let { cache.invalidate(token) } + tokensRepository.invalidate(id()) } } @@ -87,50 +78,37 @@ class TokensServiceImpl( } override fun isValid(token: String): Boolean { - if (ontrackConfigProperties.security.tokens.cache.enabled) { - val valid = cache.getIfPresent(token) - return if (valid != null) { - valid - } else { - val stillValid = internalValidityCheck(token) - cache.put(token, stillValid) - return stillValid - } - } else { - return internalValidityCheck(token) - } + return internalValidityCheck(token) } private fun internalValidityCheck(token: String): Boolean = - tokensRepository - .findAccountByToken(token) - ?.let { (_, result) -> - result.isValid() - } - ?: false + tokensRepository + .findAccountByToken(token) + ?.let { (_, result) -> + result.isValid() + } + ?: false override fun findAccountByToken(token: String): TokenAccount? { // Find the account ID val result = tokensRepository.findAccountByToken(token) return result?.let { (accountId, token) -> TokenAccount( - securityService.asAdmin { - accountService.getAccount(ID.of(accountId)) - }, - token + securityService.asAdmin { + accountService.getAccount(ID.of(accountId)) + }, + token ) } } override fun revokeAll(): Int { securityService.checkGlobalFunction(AccountManagement::class.java) - cache.invalidateAll() return tokensRepository.revokeAll() } override fun revokeToken(accountId: Int) { securityService.checkGlobalFunction(AccountManagement::class.java) - val token = tokensRepository.invalidate(accountId) - token?.let { cache.invalidate(token) } + tokensRepository.invalidate(accountId) } } \ No newline at end of file diff --git a/ontrack-service/src/test/java/net/nemerosa/ontrack/service/security/TokensServiceIT.kt b/ontrack-service/src/test/java/net/nemerosa/ontrack/service/security/TokensServiceIT.kt index a893671033c..d391a895004 100644 --- a/ontrack-service/src/test/java/net/nemerosa/ontrack/service/security/TokensServiceIT.kt +++ b/ontrack-service/src/test/java/net/nemerosa/ontrack/service/security/TokensServiceIT.kt @@ -1,18 +1,18 @@ package net.nemerosa.ontrack.service.security import net.nemerosa.ontrack.common.Time -import net.nemerosa.ontrack.it.AbstractDSLTestJUnit4Support +import net.nemerosa.ontrack.it.AbstractDSLTestSupport import net.nemerosa.ontrack.model.security.Account import net.nemerosa.ontrack.model.security.AccountManagement import net.nemerosa.ontrack.model.structure.ID import net.nemerosa.ontrack.model.structure.TokensService -import org.junit.Test +import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import java.time.Duration import java.util.* import kotlin.test.* -class TokensServiceIT : AbstractDSLTestJUnit4Support() { +class TokensServiceIT : AbstractDSLTestSupport() { @Autowired private lateinit var tokensService: TokensService @@ -163,34 +163,6 @@ class TokensServiceIT : AbstractDSLTestJUnit4Support() { } } - @Test - fun `Checking the validity of a token with cache not enabled`() { - withCustomTokenCache(false) { - asUser { - val id = securityService.currentAccount!!.id() - val token = tokensService.generateNewToken() - assertTrue(tokensService.isValid(token.value), "Token is valid") - asAdmin { tokensService.revokeToken(id) } - assertFalse(tokensService.isValid(token.value), "Token has been revoked") - assertFalse(tokensService.isValid(token.value), "Token has been revoked") - } - } - } - - @Test - fun `Checking the validity of a token with cache enabled`() { - withCustomTokenCache(true) { - asUser { - val id = securityService.currentAccount!!.id() - val token = tokensService.generateNewToken() - assertTrue(tokensService.isValid(token.value), "Token is valid") - asAdmin { tokensService.revokeToken(id) } - assertFalse(tokensService.isValid(token.value), "Token has been revoked") - assertFalse(tokensService.isValid(token.value), "Token has been revoked") - } - } - } - @Test fun `Changing the validity of a token to a shorter one with unlimited defaults`() { asUser { @@ -252,14 +224,4 @@ class TokensServiceIT : AbstractDSLTestJUnit4Support() { } } - private fun withCustomTokenCache(enabled: Boolean, code: () -> T): T { - val old = ontrackConfigProperties.security.tokens.cache.enabled - return try { - ontrackConfigProperties.security.tokens.cache.enabled = enabled - code() - } finally { - ontrackConfigProperties.security.tokens.cache.enabled = old - } - } - } \ No newline at end of file