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("처리할 수 없습니다."); + } + }); + } + +}