From c180005cf176fa03f2962c9b45bc68f6cd45733b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horv=C3=A1th=20Istv=C3=A1n?= Date: Thu, 7 Nov 2024 13:33:47 +0100 Subject: [PATCH] Set the concurrency control stricter when processing transactions --- backend/build.gradle.kts | 5 ++--- .../kotlin/hu/bme/sch/kir_pay/KirPayApplication.kt | 2 ++ .../bme/sch/kir_pay/account/AccountBalanceService.kt | 10 +++++++++- .../hu/bme/sch/kir_pay/account/AccountService.kt | 4 ++++ .../hu/bme/sch/kir_pay/common/RetryTransaction.kt | 12 ++++++++++++ .../kotlin/hu/bme/sch/kir_pay/order/OrderService.kt | 4 ++++ .../sch/kir_pay/principal/PrincipalLastUseUpdater.kt | 6 ++---- .../hu/bme/sch/kir_pay/principal/PrincipalService.kt | 2 +- 8 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 backend/src/main/kotlin/hu/bme/sch/kir_pay/common/RetryTransaction.kt diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 5e9cbda..6a606fa 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -24,14 +24,13 @@ repositories { mavenCentral() } -extra["springModulithVersion"] = "1.2.5" - dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jdbc") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.springframework.retry:spring-retry") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-csv") implementation("org.jetbrains.kotlin:kotlin-reflect") @@ -45,7 +44,7 @@ dependencies { dependencyManagement { imports { - mavenBom("org.springframework.modulith:spring-modulith-bom:${property("springModulithVersion")}") + mavenBom("org.springframework.modulith:spring-modulith-bom:1.2.5") } } diff --git a/backend/src/main/kotlin/hu/bme/sch/kir_pay/KirPayApplication.kt b/backend/src/main/kotlin/hu/bme/sch/kir_pay/KirPayApplication.kt index 5a23e9c..912199b 100644 --- a/backend/src/main/kotlin/hu/bme/sch/kir_pay/KirPayApplication.kt +++ b/backend/src/main/kotlin/hu/bme/sch/kir_pay/KirPayApplication.kt @@ -3,9 +3,11 @@ package hu.bme.sch.kir_pay import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication +import org.springframework.retry.annotation.EnableRetry import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity @SpringBootApplication +@EnableRetry @EnableMethodSecurity @ConfigurationPropertiesScan class KirPayApplication diff --git a/backend/src/main/kotlin/hu/bme/sch/kir_pay/account/AccountBalanceService.kt b/backend/src/main/kotlin/hu/bme/sch/kir_pay/account/AccountBalanceService.kt index 987a39e..63dd8a7 100644 --- a/backend/src/main/kotlin/hu/bme/sch/kir_pay/account/AccountBalanceService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/kir_pay/account/AccountBalanceService.kt @@ -1,20 +1,24 @@ package hu.bme.sch.kir_pay.account import hu.bme.sch.kir_pay.common.BadRequestException +import hu.bme.sch.kir_pay.common.RetryTransaction import hu.bme.sch.kir_pay.principal.getLoggedInPrincipal import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Isolation import org.springframework.transaction.annotation.Transactional import java.time.Clock @Service -@Transactional +@Transactional(isolation = Isolation.SERIALIZABLE) class AccountBalanceService( private val accountRepository: AccountRepository, private val events: ApplicationEventPublisher, private val clock: Clock ) { + @RetryTransaction + @Transactional(isolation = Isolation.SERIALIZABLE) fun pay(card: String, amount: Long, logEvent: Boolean): Account { if (amount < 0) throw BadRequestException("Helytelen argumentum!") val account = accountRepository.findActiveAccountByCard(card) @@ -43,6 +47,8 @@ class AccountBalanceService( } + @RetryTransaction + @Transactional(isolation = Isolation.SERIALIZABLE) fun upload(card: String, amount: Long): Account { if (amount < 0) throw BadRequestException("Helytelen argumentum!") val account = accountRepository.findActiveAccountByCard(card) @@ -56,6 +62,8 @@ class AccountBalanceService( // Returns the sender account + @RetryTransaction + @Transactional(isolation = Isolation.SERIALIZABLE) fun transfer(senderCard: String, recipientCard: String, amount: Long): Account { if (amount < 0) throw BadRequestException("Helytelen argumentum!") diff --git a/backend/src/main/kotlin/hu/bme/sch/kir_pay/account/AccountService.kt b/backend/src/main/kotlin/hu/bme/sch/kir_pay/account/AccountService.kt index 414bfd8..990f3ac 100644 --- a/backend/src/main/kotlin/hu/bme/sch/kir_pay/account/AccountService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/kir_pay/account/AccountService.kt @@ -2,9 +2,11 @@ package hu.bme.sch.kir_pay.account import hu.bme.sch.kir_pay.common.BadRequestException import hu.bme.sch.kir_pay.common.NotFoundException +import hu.bme.sch.kir_pay.common.RetryTransaction import hu.bme.sch.kir_pay.principal.getLoggedInPrincipal import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Isolation import org.springframework.transaction.annotation.Transactional import java.time.Clock @@ -59,6 +61,8 @@ class AccountService( accountRepository.findActiveAccountByCard(card) ?: throw NotFoundException("A kártyához nincs számla rendelve!") + @RetryTransaction + @Transactional(isolation = Isolation.SERIALIZABLE) fun assignCard(accountId: Int, card: String): Account { val account = accountRepository.findById(accountId).orElseThrow { BadRequestException("A számla nem található!") } if (account.card == card) return account diff --git a/backend/src/main/kotlin/hu/bme/sch/kir_pay/common/RetryTransaction.kt b/backend/src/main/kotlin/hu/bme/sch/kir_pay/common/RetryTransaction.kt new file mode 100644 index 0000000..84dcee7 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/kir_pay/common/RetryTransaction.kt @@ -0,0 +1,12 @@ +package hu.bme.sch.kir_pay.common + +import org.springframework.retry.annotation.Backoff +import org.springframework.retry.annotation.Retryable +import java.sql.SQLException + +@Retryable( + retryFor = [SQLException::class], + maxAttempts = 5, + backoff = Backoff(delay = 200, maxDelay = 750, multiplier = 1.5, random = true), +) +annotation class RetryTransaction diff --git a/backend/src/main/kotlin/hu/bme/sch/kir_pay/order/OrderService.kt b/backend/src/main/kotlin/hu/bme/sch/kir_pay/order/OrderService.kt index b7a03b5..ef89b9f 100644 --- a/backend/src/main/kotlin/hu/bme/sch/kir_pay/order/OrderService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/kir_pay/order/OrderService.kt @@ -1,9 +1,11 @@ package hu.bme.sch.kir_pay.order import hu.bme.sch.kir_pay.account.AccountService +import hu.bme.sch.kir_pay.common.RetryTransaction import hu.bme.sch.kir_pay.principal.getLoggedInPrincipal import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Isolation import org.springframework.transaction.annotation.Transactional import java.time.Clock @@ -32,6 +34,8 @@ class OrderService( orderRepository.findAllOrderWithOrderLinesOrderByTimestampDescPaginated(page.toLong() * size, size) + @RetryTransaction + @Transactional(isolation = Isolation.SERIALIZABLE) fun checkout(card: String, dto: OrderTerminalController.CheckoutDto) { val order = newOrder(card) events.publishEvent(OrderCreatedEvent(order.id, order.accountId, getLoggedInPrincipal(), clock.millis())) diff --git a/backend/src/main/kotlin/hu/bme/sch/kir_pay/principal/PrincipalLastUseUpdater.kt b/backend/src/main/kotlin/hu/bme/sch/kir_pay/principal/PrincipalLastUseUpdater.kt index 2d94c2d..8683a5f 100644 --- a/backend/src/main/kotlin/hu/bme/sch/kir_pay/principal/PrincipalLastUseUpdater.kt +++ b/backend/src/main/kotlin/hu/bme/sch/kir_pay/principal/PrincipalLastUseUpdater.kt @@ -1,14 +1,12 @@ package hu.bme.sch.kir_pay.principal import org.springframework.context.annotation.Configuration -import org.springframework.context.event.EventListener -import org.springframework.transaction.annotation.Transactional +import org.springframework.modulith.events.ApplicationModuleListener @Configuration class PrincipalLastUseUpdater(private val principalService: PrincipalService) { - @EventListener - @Transactional + @ApplicationModuleListener fun on(event: PrincipalAuthenticatedEvent) { principalService.updateLastUsed(event.principal.id) } diff --git a/backend/src/main/kotlin/hu/bme/sch/kir_pay/principal/PrincipalService.kt b/backend/src/main/kotlin/hu/bme/sch/kir_pay/principal/PrincipalService.kt index d2fe969..27ebc82 100644 --- a/backend/src/main/kotlin/hu/bme/sch/kir_pay/principal/PrincipalService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/kir_pay/principal/PrincipalService.kt @@ -67,7 +67,7 @@ class PrincipalService( fun setEnabled(id: Int, enabled: Boolean): Principal { val principal = find(id) - if (principal.role == Role.ADMIN && !enabled) throw BadRequestException("Admins nem lehet letiltani") + if (principal.role == Role.ADMIN && !enabled) throw BadRequestException("Admint nem lehet letiltani") val newPrincipal = principalRepository.save(principal.copy(active = enabled)) events.publishEvent(PrincipalUpdatedEvent(newPrincipal, getLoggedInPrincipal(), clock.millis())) return newPrincipal