클러스터에서 토큰이 중복 발급되던 문제 수정
- near 캐시 사본 때문에 다른 노드가 방금 교체한 토큰을 못 보고 같은 주기에 두 번 발급되던 문제 (분산락을 잡은 뒤의 조회에서도 옛 값을 읽어 락만으로는 막지 못했음) - 캐시 조회를 짧은 비관적 트랜잭션으로 감싸 primary 값을 읽도록 함 발급 HTTP 호출은 트랜잭션 밖에 두어 장기 트랜잭션이 파티션 맵 교환을 막지 않게 함 - 재발급 판정에 validUntilTime 추가 : 요구 시각까지 유효한 토큰이 있으면 발급하지 않음 거래 중 재발급은 0 을 넘겨 만료 기준 검사를 건너뛴다 (만료 전이어도 기관이 거부한 상황) - 2노드 시험에서 주기당 발급 1회, 만료 창 0건 확인 - 단위테스트 3건 추가
This commit is contained in:
@@ -267,11 +267,12 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
|
||||
} else if (cached.getExpiration().before(new Date(intervalTime))) {
|
||||
// 다음 스케줄 전에 만료되므로 미리 갱신한다.
|
||||
// 여러 노드가 동시에 들어와도 분산락 안에서 oldToken 을 비교해 한 번만 발급된다.
|
||||
// 여러 노드가 동시에 들어와도 분산락 안에서 다시 확인해 한 번만 발급된다.
|
||||
// 다른 노드가 이미 넣어둔 토큰이 다음 틱까지 유효하면 발급하지 않는다.
|
||||
logger.debug("Token expires before next schedule : {}, expiration date: {}",
|
||||
adapterGroupName, cached.getExpiration());
|
||||
sessionManager.reissueOutboundAccessToken(adapterGroupName, cached.getAccessToken(),
|
||||
token -> issueToken(credential));
|
||||
intervalTime, token -> issueToken(credential));
|
||||
|
||||
} else {
|
||||
logger.debug(
|
||||
@@ -625,7 +626,9 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
|
||||
// 클러스터 전역에서 한 번만 발급되도록 분산락 안에서 처리한다. 발급 결과는 캐시에 반영되므로
|
||||
// 뒤따르는 거래는 재발급하지 않는다. 구현체는 호출자가 넘긴 어댑터 속성 기준으로 고른다.
|
||||
return SessionManager.getInstance().reissueOutboundAccessToken(adapterGroupName, oldToken,
|
||||
// 거부된 토큰을 바꾸는 것이 목적이므로 만료 기준 검사는 하지 않고 oldToken 비교만 한다.
|
||||
// (만료 전이어도 상대 기관이 거부한 상황이다)
|
||||
return SessionManager.getInstance().reissueOutboundAccessToken(adapterGroupName, oldToken, 0L,
|
||||
new Function<AccessTokenVO, AccessTokenVO>() {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -228,12 +228,14 @@ public abstract class SessionManager implements Lifecycle {
|
||||
* 분산락 안에서 캐시의 토큰이 oldToken 과 같은지 확인한 뒤에만 발급하므로, 여러 노드가 동시에
|
||||
* 재발급을 시도해도 실제 발급은 한 번만 일어난다. 발급에 성공하면 캐시에 반영한다.
|
||||
*
|
||||
* @param key 어댑터그룹명
|
||||
* @param oldToken 거부된 accessToken 값
|
||||
* @param tokenSupplier 실제 발급 처리
|
||||
* @param key 어댑터그룹명
|
||||
* @param oldToken 거부된(또는 갱신 대상인) accessToken 값
|
||||
* @param validUntilTime 이 시각까지 유효한 토큰이 캐시에 있으면 발급하지 않는다.
|
||||
* 0 이하면 만료 기준 검사를 하지 않고 oldToken 비교만 한다.
|
||||
* @param tokenSupplier 실제 발급 처리
|
||||
* @return 재발급된 토큰. 다른 노드가 이미 갱신했다면 그 토큰.
|
||||
*/
|
||||
public abstract AccessTokenVO reissueOutboundAccessToken(String key, String oldToken,
|
||||
public abstract AccessTokenVO reissueOutboundAccessToken(String key, String oldToken, long validUntilTime,
|
||||
Function<AccessTokenVO, AccessTokenVO> tokenSupplier);
|
||||
|
||||
public abstract void removeOutboundAccessToken(String key);
|
||||
|
||||
@@ -957,7 +957,7 @@ public class SessionManagerForEhcache extends SessionManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccessTokenVO reissueOutboundAccessToken(String key, String oldToken,
|
||||
public AccessTokenVO reissueOutboundAccessToken(String key, String oldToken, long validUntilTime,
|
||||
Function<AccessTokenVO, AccessTokenVO> tokenSupplier) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.eactive.eai.common.session;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -44,6 +45,9 @@ import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
|
||||
import org.apache.ignite.spi.discovery.tcp.ipfinder.kubernetes.TcpDiscoveryKubernetesIpFinder;
|
||||
import org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder;
|
||||
import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;
|
||||
import org.apache.ignite.transactions.Transaction;
|
||||
import org.apache.ignite.transactions.TransactionConcurrency;
|
||||
import org.apache.ignite.transactions.TransactionIsolation;
|
||||
|
||||
import com.eactive.eai.adapter.socket2.common.Env;
|
||||
import com.eactive.eai.authserver.service.BearerTokenInfo;
|
||||
@@ -837,19 +841,64 @@ public class SessionManagerForIgnite extends SessionManager {
|
||||
return cacheOutBoundAccessToken.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 아웃바운드 토큰을 primary 노드 기준으로 읽는다.
|
||||
*
|
||||
* 일반 get() 은 near 캐시 사본을 돌려줄 수 있어, 다른 노드가 방금 교체한 토큰을 놓칠 수 있다.
|
||||
* 비관적 트랜잭션 안에서 읽으면 primary 의 값을 보장받는다. 트랜잭션은 읽기 직후 바로 닫는다.
|
||||
* (발급 HTTP 호출 구간까지 열어두면 장기 트랜잭션이 되어 파티션 맵 교환을 막는다)
|
||||
*
|
||||
* @param key 어댑터그룹명
|
||||
* @return 캐시에 있는 토큰. 없으면 null.
|
||||
*/
|
||||
private AccessTokenVO readFromPrimary(String key) {
|
||||
try (Transaction tx = manager.transactions().txStart(TransactionConcurrency.PESSIMISTIC,
|
||||
TransactionIsolation.REPEATABLE_READ)) {
|
||||
AccessTokenVO token = cacheOutBoundAccessToken.get(key);
|
||||
tx.commit();
|
||||
return token;
|
||||
} catch (Throwable e) {
|
||||
logger.error("occuring exception in readFromPrimary. key=" + key, e);
|
||||
return cacheOutBoundAccessToken.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
/** 토큰으로 쓸 수 있는 값인지 (빈 토큰은 발급 실패로 본다) */
|
||||
private boolean isUsableToken(AccessTokenVO token) {
|
||||
return token != null && StringUtils.isNotBlank(token.getAccessToken());
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시의 토큰이 이미 갱신된 것이라 다시 발급할 필요가 없는지 판단한다.
|
||||
*
|
||||
* 값이 바뀌었는지(oldToken 비교)와, 요구하는 시각까지 유효한지를 함께 본다.
|
||||
* 값 비교만 하면 서로 다른 시점에 조회한 노드들이 각자 발급할 수 있다.
|
||||
*
|
||||
* @param token 캐시에 있는 토큰
|
||||
* @param oldToken 갱신 대상으로 보고 들어온 accessToken 값
|
||||
* @param validUntilTime 이 시각까지 유효하면 갱신 불필요. 0 이하면 만료 기준 검사 생략.
|
||||
* @return 갱신이 필요 없으면 true
|
||||
*/
|
||||
private boolean isAlreadyReissued(AccessTokenVO token, String oldToken, long validUntilTime) {
|
||||
if (!isUsableToken(token)) {
|
||||
return false;
|
||||
}
|
||||
if (validUntilTime > 0 && token.getExpiration() != null
|
||||
&& !token.getExpiration().before(new Date(validUntilTime))) {
|
||||
// 다른 노드가 넣어둔 토큰이 요구 시각까지 유효하다.
|
||||
return true;
|
||||
}
|
||||
return !StringUtils.equals(token.getAccessToken(), oldToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccessTokenVO reissueOutboundAccessToken(String key, String oldToken,
|
||||
public AccessTokenVO reissueOutboundAccessToken(String key, String oldToken, long validUntilTime,
|
||||
Function<AccessTokenVO, AccessTokenVO> tokenSupplier) {
|
||||
|
||||
AccessTokenVO token = cacheOutBoundAccessToken.get(key);
|
||||
AccessTokenVO token = readFromPrimary(key);
|
||||
|
||||
// 락을 잡기 전에 먼저 확인한다. 다른 노드가 이미 갱신했으면 그 토큰을 쓴다.
|
||||
if (isUsableToken(token) && !StringUtils.equals(token.getAccessToken(), oldToken)) {
|
||||
if (isAlreadyReissued(token, oldToken, validUntilTime)) {
|
||||
return token;
|
||||
}
|
||||
|
||||
@@ -866,8 +915,10 @@ public class SessionManagerForIgnite extends SessionManager {
|
||||
}
|
||||
|
||||
// 락 획득 사이에 다른 노드가 갱신했을 수 있으므로 다시 확인한다.
|
||||
token = cacheOutBoundAccessToken.get(key);
|
||||
if (isUsableToken(token) && !StringUtils.equals(token.getAccessToken(), oldToken)) {
|
||||
// near 캐시 사본은 무효화가 늦을 수 있어 그냥 get() 하면 이미 교체된 옛 토큰을 읽는다.
|
||||
// 짧은 비관적 트랜잭션으로 읽어 primary 의 값을 보장받는다. (발급 구간까지 끌고 가지 않는다)
|
||||
token = readFromPrimary(key);
|
||||
if (isAlreadyReissued(token, oldToken, validUntilTime)) {
|
||||
logger.debug("reissueOutboundAccessToken already reissued by another node. key=" + key);
|
||||
return token;
|
||||
}
|
||||
|
||||
+4
-4
@@ -49,9 +49,9 @@ class AccessTokenManagerByDBRetryTest {
|
||||
injectSessionManager(mockSessionManager);
|
||||
|
||||
// 분산락 구간을 흉내 낸다. supplier 를 그대로 실행해 결과를 돌려준다.
|
||||
Mockito.when(mockSessionManager.reissueOutboundAccessToken(anyString(), any(), any()))
|
||||
Mockito.when(mockSessionManager.reissueOutboundAccessToken(anyString(), any(), Mockito.anyLong(), any()))
|
||||
.thenAnswer(invocation -> {
|
||||
Function<AccessTokenVO, AccessTokenVO> supplier = invocation.getArgument(2);
|
||||
Function<AccessTokenVO, AccessTokenVO> supplier = invocation.getArgument(3);
|
||||
return supplier.apply(null);
|
||||
});
|
||||
}
|
||||
@@ -95,7 +95,7 @@ class AccessTokenManagerByDBRetryTest {
|
||||
AccessTokenVO result = manager.retryAccessTokenVO(GROUP, adapterProp(SERVICE_CLASS), "OLD");
|
||||
|
||||
assertEquals("NEW", result.getAccessToken());
|
||||
Mockito.verify(mockSessionManager).reissueOutboundAccessToken(eq(GROUP), eq("OLD"), any());
|
||||
Mockito.verify(mockSessionManager).reissueOutboundAccessToken(eq(GROUP), eq("OLD"), Mockito.anyLong(), any());
|
||||
Mockito.verify(mockSessionManager, Mockito.never()).getOutboundAccessToken(anyString(), any());
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ class AccessTokenManagerByDBRetryTest {
|
||||
AccessTokenVO result = manager.retryAccessTokenVO(GROUP, adapterProp("no.such.TokenService"), "OLD");
|
||||
|
||||
assertEquals("CACHED", result.getAccessToken());
|
||||
Mockito.verify(mockSessionManager, Mockito.never()).reissueOutboundAccessToken(anyString(), any(), any());
|
||||
Mockito.verify(mockSessionManager, Mockito.never()).reissueOutboundAccessToken(anyString(), any(), Mockito.anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+5
-5
@@ -87,7 +87,7 @@ class AccessTokenManagerByDBScheduleTest {
|
||||
runTask();
|
||||
|
||||
Mockito.verify(mockSessionManager).getOutboundAccessToken(eq(GROUP), any());
|
||||
Mockito.verify(mockSessionManager, Mockito.never()).reissueOutboundAccessToken(anyString(), any(), any());
|
||||
Mockito.verify(mockSessionManager, Mockito.never()).reissueOutboundAccessToken(anyString(), any(), Mockito.anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -98,7 +98,7 @@ class AccessTokenManagerByDBScheduleTest {
|
||||
|
||||
runTask();
|
||||
|
||||
Mockito.verify(mockSessionManager).reissueOutboundAccessToken(eq(GROUP), eq("OLD-TOKEN"), any());
|
||||
Mockito.verify(mockSessionManager).reissueOutboundAccessToken(eq(GROUP), eq("OLD-TOKEN"), Mockito.anyLong(), any());
|
||||
Mockito.verify(mockSessionManager, Mockito.never()).getOutboundAccessToken(anyString(), any());
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ class AccessTokenManagerByDBScheduleTest {
|
||||
|
||||
runTask();
|
||||
|
||||
Mockito.verify(mockSessionManager).reissueOutboundAccessToken(eq(GROUP), eq("OLD-TOKEN"), any());
|
||||
Mockito.verify(mockSessionManager).reissueOutboundAccessToken(eq(GROUP), eq("OLD-TOKEN"), Mockito.anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -119,7 +119,7 @@ class AccessTokenManagerByDBScheduleTest {
|
||||
|
||||
runTask();
|
||||
|
||||
Mockito.verify(mockSessionManager, Mockito.never()).reissueOutboundAccessToken(anyString(), any(), any());
|
||||
Mockito.verify(mockSessionManager, Mockito.never()).reissueOutboundAccessToken(anyString(), any(), Mockito.anyLong(), any());
|
||||
Mockito.verify(mockSessionManager, Mockito.never()).getOutboundAccessToken(anyString(), any());
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ class AccessTokenManagerByDBScheduleTest {
|
||||
|
||||
runTask();
|
||||
|
||||
Mockito.verify(mockSessionManager, Mockito.never()).reissueOutboundAccessToken(anyString(), any(), any());
|
||||
Mockito.verify(mockSessionManager, Mockito.never()).reissueOutboundAccessToken(anyString(), any(), Mockito.anyLong(), any());
|
||||
Mockito.verify(mockSessionManager, Mockito.never()).getOutboundAccessToken(anyString(), any());
|
||||
}
|
||||
}
|
||||
|
||||
+79
-8
@@ -20,7 +20,12 @@ import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.ignite.Ignite;
|
||||
import org.apache.ignite.IgniteCache;
|
||||
import org.apache.ignite.IgniteTransactions;
|
||||
import org.apache.ignite.transactions.Transaction;
|
||||
import org.apache.ignite.transactions.TransactionConcurrency;
|
||||
import org.apache.ignite.transactions.TransactionIsolation;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -50,6 +55,9 @@ class SessionManagerForIgniteReissueTest {
|
||||
/** 어댑터그룹별 락 (같은 키면 같은 인스턴스를 돌려줘야 경합 테스트가 성립한다) */
|
||||
private Map<String, Lock> locks;
|
||||
|
||||
/** readFromPrimary 가 여는 트랜잭션 */
|
||||
private Transaction transaction;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
sessionManager = new SessionManagerForIgnite();
|
||||
@@ -69,6 +77,25 @@ class SessionManagerForIgniteReissueTest {
|
||||
key -> new ReentrantLock()));
|
||||
|
||||
injectCache(cache);
|
||||
injectIgnite();
|
||||
}
|
||||
|
||||
/**
|
||||
* readFromPrimary 가 쓰는 Ignite 트랜잭션을 mock 으로 주입한다.
|
||||
* 주입하지 않으면 NPE 로 폴백 경로를 타게 되어 실제 동작을 검증하지 못한다.
|
||||
*/
|
||||
private void injectIgnite() throws Exception {
|
||||
transaction = Mockito.mock(Transaction.class);
|
||||
IgniteTransactions transactions = Mockito.mock(IgniteTransactions.class);
|
||||
Mockito.when(transactions.txStart(any(TransactionConcurrency.class), any(TransactionIsolation.class)))
|
||||
.thenReturn(transaction);
|
||||
|
||||
Ignite ignite = Mockito.mock(Ignite.class);
|
||||
Mockito.when(ignite.transactions()).thenReturn(transactions);
|
||||
|
||||
Field field = SessionManagerForIgnite.class.getDeclaredField("manager");
|
||||
field.setAccessible(true);
|
||||
field.set(null, ignite);
|
||||
}
|
||||
|
||||
/** private static cacheOutBoundAccessToken 에 mock 을 주입한다. */
|
||||
@@ -99,7 +126,7 @@ class SessionManagerForIgniteReissueTest {
|
||||
void 캐시비었을때_발급() {
|
||||
AtomicInteger issued = new AtomicInteger();
|
||||
|
||||
AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", supplier(issued, "NEW"));
|
||||
AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", 0L, supplier(issued, "NEW"));
|
||||
|
||||
assertEquals(1, issued.get());
|
||||
assertEquals("NEW", result.getAccessToken());
|
||||
@@ -112,7 +139,7 @@ class SessionManagerForIgniteReissueTest {
|
||||
store.put(GROUP, token("OLD"));
|
||||
AtomicInteger issued = new AtomicInteger();
|
||||
|
||||
AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", supplier(issued, "NEW"));
|
||||
AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", 0L, supplier(issued, "NEW"));
|
||||
|
||||
assertEquals(1, issued.get());
|
||||
assertEquals("NEW", result.getAccessToken());
|
||||
@@ -125,7 +152,7 @@ class SessionManagerForIgniteReissueTest {
|
||||
store.put(GROUP, token("ALREADY-NEW"));
|
||||
AtomicInteger issued = new AtomicInteger();
|
||||
|
||||
AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", supplier(issued, "NEW"));
|
||||
AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", 0L, supplier(issued, "NEW"));
|
||||
|
||||
assertEquals(0, issued.get(), "다른 토큰이 이미 캐시에 있으면 발급하지 않아야 한다");
|
||||
assertEquals("ALREADY-NEW", result.getAccessToken());
|
||||
@@ -137,7 +164,7 @@ class SessionManagerForIgniteReissueTest {
|
||||
store.put(GROUP, token("OLD"));
|
||||
AtomicInteger issued = new AtomicInteger();
|
||||
|
||||
AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", supplier(issued, null));
|
||||
AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", 0L, supplier(issued, null));
|
||||
|
||||
assertEquals(1, issued.get());
|
||||
assertNull(result);
|
||||
@@ -154,13 +181,57 @@ class SessionManagerForIgniteReissueTest {
|
||||
locks.put(GROUP, neverAcquired);
|
||||
|
||||
AtomicInteger issued = new AtomicInteger();
|
||||
AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", supplier(issued, "NEW"));
|
||||
AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", 0L, supplier(issued, "NEW"));
|
||||
|
||||
assertEquals(0, issued.get());
|
||||
assertEquals("OLD", result.getAccessToken());
|
||||
Mockito.verify(neverAcquired, Mockito.never()).unlock();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-1. 캐시 토큰이 요구 시각까지 유효하면 값이 같아도 발급하지 않는다")
|
||||
void 충분히유효하면_발급안함() {
|
||||
// 다른 노드가 방금 넣어둔 토큰. 조회 시점 차이로 oldToken 과 값이 같게 들어와도
|
||||
// 다음 틱까지 유효하면 다시 발급할 이유가 없다.
|
||||
store.put(GROUP, token("SAME-TOKEN"));
|
||||
AtomicInteger issued = new AtomicInteger();
|
||||
|
||||
long validUntil = System.currentTimeMillis() + 60_000L;
|
||||
AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "SAME-TOKEN", validUntil,
|
||||
supplier(issued, "NEW"));
|
||||
|
||||
assertEquals(0, issued.get(), "요구 시각까지 유효하면 발급하지 않아야 한다");
|
||||
assertEquals("SAME-TOKEN", result.getAccessToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-2. 요구 시각 전에 만료되면 발급한다")
|
||||
void 요구시각전_만료면_발급() {
|
||||
store.put(GROUP, token("SAME-TOKEN"));
|
||||
AtomicInteger issued = new AtomicInteger();
|
||||
|
||||
// 캐시 토큰은 10분 뒤 만료인데 20분 뒤까지 유효해야 한다면 갱신 대상이다.
|
||||
long validUntil = System.currentTimeMillis() + 1_200_000L;
|
||||
AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "SAME-TOKEN", validUntil,
|
||||
supplier(issued, "NEW"));
|
||||
|
||||
assertEquals(1, issued.get());
|
||||
assertEquals("NEW", result.getAccessToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-3. 캐시 조회는 비관적 트랜잭션 안에서 하고 바로 커밋한다")
|
||||
void 트랜잭션으로_읽는다() {
|
||||
store.put(GROUP, token("OLD"));
|
||||
|
||||
sessionManager.reissueOutboundAccessToken(GROUP, "OLD", 0L, supplier(new AtomicInteger(), "NEW"));
|
||||
|
||||
// 락 전 1회 + 락 안 1회
|
||||
Mockito.verify(transaction, Mockito.times(2)).commit();
|
||||
// 발급(HTTP) 구간까지 트랜잭션을 열어두면 장기 트랜잭션이 되어 파티션 맵 교환을 막는다.
|
||||
Mockito.verify(transaction, Mockito.never()).rollback();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("6. 경합 - 여러 스레드가 같은 토큰으로 동시에 재발급해도 발급은 한 번만 일어난다")
|
||||
void 동시재발급_한번만() throws Exception {
|
||||
@@ -190,7 +261,7 @@ class SessionManagerForIgniteReissueTest {
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
start.await();
|
||||
results[index] = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", slowSupplier);
|
||||
results[index] = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", 0L, slowSupplier);
|
||||
} catch (Exception e) {
|
||||
// 결과가 null 로 남아 아래 검증에서 걸린다.
|
||||
} finally {
|
||||
@@ -233,8 +304,8 @@ class SessionManagerForIgniteReissueTest {
|
||||
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
executor.submit(() -> sessionManager.reissueOutboundAccessToken("GRP-A", "OLD", blockingSupplier));
|
||||
executor.submit(() -> sessionManager.reissueOutboundAccessToken("GRP-B", "OLD", blockingSupplier));
|
||||
executor.submit(() -> sessionManager.reissueOutboundAccessToken("GRP-A", "OLD", 0L, blockingSupplier));
|
||||
executor.submit(() -> sessionManager.reissueOutboundAccessToken("GRP-B", "OLD", 0L, blockingSupplier));
|
||||
|
||||
assertTrue(bothInside.await(10, TimeUnit.SECONDS), "두 그룹이 동시에 발급 구간에 들어가야 한다");
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user