Skip to content

Commit 387cc60

Browse files
committed
Implemented rate limiting detection while posting events
1 parent cf2357c commit 387cc60

File tree

5 files changed

+98
-18
lines changed

5 files changed

+98
-18
lines changed

src/main/java/com/exceptionless/exceptionlessclient/queue/DefaultEventQueue.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
import com.exceptionless.exceptionlessclient.exceptions.SubmissionClientException;
55
import com.exceptionless.exceptionlessclient.models.Event;
66
import com.exceptionless.exceptionlessclient.models.storage.StorageItem;
7-
import com.exceptionless.exceptionlessclient.submission.SubmissionResponse;
87
import com.exceptionless.exceptionlessclient.storage.StorageProviderIF;
98
import com.exceptionless.exceptionlessclient.submission.SubmissionClientIF;
9+
import com.exceptionless.exceptionlessclient.submission.SubmissionResponse;
1010
import com.exceptionless.exceptionlessclient.utils.VisibleForTesting;
1111
import lombok.Builder;
1212
import lombok.extern.slf4j.Slf4j;
@@ -158,6 +158,13 @@ private void processSubmissionResponse(
158158
return;
159159
}
160160

161+
if (response.isRateLimited()) {
162+
log.error(
163+
"Service is rate limited because of either you have exceeded your rate limit or server is under stress.");
164+
suspendProcessing();
165+
return;
166+
}
167+
161168
if (response.isServiceUnavailable()) {
162169
log.error("Service returns service unavailable");
163170
suspendProcessing();

src/main/java/com/exceptionless/exceptionlessclient/submission/DefaultSubmissionClient.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
import java.nio.charset.StandardCharsets;
1616
import java.time.Duration;
1717
import java.util.List;
18-
import java.util.Optional;
1918
import java.util.stream.Collectors;
2019

2120
@Slf4j
2221
public class DefaultSubmissionClient implements SubmissionClientIF {
2322
private static final String CONFIGURATION_VERSION_HEADER = "x-exceptionless-configversion";
23+
private static final String RATE_LIMITING_HEADER = "x-ratelimit-remaining";
2424

2525
private final Configuration configuration;
2626
private final SettingsManager settingsManager;
@@ -75,29 +75,33 @@ private SubmissionResponse postSubmission(String url, Object data) {
7575
Response response = httpClient.newCall(request).execute();
7676

7777
if (response.code() / 100 == 2) {
78-
updateSettingsFromHeaders(response.headers());
78+
updateSettingsFromHeaders(response);
7979
}
8080

8181
ResponseBody body = response.body();
8282
return SubmissionResponse.builder()
8383
.code(response.code())
8484
.body(body == null ? "" : body.string())
85+
.rateLimitingHeaderFound(isRateLimitingHeaderFound(response))
8586
.build();
8687
} catch (Exception e) {
8788
throw new SubmissionClientException(e);
8889
}
8990
}
9091

91-
private void updateSettingsFromHeaders(Headers headers) {
92-
Optional<String> maybeSettingsVersion =
93-
Optional.ofNullable(headers.get(CONFIGURATION_VERSION_HEADER));
94-
if (maybeSettingsVersion.isPresent()) {
95-
settingsManager.checkVersion(Long.parseLong(maybeSettingsVersion.get()));
92+
private void updateSettingsFromHeaders(Response response) {
93+
String settingsVersion = response.headers().get(CONFIGURATION_VERSION_HEADER);
94+
if (settingsVersion != null) {
95+
settingsManager.checkVersion(Long.parseLong(settingsVersion));
9696
} else {
9797
log.error("No config version header was returned");
9898
}
9999
}
100100

101+
private boolean isRateLimitingHeaderFound(Response response) {
102+
return response.headers().get(RATE_LIMITING_HEADER) != null;
103+
}
104+
101105
@Override
102106
public void sendHeartBeat(String sessionIdOrUserId, boolean closeSession) {
103107
try {

src/main/java/com/exceptionless/exceptionlessclient/submission/SubmissionResponse.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
public class SubmissionResponse {
99
int code;
1010
String body;
11+
boolean rateLimitingHeaderFound;
1112

1213
public boolean isSuccess() {
1314
return code >= 200 && code <= 299;
@@ -36,4 +37,8 @@ public boolean isNotFound() {
3637
public boolean isRequestEntityTooLarge() {
3738
return code == 413;
3839
}
40+
41+
public boolean isRateLimited() {
42+
return rateLimitingHeaderFound || code == 429;
43+
}
3944
}

src/test/java/com/exceptionless/exceptionlessclient/queue/DefaultEventQueueTest.java

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
import com.exceptionless.exceptionlessclient.configuration.Configuration;
55
import com.exceptionless.exceptionlessclient.exceptions.SubmissionClientException;
66
import com.exceptionless.exceptionlessclient.models.Event;
7-
import com.exceptionless.exceptionlessclient.submission.SubmissionResponse;
87
import com.exceptionless.exceptionlessclient.storage.InMemoryStorage;
98
import com.exceptionless.exceptionlessclient.storage.InMemoryStorageProvider;
109
import com.exceptionless.exceptionlessclient.submission.DefaultSubmissionClient;
10+
import com.exceptionless.exceptionlessclient.submission.SubmissionResponse;
1111
import org.junit.jupiter.api.BeforeEach;
1212
import org.junit.jupiter.api.Test;
1313
import org.junit.jupiter.api.extension.ExtendWith;
@@ -111,7 +111,9 @@ public void itShouldNotPostEmptyEvents() {
111111
public void itShouldSuspendProcessingOnClientException() {
112112
storage.save(event);
113113

114-
doThrow(new SubmissionClientException("test")).when(submissionClient).postEvents(List.of(event));
114+
doThrow(new SubmissionClientException("test"))
115+
.when(submissionClient)
116+
.postEvents(List.of(event));
115117

116118
queue.onEventsPosted(testHandler);
117119
queue.process();
@@ -137,6 +139,42 @@ public void itShouldSuspendProcessingIfServiceIsUnavailable() {
137139
verify(testHandler, times(1)).accept(List.of(event), response);
138140
}
139141

142+
@Test
143+
public void itShouldSuspendProcessingIfServiceIsRateLimitedByHeader() {
144+
storage.save(event);
145+
146+
SubmissionResponse response =
147+
SubmissionResponse.builder()
148+
.body("test-message")
149+
.code(999)
150+
.rateLimitingHeaderFound(true)
151+
.build();
152+
doReturn(response).when(submissionClient).postEvents(List.of(event));
153+
154+
queue.onEventsPosted(testHandler);
155+
queue.process();
156+
157+
assertThat(queue.isProcessingCurrentlySuspended()).isTrue();
158+
assertThat(storage.peek().getValue()).isEqualTo(event);
159+
verify(testHandler, times(1)).accept(List.of(event), response);
160+
}
161+
162+
@Test
163+
public void itShouldSuspendProcessingIfServiceIsRateLimitedByCode() {
164+
storage.save(event);
165+
166+
SubmissionResponse response =
167+
SubmissionResponse.builder().body("test-message").code(429).build();
168+
doReturn(response).when(submissionClient).postEvents(List.of(event));
169+
170+
queue.onEventsPosted(testHandler);
171+
queue.process();
172+
173+
assertThat(queue.isProcessingCurrentlySuspended()).isTrue();
174+
assertThat(storage.peek().getValue()).isEqualTo(event);
175+
verify(testHandler, times(1)).accept(List.of(event), response);
176+
}
177+
140178
@Test
141179
public void itShouldSuspendAndDiscardProcessingAndClearQueueIfNoPayment() {
142180
storage.save(event);
@@ -250,11 +288,9 @@ public void itShouldReduceSubmissionBatchSizeIfRequestEntitiesAreTooLarge() {
250288
queue.process();
251289

252290
// One invocation with full batch
253-
verify(submissionClient, times(1))
254-
.postEvents(argThat(argument -> argument.size() == 3));
291+
verify(submissionClient, times(1)).postEvents(argThat(argument -> argument.size() == 3));
255292
// One invocation with reduced batch
256-
verify(submissionClient, times(1))
257-
.postEvents(argThat(argument -> argument.size() == 2));
293+
verify(submissionClient, times(1)).postEvents(argThat(argument -> argument.size() == 2));
258294
}
259295

260296
@Test
@@ -303,11 +339,9 @@ public void itShouldResetSubmissionBatchSizeOnNextSuccessfulResponse() {
303339

304340
// Two invocations with full batch; First with the default size and next after a successful
305341
// response
306-
verify(submissionClient, times(2))
307-
.postEvents(argThat(argument -> argument.size() == 3));
342+
verify(submissionClient, times(2)).postEvents(argThat(argument -> argument.size() == 3));
308343
// One invocation with reduced batch
309-
verify(submissionClient, times(1))
310-
.postEvents(argThat(argument -> argument.size() == 2));
344+
verify(submissionClient, times(1)).postEvents(argThat(argument -> argument.size() == 2));
311345
}
312346

313347
@Test

src/test/java/com/exceptionless/exceptionlessclient/submission/DefaultSubmissionClientTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,36 @@ public void itCanPostEventsSuccessfully() throws IOException {
7676
verify(settingsManager, times(1)).checkVersion(3);
7777
}
7878

79+
@Test
80+
public void itCanDetectRateLimitingFromHeaders() throws IOException {
81+
Response response =
82+
responseBuilder
83+
.headers(
84+
Headers.of(
85+
Map.of("x-exceptionless-configversion", "3", "x-ratelimit-remaining", "0")))
86+
.build();
87+
doReturn(response).when(call).execute();
88+
doReturn(call)
89+
.when(httpClient)
90+
.newCall(
91+
argThat(
92+
request ->
93+
request
94+
.url()
95+
.toString()
96+
.equals(
97+
"http://test-server-url/api/v2/events?access_token=test-api-key")
98+
&& request.method().equals("POST")));
99+
100+
SubmissionResponse submissionResponse =
101+
submissionClient.postEvents(List.of(Event.builder().build()));
102+
103+
assertThat(submissionResponse.getBody()).isEqualTo("test-body");
104+
assertThat(submissionResponse.getCode()).isEqualTo(200);
105+
verify(settingsManager, times(1)).checkVersion(3);
106+
assertThat(submissionResponse.isRateLimited()).isTrue();
107+
}
108+
79109
@Test
80110
public void itCanPostEventsSuccessfullyWhenNoSettingHeaderIsReturned() throws IOException {
81111
doReturn(responseBuilder.build()).when(call).execute();

0 commit comments

Comments
 (0)