Skip to content

Commit f54bbae

Browse files
authored
TOO_MANY_REQUESTS при обновлении токена #15 (#16)
1 parent a57145c commit f54bbae

File tree

2 files changed

+71
-4
lines changed

2 files changed

+71
-4
lines changed

gigachat-java/src/main/java/chat/giga/client/auth/TokenBasedAuthClient.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.time.Instant;
1414
import java.util.Base64;
1515
import java.util.concurrent.atomic.AtomicReference;
16+
import java.util.concurrent.locks.ReentrantLock;
1617

1718
import static chat.giga.http.client.HttpHeaders.USER_AGENT;
1819
import static chat.giga.http.client.MediaType.APPLICATION_JSON;
@@ -24,6 +25,7 @@ abstract class TokenBasedAuthClient {
2425
protected final ObjectMapper objectMapper = JsonUtils.objectMapper();
2526

2627
private final AtomicReference<AccessToken> accessToken = new AtomicReference<>();
28+
private final ReentrantLock lock = new ReentrantLock();
2729

2830
protected HttpRequestBuilder getTokenRequest(String url, Scope scope, String user, String password) {
2931
var credentials = user + ":" + password;
@@ -48,10 +50,20 @@ protected String getBearerAuth() {
4850
}
4951

5052
protected String retrieveTokenIfExpired() {
51-
return accessToken.updateAndGet(t -> {
52-
var expiresAt = t == null ? null : t.expiresAt();
53-
return expiresAt != null && Instant.now().isBefore(expiresAt) ? t : getToken();
54-
}).token();
53+
AccessToken token = accessToken.get();
54+
if (token == null || Instant.now().isAfter(token.expiresAt())) {
55+
lock.lock();
56+
try {
57+
token = accessToken.get();
58+
if (token == null || Instant.now().isAfter(token.expiresAt())) {
59+
token = getToken();
60+
accessToken.set(token);
61+
}
62+
} finally {
63+
lock.unlock();
64+
}
65+
}
66+
return token.token();
5567
}
5668

5769
protected abstract AccessToken getToken();

gigachat-java/src/test/java/chat/giga/client/auth/OAuthClientTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,20 @@
2222
import java.nio.charset.StandardCharsets;
2323
import java.time.Instant;
2424
import java.util.List;
25+
import java.util.concurrent.CountDownLatch;
26+
import java.util.concurrent.ExecutorService;
27+
import java.util.concurrent.Executors;
28+
import java.util.concurrent.TimeUnit;
2529

2630
import static org.assertj.core.api.Assertions.assertThat;
2731
import static org.junit.jupiter.api.Assertions.assertEquals;
2832
import static org.junit.jupiter.api.Assertions.assertFalse;
2933
import static org.junit.jupiter.api.Assertions.assertNull;
3034
import static org.junit.jupiter.api.Assertions.assertThrows;
35+
import static org.junit.jupiter.api.Assertions.assertTrue;
3136
import static org.mockito.ArgumentMatchers.any;
37+
import static org.mockito.Mockito.doAnswer;
38+
import static org.mockito.Mockito.spy;
3239
import static org.mockito.Mockito.times;
3340
import static org.mockito.Mockito.verify;
3441
import static org.mockito.Mockito.when;
@@ -116,6 +123,54 @@ public void authenticate() throws Exception {
116123
});
117124
}
118125

126+
@Test
127+
public void shouldCallGetTokenOnlyOnceUnderContention() throws Exception {
128+
OAuthClient authClient = spy(new OAuthClient(null, "client", "secret", null, Scope.GIGACHAT_API_B2B, "url"));
129+
130+
AccessTokenResponse mockResponse = AccessTokenResponse.builder()
131+
.accessToken(token)
132+
.expiresAt(Instant.now()
133+
.plusSeconds(1000L).toEpochMilli())
134+
.build();
135+
136+
// Имитируем задержку сети, чтобы увеличить окно конкуренции
137+
doAnswer(invocation -> {
138+
Thread.sleep(100);
139+
return mockResponse;
140+
}).when(authClient).oauth();
141+
142+
int threadCount = 10;
143+
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
144+
CountDownLatch startLatch = new CountDownLatch(1);
145+
CountDownLatch finishLatch = new CountDownLatch(threadCount);
146+
HttpRequestBuilder requestBuilder = HttpRequest.builder()
147+
.url("test.ru/models");
148+
149+
for (int i = 0; i < threadCount; i++) {
150+
executor.submit(() -> {
151+
try {
152+
startLatch.await(); // Ждем сигнала к одновременному старту
153+
authClient.authenticate(requestBuilder);
154+
} catch (InterruptedException e) {
155+
Thread.currentThread().interrupt();
156+
} finally {
157+
finishLatch.countDown();
158+
}
159+
});
160+
}
161+
162+
startLatch.countDown();
163+
164+
// Ждем завершения всех потоков
165+
boolean completed = finishLatch.await(2, TimeUnit.SECONDS);
166+
assertTrue(completed, "Threads did not finish in time");
167+
168+
// Проверяем, что сетевой запрос был выполнен ровно 1 раз
169+
verify(authClient, times(1)).getToken();
170+
171+
executor.shutdown();
172+
}
173+
119174
@Test
120175
public void authenticateByKey() throws Exception {
121176
AuthClient authClient = AuthClientBuilder.builder()

0 commit comments

Comments
 (0)