Skip to content

Commit 2cc6b70

Browse files
authored
fix: add retry on 503 errors (jeremylong#90)
1 parent 27c72f8 commit 2cc6b70

File tree

9 files changed

+123
-12
lines changed

9 files changed

+123
-12
lines changed

buildSrc/src/main/groovy/vuln.tools.java-common-conventions.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ plugins {
1212
}
1313

1414
group 'io.github.jeremylong'
15-
version = '5.0.1'
15+
version = '5.0.2'
1616

1717
repositories {
1818
mavenCentral()

open-vulnerability-clients/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@ See API usage examples in the [open-vulnerability-store](https://github.com/jere
3939
<dependency>
4040
<groupId>io.github.jeremylong</groupId>
4141
<artifactId>open-vulnerability-clients</artifactId>
42-
<version>5.0.1</version>
42+
<version>5.0.2</version>
4343
</dependency>
4444
```
4545

4646
### gradle
4747

4848
```groovy
49-
implementation 'io.github.jeremylong:open-vulnerability-clients:5.0.1'
49+
implementation 'io.github.jeremylong:open-vulnerability-clients:5.0.2'
5050
```
5151

5252
### api usage
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*
14+
* SPDX-License-Identifier: Apache-2.0
15+
* Copyright (c) 2023 Jeremy Long. All Rights Reserved.
16+
*/
17+
package io.github.jeremylong.openvulnerability.client.nvd;
18+
19+
import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
20+
import org.apache.hc.core5.http.HttpResponse;
21+
import org.apache.hc.core5.http.protocol.HttpContext;
22+
import org.apache.hc.core5.util.TimeValue;
23+
24+
import java.io.IOException;
25+
import java.util.ArrayList;
26+
import java.util.Arrays;
27+
import java.util.concurrent.TimeUnit;
28+
29+
/**
30+
* Implements a back-off delay retry strategy.
31+
*/
32+
public class NvdApiRetryStrategy extends DefaultHttpRequestRetryStrategy {
33+
/**
34+
* Maximum number of allowed retries.
35+
*/
36+
private final int maxRetries;
37+
38+
/**
39+
* Retry interval between subsequent retries in milliseconds.
40+
*/
41+
private final long delay;
42+
43+
public NvdApiRetryStrategy(int maxRetries, long delay) {
44+
super(maxRetries, TimeValue.of(delay, TimeUnit.MILLISECONDS), new ArrayList<Class<? extends IOException>>(),
45+
Arrays.asList(503));
46+
this.maxRetries = maxRetries;
47+
this.delay = delay;
48+
}
49+
50+
@Override
51+
public TimeValue getRetryInterval(final HttpResponse response, final int execCount, final HttpContext context) {
52+
53+
if (execCount < maxRetries / 2) {
54+
return TimeValue.of(delay * execCount, TimeUnit.MILLISECONDS);
55+
}
56+
57+
return TimeValue.of(delay * execCount / 2, TimeUnit.MILLISECONDS);
58+
}
59+
}

open-vulnerability-clients/src/main/java/io/github/jeremylong/openvulnerability/client/nvd/NvdCveClient.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,20 @@ public class NvdCveClient implements PagedDataSource<DefCveItem> {
133133
* @param maxPageCount the maximum number of pages to retrieve from the NVD API.
134134
*/
135135
NvdCveClient(String apiKey, String endpoint, int threadCount, int maxPageCount) {
136-
this(apiKey, endpoint, apiKey == null ? 6500 : 600, threadCount, maxPageCount);
136+
this(apiKey, endpoint, apiKey == null ? 6500 : 600, threadCount, maxPageCount, 10);
137+
}
138+
139+
/**
140+
* Constructs a new NVD CVE API client.
141+
*
142+
* @param apiKey the api key; can be null
143+
* @param endpoint the endpoint for the NVD CVE API; if null the default endpoint is used
144+
* @param threadCount the number of threads to use when calling the NVD API.
145+
* @param maxPageCount the maximum number of pages to retrieve from the NVD API.
146+
* @param maxRetryCount the maximum number of retries for 503 status code responses.
147+
*/
148+
NvdCveClient(String apiKey, String endpoint, int threadCount, int maxPageCount, int maxRetryCount) {
149+
this(apiKey, endpoint, apiKey == null ? 6500 : 600, threadCount, maxPageCount, maxRetryCount);
137150
}
138151

139152
/**
@@ -144,8 +157,9 @@ public class NvdCveClient implements PagedDataSource<DefCveItem> {
144157
* @param delay the delay in milliseconds between API calls on a single thread.
145158
* @param threadCount the number of threads to use when calling the NVD API.
146159
* @param maxPageCount the maximum number of pages to retrieve from the NVD API.
160+
* @param maxRetryCount the maximum number of retries for 503 status code responses.
147161
*/
148-
NvdCveClient(String apiKey, String endpoint, long delay, int threadCount, int maxPageCount) {
162+
NvdCveClient(String apiKey, String endpoint, long delay, int threadCount, int maxPageCount, int maxRetryCount) {
149163
this.apiKey = apiKey;
150164
if (endpoint == null) {
151165
this.endpoint = DEFAULT_ENDPOINT;
@@ -173,7 +187,7 @@ public class NvdCveClient implements PagedDataSource<DefCveItem> {
173187
}
174188
clients = new ArrayList<>(threadCount);
175189
for (int i = 0; i < threadCount; i++) {
176-
clients.add(new RateLimitedClient(delay, meter));
190+
clients.add(new RateLimitedClient(maxRetryCount, delay, meter));
177191
}
178192
objectMapper = new ObjectMapper();
179193
objectMapper.registerModule(new JavaTimeModule());

open-vulnerability-clients/src/main/java/io/github/jeremylong/openvulnerability/client/nvd/NvdCveClientBuilder.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ public final class NvdCveClientBuilder {
6464
* The minimum delay between API calls in milliseconds.
6565
*/
6666
private long delay;
67+
/**
68+
* The maximum number of retries for 503 responses from the NVD.
69+
*/
70+
private int maxRetryCount = 10;
6771
/**
6872
* The number of threads to use when calling the NVD API.
6973
*/
@@ -122,6 +126,17 @@ public NvdCveClientBuilder withDelay(long milliseconds) {
122126
return this;
123127
}
124128

129+
/**
130+
* Set the maximum number of retries for 503 responses from the NVD; default is 10.
131+
*
132+
* @param maxRetryCount the maximum number of retries for 503 responses from the NVD.
133+
* @return the builder
134+
*/
135+
public NvdCveClientBuilder withMaxRetryCount(int maxRetryCount) {
136+
this.maxRetryCount = maxRetryCount;
137+
return this;
138+
}
139+
125140
/**
126141
* Set the number of threads to use when calling the NVD API.
127142
*
@@ -322,9 +337,9 @@ public NvdCveClientBuilder withVersionEnd(String versionEnd, VersionType endType
322337
public NvdCveClient build() {
323338
NvdCveClient client;
324339
if (delay > 0) {
325-
client = new NvdCveClient(apiKey, endpoint, delay, threadCount, maxPageCount);
340+
client = new NvdCveClient(apiKey, endpoint, delay, threadCount, maxPageCount, maxRetryCount);
326341
} else {
327-
client = new NvdCveClient(apiKey, endpoint, threadCount, maxPageCount);
342+
client = new NvdCveClient(apiKey, endpoint, threadCount, maxPageCount, maxRetryCount);
328343
}
329344
if (!filters.isEmpty()) {
330345
client.setFilters(filters);

open-vulnerability-clients/src/main/java/io/github/jeremylong/openvulnerability/client/nvd/RateLimitedClient.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,25 @@ class RateLimitedClient implements AutoCloseable {
107107
* @param meter the rate meter to limit the request rate
108108
*/
109109
RateLimitedClient(long minimumDelay, RateMeter meter) {
110+
this(10, minimumDelay, meter);
111+
}
112+
113+
/**
114+
* Construct a rate limited client with a given delay and request window configuration. This allows callers to
115+
* configure 5 requests are allowed over a 30-second rolling window and we will delay at least 4 seconds between
116+
* calls to help more evenly distribute the calls across the request window.
117+
*
118+
* @param maxRetries the maximum number of retry attemps
119+
* @param minimumDelay the number of milliseconds to wait between API calls
120+
* @param meter the rate meter to limit the request rate
121+
*/
122+
RateLimitedClient(int maxRetries, long minimumDelay, RateMeter meter) {
110123
this.meter = meter;
111124
this.delay = minimumDelay;
112125
LOG.debug("rate limited call delay: {}", delay);
113-
126+
NvdApiRetryStrategy retryStrategy = new NvdApiRetryStrategy(maxRetries, minimumDelay);
114127
SystemDefaultRoutePlanner planner = new SystemDefaultRoutePlanner(ProxySelector.getDefault());
115-
client = HttpAsyncClients.custom().setRoutePlanner(planner).build();
128+
client = HttpAsyncClients.custom().setRoutePlanner(planner).setRetryStrategy(retryStrategy).build();
116129
client.start();
117130
}
118131

vulnz/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export JAVA_OPTS="-Xmx2g"
7272
Alternatively, run the CLI using the `-Xmx2g` argument:
7373

7474
```bash
75-
java -Xmx2g -jar ./vulnz-5.0.1.jar
75+
java -Xmx2g -jar ./vulnz-5.0.2.jar
7676
```
7777

7878
### Creating the Cache
@@ -89,7 +89,7 @@ for file in *.json; do gzip -k "${file}"; done
8989
Alternatively, without using the above install command:
9090

9191
```bash
92-
./vulnz-5.0.1.jar cve --cache --directory ./cache
92+
./vulnz-5.0.2.jar cve --cache --directory ./cache
9393
cd cache
9494
for file in *.json; do gzip -k "${file}"; done
9595
```

vulnz/src/main/java/io/github/jeremylong/vulnz/cli/commands/AbstractNvdCommand.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ public abstract class AbstractNvdCommand extends AbstractJsonCommand {
2828
@CommandLine.Option(names = {
2929
"--delay"}, description = "The delay in milliseconds between API calls to the NVD - important if pulling a larger data set without an API Key")
3030
private int delay;
31+
@CommandLine.Option(names = {
32+
"--maxRetry"}, description = "The maximum number of retry attempts on 503 errors from the NVD API")
33+
private int maxRetry;
3134
@CommandLine.Option(names = {
3235
"--pageCount"}, description = "The number of `pages` of data to retrieve from the NVD if more then a single page is returned")
3336
private int pageCount = 0;
@@ -59,6 +62,10 @@ protected int getDelay() {
5962
return delay;
6063
}
6164

65+
protected int getMaxRetry() {
66+
return maxRetry;
67+
}
68+
6269
/**
6370
* Returns the NVD API Key if supplied.
6471
*

vulnz/src/main/java/io/github/jeremylong/vulnz/cli/commands/CveCommand.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ public Integer timedCall() throws Exception {
113113
if (getDelay() > 0) {
114114
builder.withDelay(getDelay());
115115
}
116+
if (getMaxRetry() > 0) {
117+
builder.withMaxRetryCount(getMaxRetry());
118+
}
116119
if (cveId != null) {
117120
builder.withFilter(NvdCveClientBuilder.Filter.CVE_ID, cveId);
118121
}

0 commit comments

Comments
 (0)