From 74a9ba2ca98dbc48a7fe3192e8b1ba51304e948c Mon Sep 17 00:00:00 2001 From: mrflick72 Date: Fri, 22 Nov 2024 23:18:16 +0100 Subject: [PATCH] WIP lock implementation --- local-environment/application.yml | 5 ++ .../server/job/DatabaseTtlEntryCleanJob.kt | 51 +++++++++++++++++-- .../vauthenticator/server/job/LockService.kt | 42 +++++++++++++++ .../server/keys/adapter/java/KeyInitJob.kt | 5 +- .../job/DatabaseTtlEntryCleanJobTest.kt | 36 ++++++++++--- .../server/job/RedisLockServiceTest.kt | 31 +++++++++++ 6 files changed, 158 insertions(+), 12 deletions(-) create mode 100644 src/main/kotlin/com/vauthenticator/server/job/LockService.kt create mode 100644 src/test/kotlin/com/vauthenticator/server/job/RedisLockServiceTest.kt diff --git a/local-environment/application.yml b/local-environment/application.yml index fe1a62dc..cd69ae29 100644 --- a/local-environment/application.yml +++ b/local-environment/application.yml @@ -2,6 +2,11 @@ endSessionWithoutDiscovery: true oidcEndSessionUrl: ${vauthenticator.host}/oidc/logout auth.oidcIss: ${vauthenticator.host} +scheduled: + database-cleanup: + lock-ttl: 30000 + cron: "0 * * * * *" + event: consumer: enable: diff --git a/src/main/kotlin/com/vauthenticator/server/job/DatabaseTtlEntryCleanJob.kt b/src/main/kotlin/com/vauthenticator/server/job/DatabaseTtlEntryCleanJob.kt index 59e447d2..17c54836 100644 --- a/src/main/kotlin/com/vauthenticator/server/job/DatabaseTtlEntryCleanJob.kt +++ b/src/main/kotlin/com/vauthenticator/server/job/DatabaseTtlEntryCleanJob.kt @@ -1,29 +1,53 @@ package com.vauthenticator.server.job +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile +import org.springframework.data.redis.core.RedisTemplate import org.springframework.jdbc.core.JdbcTemplate import org.springframework.jdbc.core.queryForList +import org.springframework.scheduling.annotation.EnableScheduling import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Repository import org.springframework.transaction.annotation.Transactional import java.time.Clock - @Transactional class DatabaseTtlEntryCleanJob( private val jdbcTemplate: JdbcTemplate, + private val lockTtl: Long, + private val lockService: LockService, private val clock: Clock ) { + private val logger = LoggerFactory.getLogger(DatabaseTtlEntryCleanJob::class.java) + @Scheduled(cron = "\${scheduled.database-cleanup.cron}") fun execute() { - val now = clock.instant().epochSecond + try { + logger.info("Try to Schedule") + + lockService.lock(lockTtl) + logger.info("Job Running") + val now = clock.instant().epochSecond + + deleteOldTicket(now) + deleteOldKeys(now) + logger.info("Job Completed") + } finally { + lockService.unlock() + } - deleteOldTicket(now) - deleteOldKeys(now) } private fun deleteOldKeys(now: Long) { val keysToBeDeleted = - jdbcTemplate.queryForList("SELECT key_id,key_purpose FROM KEYS WHERE key_expiration_date_timestamp < ?", now) + jdbcTemplate.queryForList( + "SELECT key_id,key_purpose FROM KEYS WHERE key_expiration_date_timestamp < ?", + now + ) keysToBeDeleted.forEach { jdbcTemplate.update( "DELETE FROM KEYS WHERE key_id = ? AND key_purpose = ?;", it["key_id"], it["key_purpose"] @@ -42,4 +66,21 @@ class DatabaseTtlEntryCleanJob( } } +} + + +@Profile("!kms") +@EnableScheduling +@Configuration(proxyBeanMethods = false) +class DatabaseTtlEntryCleanJobConfig { + + @Bean + fun databaseTtlEntryCleanJob( + @Value("\${scheduled.database-cleanup.lock-ttl}") lockTtl: Long, + lockService: LockService, + jdbcTemplate: JdbcTemplate + ) = DatabaseTtlEntryCleanJob(jdbcTemplate, lockTtl, lockService, Clock.systemUTC()) + + @Bean + fun lockService(redisTemplate: RedisTemplate) = RedisLockService(redisTemplate) } \ No newline at end of file diff --git a/src/main/kotlin/com/vauthenticator/server/job/LockService.kt b/src/main/kotlin/com/vauthenticator/server/job/LockService.kt new file mode 100644 index 00000000..9786dd05 --- /dev/null +++ b/src/main/kotlin/com/vauthenticator/server/job/LockService.kt @@ -0,0 +1,42 @@ +package com.vauthenticator.server.job + +import org.slf4j.LoggerFactory +import org.springframework.data.redis.core.RedisTemplate +import java.util.concurrent.TimeUnit + + +interface LockService { + + fun lock(timeout: Long) + + fun unlock() + +} + +class RedisLockService( + private val redisTemplate: RedisTemplate +) : LockService { + + private val logger = LoggerFactory.getLogger(RedisLockService::class.java) + + override fun lock(timeout: Long) { + if (!acquireLockWith(timeout)) { + logger.info("lock already acquired") + Thread.sleep(timeout) + } else { + logger.info("lock acquired") + } + } + + private fun acquireLockWith(timeout: Long): Boolean { + logger.info("try to acquire Lock") + return redisTemplate.opsForValue() + .setIfAbsent("lockKey", "locked", timeout, TimeUnit.MILLISECONDS)!! + } + + override fun unlock() { + logger.info("lock released") + redisTemplate.opsForValue().getAndDelete("lockKey") + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/vauthenticator/server/keys/adapter/java/KeyInitJob.kt b/src/main/kotlin/com/vauthenticator/server/keys/adapter/java/KeyInitJob.kt index 58077ddc..8ab29e21 100644 --- a/src/main/kotlin/com/vauthenticator/server/keys/adapter/java/KeyInitJob.kt +++ b/src/main/kotlin/com/vauthenticator/server/keys/adapter/java/KeyInitJob.kt @@ -1,6 +1,7 @@ package com.vauthenticator.server.keys.adapter.java import com.vauthenticator.server.keys.domain.* +import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.boot.ApplicationArguments import org.springframework.boot.ApplicationRunner @@ -15,6 +16,8 @@ class KeyInitJob( private val keyRepository: KeyRepository ) : ApplicationRunner { + val logger = LoggerFactory.getLogger(KeyInitJob::class.java) + override fun run(args: ApplicationArguments) { if (keyStorage.signatureKeys().keys.isEmpty()) { @@ -23,7 +26,7 @@ class KeyInitJob( keyPurpose = KeyPurpose.SIGNATURE, keyType = KeyType.ASYMMETRIC, ) - println(kid) + logger.info("Token Signature Key init job has been executed. Key ID generated: $kid") } } diff --git a/src/test/kotlin/com/vauthenticator/server/job/DatabaseTtlEntryCleanJobTest.kt b/src/test/kotlin/com/vauthenticator/server/job/DatabaseTtlEntryCleanJobTest.kt index 77d1f82c..591b8b88 100644 --- a/src/test/kotlin/com/vauthenticator/server/job/DatabaseTtlEntryCleanJobTest.kt +++ b/src/test/kotlin/com/vauthenticator/server/job/DatabaseTtlEntryCleanJobTest.kt @@ -2,8 +2,11 @@ package com.vauthenticator.server.job import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vauthenticator.server.keys.adapter.jdbc.JdbcKeyStorage -import com.vauthenticator.server.keys.domain.* +import com.vauthenticator.server.keys.domain.DataKey import com.vauthenticator.server.keys.domain.KeyPurpose.SIGNATURE +import com.vauthenticator.server.keys.domain.KeyType +import com.vauthenticator.server.keys.domain.Kid +import com.vauthenticator.server.keys.domain.MasterKid import com.vauthenticator.server.support.AccountTestFixture import com.vauthenticator.server.support.ClientAppFixture import com.vauthenticator.server.support.JdbcUtils.jdbcTemplate @@ -11,16 +14,27 @@ import com.vauthenticator.server.support.JdbcUtils.resetDb import com.vauthenticator.server.support.TicketFixture import com.vauthenticator.server.ticket.adapter.jdbc.JdbcTicketRepository import com.vauthenticator.server.ticket.domain.TicketId -import org.junit.jupiter.api.Assertions +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.just +import io.mockk.runs +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith import java.time.Clock import java.time.Duration import java.util.* - +@ExtendWith(MockKExtension::class) class DatabaseTtlEntryCleanJobTest { + @MockK + lateinit var lockService: LockService + @BeforeEach fun setUp() { resetDb() @@ -31,7 +45,7 @@ class DatabaseTtlEntryCleanJobTest { val ticketRepository = JdbcTicketRepository(jdbcTemplate, jacksonObjectMapper()) val keyStorage = JdbcKeyStorage(jdbcTemplate, Clock.systemDefaultZone()) - val uut = DatabaseTtlEntryCleanJob(jdbcTemplate, Clock.systemUTC()) + val uut = DatabaseTtlEntryCleanJob(jdbcTemplate, 100, lockService, Clock.systemUTC()) val kid = Kid("") val anAccount = AccountTestFixture.anAccount() @@ -47,11 +61,21 @@ class DatabaseTtlEntryCleanJobTest { ) keyStorage.keyDeleteJodPlannedFor(kid, Duration.ofSeconds(-200), SIGNATURE) + every { + lockService.lock(100) + lockService.unlock() + } just runs + uut.execute() + verify { + lockService.lock(100) + lockService.unlock() + } + val actualTicket = ticketRepository.loadFor(TicketId("A_TICKET")) - Assertions.assertTrue(actualTicket.isEmpty) - Assertions.assertThrows(NoSuchElementException::class.java) { + assertTrue(actualTicket.isEmpty) + assertThrows(NoSuchElementException::class.java) { keyStorage.findOne(kid, SIGNATURE) } } diff --git a/src/test/kotlin/com/vauthenticator/server/job/RedisLockServiceTest.kt b/src/test/kotlin/com/vauthenticator/server/job/RedisLockServiceTest.kt new file mode 100644 index 00000000..f4bad567 --- /dev/null +++ b/src/test/kotlin/com/vauthenticator/server/job/RedisLockServiceTest.kt @@ -0,0 +1,31 @@ +package com.vauthenticator.server.job + +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.data.redis.core.ValueOperations +import java.util.concurrent.TimeUnit + +@ExtendWith(MockKExtension::class) +class RedisLockServiceTest { + + @MockK + lateinit var redisTemplate: RedisTemplate + + @Test + fun `when lock and unlock`() { + val uut = RedisLockService(redisTemplate) + + every { redisTemplate.opsForValue() } returns mockk> { + every { setIfAbsent("lockKey", "locked", 100, TimeUnit.MILLISECONDS) } returns true + every { getAndDelete("lockKey") } returns "lockKey" + } + + uut.lock(100) + uut.unlock() + } +} \ No newline at end of file