Skip to content

Commit

Permalink
�feat: 분산락 타임아웃 상황에서 트랜잭션 롤백 문제 해결 (#345)
Browse files Browse the repository at this point in the history
* feat: TransactionalTimeout 어노테이션 추가

* feat: 트랜잭션 타임아웃 어노테이션이 붙은 경우 처리 로직 추가

* feat: 분산락을 사용하는 서비스 로직에 대해 타임아웃을 활용하도록 어노테이션 수정

* fix: DistributedLock이 풀리는 범위가 잘못되던 오류 수정

* fix: 트랜잭션 타임아웃 AOP 문제 수정

* refactor: git test run에서 info가 찍히지 않게 수정
  • Loading branch information
chhs2131 authored Aug 29, 2024
1 parent a3b2f29 commit 5ffb478
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test_run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ jobs:
cache: gradle

- name: Run tests
run: ./gradlew clean test --info
run: ./gradlew clean test

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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("처리할 수 없습니다.");
}
});
}

}

0 comments on commit 5ffb478

Please sign in to comment.