From 5ffb478c31c72b7ec50c8dc963a5bebc7388912b Mon Sep 17 00:00:00 2001 From: HyeonSik Choi Date: Thu, 29 Aug 2024 18:05:56 +0900 Subject: [PATCH] =?UTF-8?q?=08feat:=20=EB=B6=84=EC=82=B0=EB=9D=BD=20?= =?UTF-8?q?=ED=83=80=EC=9E=84=EC=95=84=EC=9B=83=20=EC=83=81=ED=99=A9?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20?= =?UTF-8?q?=EB=A1=A4=EB=B0=B1=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20?= =?UTF-8?q?(#345)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: TransactionalTimeout 어노테이션 추가 * feat: 트랜잭션 타임아웃 어노테이션이 붙은 경우 처리 로직 추가 * feat: 분산락을 사용하는 서비스 로직에 대해 타임아웃을 활용하도록 어노테이션 수정 * fix: DistributedLock이 풀리는 범위가 잘못되던 오류 수정 * fix: 트랜잭션 타임아웃 AOP 문제 수정 * refactor: git test run에서 info가 찍히지 않게 수정 --- .github/workflows/test_run.yml | 2 +- .../service/auctioneer/BasicAuctioneer.java | 3 +- .../core/payment/service/PaymentService.java | 3 +- .../global/aop/DistributedLockAspect.java | 2 +- .../global/aop/TransactionalTimeout.java | 12 ++++ .../aop/TransactionalTimeoutAspect.java | 56 +++++++++++++++++++ 6 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/wootecam/luckyvickyauction/global/aop/TransactionalTimeout.java create mode 100644 src/main/java/com/wootecam/luckyvickyauction/global/aop/TransactionalTimeoutAspect.java diff --git a/.github/workflows/test_run.yml b/.github/workflows/test_run.yml index b7b6716e..b7cdadd8 100644 --- a/.github/workflows/test_run.yml +++ b/.github/workflows/test_run.yml @@ -27,5 +27,5 @@ jobs: cache: gradle - name: Run tests - run: ./gradlew clean test --info + run: ./gradlew clean test diff --git a/src/main/java/com/wootecam/luckyvickyauction/core/auction/service/auctioneer/BasicAuctioneer.java b/src/main/java/com/wootecam/luckyvickyauction/core/auction/service/auctioneer/BasicAuctioneer.java index f5ad726f..f8b1000c 100644 --- a/src/main/java/com/wootecam/luckyvickyauction/core/auction/service/auctioneer/BasicAuctioneer.java +++ b/src/main/java/com/wootecam/luckyvickyauction/core/auction/service/auctioneer/BasicAuctioneer.java @@ -10,6 +10,7 @@ import com.wootecam.luckyvickyauction.core.payment.domain.ReceiptStatus; import com.wootecam.luckyvickyauction.core.payment.service.PaymentService; import com.wootecam.luckyvickyauction.global.aop.DistributedLock; +import com.wootecam.luckyvickyauction.global.aop.TransactionalTimeout; import com.wootecam.luckyvickyauction.global.dto.AuctionPurchaseRequestMessage; import com.wootecam.luckyvickyauction.global.dto.AuctionRefundRequestMessage; import com.wootecam.luckyvickyauction.global.exception.AuthorizationException; @@ -38,7 +39,7 @@ public class BasicAuctioneer implements Auctioneer { * 성공하면 -> Receipt 저장 및 구매자, 판매자 업데이트 적용 */ @Override - @Transactional + @TransactionalTimeout @Timed("purchase_process_time") @DistributedLock("#message.auctionId + ':auction:lock'") public void process(AuctionPurchaseRequestMessage message) { diff --git a/src/main/java/com/wootecam/luckyvickyauction/core/payment/service/PaymentService.java b/src/main/java/com/wootecam/luckyvickyauction/core/payment/service/PaymentService.java index c93dde24..469468da 100644 --- a/src/main/java/com/wootecam/luckyvickyauction/core/payment/service/PaymentService.java +++ b/src/main/java/com/wootecam/luckyvickyauction/core/payment/service/PaymentService.java @@ -4,6 +4,7 @@ import com.wootecam.luckyvickyauction.core.member.domain.MemberRepository; import com.wootecam.luckyvickyauction.core.member.dto.SignInInfo; import com.wootecam.luckyvickyauction.global.aop.DistributedLock; +import com.wootecam.luckyvickyauction.global.aop.TransactionalTimeout; import com.wootecam.luckyvickyauction.global.exception.BadRequestException; import com.wootecam.luckyvickyauction.global.exception.ErrorCode; import com.wootecam.luckyvickyauction.global.exception.NotFoundException; @@ -31,7 +32,7 @@ public void chargePoint(SignInInfo memberInfo, long chargePoint) { memberRepository.save(member); } - @Transactional + @TransactionalTimeout @DistributedLock("#recipientId + ':point:lock'") public void pointTransfer(long senderId, long recipientId, long amount) { Member sender = findMemberObject(senderId); diff --git a/src/main/java/com/wootecam/luckyvickyauction/global/aop/DistributedLockAspect.java b/src/main/java/com/wootecam/luckyvickyauction/global/aop/DistributedLockAspect.java index e2a8ff0b..4cabb813 100644 --- a/src/main/java/com/wootecam/luckyvickyauction/global/aop/DistributedLockAspect.java +++ b/src/main/java/com/wootecam/luckyvickyauction/global/aop/DistributedLockAspect.java @@ -23,8 +23,8 @@ public class DistributedLockAspect { public Object around(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable { String key = getLockName(joinPoint, distributedLock); + lockProvider.tryLock(key); try { - lockProvider.tryLock(key); return joinPoint.proceed(); } finally { lockProvider.unlock(key); diff --git a/src/main/java/com/wootecam/luckyvickyauction/global/aop/TransactionalTimeout.java b/src/main/java/com/wootecam/luckyvickyauction/global/aop/TransactionalTimeout.java new file mode 100644 index 00000000..45c2aec5 --- /dev/null +++ b/src/main/java/com/wootecam/luckyvickyauction/global/aop/TransactionalTimeout.java @@ -0,0 +1,12 @@ +package com.wootecam.luckyvickyauction.global.aop; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface TransactionalTimeout { + // int timeoutMillis() default 60000; +} diff --git a/src/main/java/com/wootecam/luckyvickyauction/global/aop/TransactionalTimeoutAspect.java b/src/main/java/com/wootecam/luckyvickyauction/global/aop/TransactionalTimeoutAspect.java new file mode 100644 index 00000000..f2b846e2 --- /dev/null +++ b/src/main/java/com/wootecam/luckyvickyauction/global/aop/TransactionalTimeoutAspect.java @@ -0,0 +1,56 @@ +package com.wootecam.luckyvickyauction.global.aop; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionTemplate; + +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +@Order(2) +public class TransactionalTimeoutAspect { + + private static final int TIMEOUT_MARGIN = 100; + private final TransactionTemplate transactionTemplate; + + @Value("${lock.redisson.lease_time: 500}") + private int leaseTime; // TODO: [시간을 초기화하고 보관하는 Bean을 별도로 만들어 관리하기] [writeAt: 2024/08/29/17:27] [writeBy: chhs2131] + + @Around("@annotation(transactionalTimeout)") + public Object handleCustomTransaction(ProceedingJoinPoint joinPoint, TransactionalTimeout transactionalTimeout) { + long startTime = System.currentTimeMillis(); + long timeoutMillis = leaseTime - TIMEOUT_MARGIN; + + return transactionTemplate.execute((TransactionStatus status) -> { + try { + Object result = joinPoint.proceed(); + long elapsedTime = System.currentTimeMillis() - startTime; + + if (elapsedTime > timeoutMillis) { + log.debug("트랜잭션 타임아웃을 초과했습니다. 초과시간: {}ms", elapsedTime - timeoutMillis); + status.setRollbackOnly(); + throw new RuntimeException( + "Transaction timed out after " + elapsedTime + " ms. Timeout was set to " + timeoutMillis + + " ms."); + } + + return result; // 정상 수행한 결과 반환 + } catch (RuntimeException ex) { + status.setRollbackOnly(); + throw ex; + } catch (Throwable e) { + log.error("message={}", e.getMessage(), e); + throw new RuntimeException("처리할 수 없습니다."); + } + }); + } + +}