From 30639b15edb9e6becabfbe7a2da288af191873df Mon Sep 17 00:00:00 2001 From: bifurcated Date: Mon, 15 Jan 2024 22:43:19 +0300 Subject: [PATCH] Fix concurrency transactional --- pom.xml | 10 ++++ .../wallet/service/WalletService.java | 13 +++++- .../controller/WalletControllerTest.java | 46 ++++++++++++++++++- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 54cab32..a246ac2 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,16 @@ 1.18.3 test + + org.springframework.retry + spring-retry + 2.0.3 + + + org.springframework + spring-aspects + 6.0.11 + diff --git a/src/main/java/com/bifurcated/wallet/service/WalletService.java b/src/main/java/com/bifurcated/wallet/service/WalletService.java index b7d5d29..7dfae18 100644 --- a/src/main/java/com/bifurcated/wallet/service/WalletService.java +++ b/src/main/java/com/bifurcated/wallet/service/WalletService.java @@ -5,9 +5,14 @@ import com.bifurcated.wallet.errors.NotEnoughMoneyError; import com.bifurcated.wallet.errors.WalletNotFoundError; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import java.sql.SQLException; import java.util.UUID; @@ -20,11 +25,15 @@ public WalletService(WalletRepo walletRepo) { this.walletRepo = walletRepo; } - @Transactional + @Transactional( + isolation = Isolation.SERIALIZABLE, + propagation = Propagation.REQUIRES_NEW) + @Retryable(retryFor = SQLException.class, maxAttempts = 17, backoff = @Backoff(delay = 500)) public Wallet addAmount(UUID id, Float amount) { var wallet = walletRepo.findById(id).orElseThrow(WalletNotFoundError::new); wallet.setAmount(wallet.getAmount() + amount); - return walletRepo.save(wallet); + Wallet save = walletRepo.save(wallet); + return save; } @Transactional diff --git a/src/test/java/com/bifurcated/wallet/controller/WalletControllerTest.java b/src/test/java/com/bifurcated/wallet/controller/WalletControllerTest.java index 6d6e762..367893a 100644 --- a/src/test/java/com/bifurcated/wallet/controller/WalletControllerTest.java +++ b/src/test/java/com/bifurcated/wallet/controller/WalletControllerTest.java @@ -6,14 +6,23 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -26,6 +35,7 @@ @AutoConfigureMockMvc @ActiveProfiles("test") class WalletControllerTest { + private static Logger logger = LoggerFactory.getLogger(WalletControllerTest.class); private static final String END_POINT_PATH = "/api/v1"; @Autowired private MockMvc mockMvc; @@ -34,7 +44,6 @@ class WalletControllerTest { @Autowired private WalletRepo repository; - @BeforeEach public void setup() { repository.deleteAll(); @@ -155,4 +164,39 @@ public void testWalletGetAmountNotFound() throws Exception { .andDo(print()) .andExpect(status().isNotFound()); } + + @Test + public void testWalletDepositDDOSAttack() throws Exception { + record WalletRequest(UUID valletId, String operationType, Float amount){} + record BalanceResponse(Float amount){} + UUID id = UUID.fromString("b3919077-79e6-4570-bfe0-980ef18f3731"); + WalletRequest walletRequest = new WalletRequest( + id, "DEPOSIT", 1000F); + + String requestBody = objectMapper.writeValueAsString(walletRequest); + + ExecutorService executorService = Executors.newFixedThreadPool(10000); + List> futures = new ArrayList<>(); + for (int i = 0; i < 300; i++) { + Callable callable = () -> { + ResultActions perform = this.mockMvc.perform(post(END_POINT_PATH + "/wallet") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)); + return perform.andExpect(status().isOk()); + }; + + Future future = executorService.submit(callable); + futures.add(future); + } + for (Future future : futures) { + future.get(); + } + + BalanceResponse balanceResponse = new BalanceResponse(302000F); + String response = objectMapper.writeValueAsString(balanceResponse); + this.mockMvc.perform(get(END_POINT_PATH+"/wallets/"+id)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().json(response)); + } } \ No newline at end of file