Skip to content

Commit 0288de6

Browse files
authored
Merge pull request #57 from exceptionless/rate-limiting
Implemented rate limiting detection while posting events
2 parents fd8f334 + 7636e1c commit 0288de6

File tree

7 files changed

+118
-9
lines changed

7 files changed

+118
-9
lines changed

src/main/java/com/exceptionless/exceptionlessclient/configuration/Configuration.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class Configuration {
1818
public static class Property {
1919
public static final String API_KEY = "apiKey";
2020
public static final String SERVER_URL = "serverUrl";
21+
public static final String CONFIG_SERVER_URL = "configServerUrl";
2122
public static final String HEART_BEAT_SERVER_URL = "heartbeatServerUrl";
2223
public static final String UPDATE_SETTINGS_WHEN_IDLE_INTERVAL =
2324
"updateSettingsWhenIdleInterval";
@@ -94,6 +95,13 @@ public void setServerUrl(String serverUrl) {
9495
propertyChangeSupport.firePropertyChange(Property.SERVER_URL, prevValue, serverUrl);
9596
}
9697

98+
public void setConfigServerUrl(String configServerUrl) {
99+
String prevValue = this.configServerUrl;
100+
this.configServerUrl = configServerUrl;
101+
propertyChangeSupport.firePropertyChange(
102+
Property.CONFIG_SERVER_URL, prevValue, configServerUrl);
103+
}
104+
97105
public void setHeartbeatServerUrl(String heartbeatServerUrl) {
98106
String prevValue = this.heartbeatServerUrl;
99107
this.heartbeatServerUrl = heartbeatServerUrl;

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,13 @@ private void processSubmissionResponse(
159159
return;
160160
}
161161

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

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

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

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

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

7676
if (response.isSuccessful()) {
77-
updateSettingsFromHeaders(response.headers());
77+
updateSettingsFromHeaders(response);
7878
}
7979

8080
ResponseBody body = response.body();
8181
return SubmissionResponse.builder()
8282
.code(response.code())
8383
.body(body == null ? "" : body.string())
84+
.rateLimitingHeaderFound(isRateLimitingHeaderFound(response))
8485
.build();
8586
} catch (Exception e) {
8687
return SubmissionResponse.builder().exception(e).build();
8788
}
8889
}
8990

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

100+
private boolean isRateLimitingHeaderFound(Response response) {
101+
return response.headers().get(RATE_LIMITING_HEADER) != null;
102+
}
103+
100104
@Override
101105
public void sendHeartBeat(String sessionIdOrUserId, boolean closeSession) {
102106
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
Exception exception;
1213

1314
public boolean isSuccess() {
@@ -38,6 +39,10 @@ public boolean isRequestEntityTooLarge() {
3839
return code == 413;
3940
}
4041

42+
public boolean isRateLimited() {
43+
return rateLimitingHeaderFound || code == 429;
44+
}
45+
4146
public boolean hasException() {
4247
return exception != null;
4348
}

src/test/java/com/exceptionless/exceptionlessclient/configuration/ConfigurationTest.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public void itCanNotifyChangesToListeners() {
3131
Configuration.builder()
3232
.apiKey("old-api-key")
3333
.serverUrl("old-server-url")
34+
.configServerUrl("old-config-server-url")
3435
.heartbeatServerUrl("old-heartbeat-server-url")
3536
.updateSettingsWhenIdleInterval(1L)
3637
.submissionBatchSize(50)
@@ -41,6 +42,7 @@ public void itCanNotifyChangesToListeners() {
4142

4243
configuration.setApiKey("new-api-key");
4344
configuration.setServerUrl("new-server-url");
45+
configuration.setConfigServerUrl("new-config-server-url");
4446
configuration.setHeartbeatServerUrl("new-heartbeat-server-url");
4547
configuration.setUpdateSettingsWhenIdleInterval(2L);
4648
configuration.setSubmissionBatchSize(100);
@@ -51,15 +53,32 @@ public void itCanNotifyChangesToListeners() {
5153
List.of(
5254
"apiKey",
5355
"serverUrl",
56+
"configServerUrl",
5457
"heartbeatServerUrl",
5558
"updateSettingsWhenIdleInterval",
5659
"submissionBatchSize",
5760
"submissionClientTimeoutInMillis",
5861
"settingsClientTimeoutInMillis");
5962
List<Object> oldValues =
60-
List.of("old-api-key", "old-server-url", "old-heartbeat-server-url", 1L, 50, 10, 10);
63+
List.of(
64+
"old-api-key",
65+
"old-server-url",
66+
"old-config-server-url",
67+
"old-heartbeat-server-url",
68+
1L,
69+
50,
70+
10,
71+
10);
6172
List<Object> newValues =
62-
List.of("new-api-key", "new-server-url", "new-heartbeat-server-url", 2L, 100, 20, 20);
73+
List.of(
74+
"new-api-key",
75+
"new-server-url",
76+
"new-config-server-url",
77+
"new-heartbeat-server-url",
78+
2L,
79+
100,
80+
20,
81+
20);
6382

6483
properties.forEach(
6584
property ->

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,42 @@ public void itShouldSuspendProcessingIfServiceIsUnavailable() {
138138
verify(testHandler, times(1)).accept(List.of(event), response);
139139
}
140140

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

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

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

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

0 commit comments

Comments
 (0)