Skip to content

Commit a514108

Browse files
authored
feat: adds InfluxDBApiHttpException (#163)
* feat: adds InfluxDBApiHttpException to allow recovery from some irregular response codes * chore: remove superfluous constructors and add unit test of new exception type * docs: adds example RetryExample.java, minor change to pom.xml * chore: add license to RetryExample.java * docs: update CHANGELOG.md * docs: fix lint examples/README.md * docs: lint again examples/README.md * docs: fix lint in markdown - again * docs: fix lint for list style in README.md * docs: add javadoc to two new methods
1 parent 9a253eb commit a514108

8 files changed

Lines changed: 434 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## 0.9.0 [unreleased]
22

3+
### Features
4+
5+
1. [#163](https://github.com/InfluxCommunity/influxdb3-java/pull/163): Introduces `InfluxDBApiHttpException` to facilitate write retries and error recovery.
6+
37
### Bug Fixes
48

59
1. [#148](https://github.com/InfluxCommunity/influxdb3-java/pull/148): InfluxDB Edge (OSS) error handling

examples/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,27 @@
66
## Basic
77

88
- [IOxExample](src/main/java/com/influxdb/v3/IOxExample.java) - How to use write and query data from InfluxDB IOx
9+
10+
## RetryExample
11+
12+
- [RetryExample](src/main/java/com/influxdb/v3/RetryExample.java) - How to catch an `InfluxDBApiHttpException` to then retry making write requests.
13+
14+
### Command line run
15+
16+
- Set environment variables.
17+
18+
```bash
19+
20+
export INFLUX_HOST=<INFLUX_CLOUD_HOST_URL>
21+
export INFLUX_TOKEN=<ORGANIZATION_TOKEN>
22+
export INFLUX_DATABASE=<TARGET_DATABASE>
23+
24+
```
25+
26+
- Run with maven
27+
28+
```bash
29+
mvn compile exec:java -Dexec.main="com.influxdb.v3.RetryExample"
30+
```
31+
32+
- Repeat previous step to force an HTTP 429 response and rewrite attempt.

examples/pom.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
<properties>
3434
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
35+
<exec.main>com.influxdb.v3.IOxExample</exec.main>
3536
</properties>
3637

3738
<dependencies>
@@ -56,7 +57,7 @@
5657
</execution>
5758
</executions>
5859
<configuration>
59-
<mainClass>com.influxdb.v3.IOxExample</mainClass>
60+
<mainClass>${exec.main}</mainClass>
6061
</configuration>
6162
</plugin>
6263
<plugin>
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
* THE SOFTWARE.
21+
*/
22+
package com.influxdb.v3;
23+
24+
import java.time.Instant;
25+
import java.time.LocalDateTime;
26+
import java.time.ZoneOffset;
27+
import java.time.format.DateTimeParseException;
28+
import java.time.temporal.ChronoUnit;
29+
import java.util.ArrayList;
30+
import java.util.List;
31+
import java.util.UUID;
32+
import java.util.concurrent.TimeUnit;
33+
import java.util.logging.Logger;
34+
35+
import com.influxdb.v3.client.InfluxDBApiHttpException;
36+
import com.influxdb.v3.client.InfluxDBClient;
37+
import com.influxdb.v3.client.Point;
38+
39+
40+
/**
41+
* This examples shows how to catch certain HttpExceptions and leverage their status code and header
42+
* values to attempt to rewrite data refused by the server. It attempts to exceed the maximum allowable writes
43+
* for an unpaid Influx cloud account.
44+
* <p>
45+
* A First run with 100,000 records should succeed. but a follow-up run within a couple of minutes
46+
* should result in a refusal with the HTTP status code 429 "Too Many Requests"
47+
* <p>
48+
* See the examples README.md for more information.
49+
*/
50+
public final class RetryExample {
51+
52+
private static final java.util.logging.Logger LOG = Logger.getLogger(RetryExample.class.getName());
53+
54+
private RetryExample() { }
55+
56+
static int retries = 0;
57+
58+
public static String resolveProperty(final String property, final String fallback) {
59+
return System.getProperty(property, System.getenv(property)) == null
60+
? fallback : System.getProperty(property, System.getenv(property));
61+
}
62+
63+
public static void main(final String[] args) throws Exception {
64+
65+
String host = resolveProperty("INFLUX_HOST", "https://us-east-1-1.aws.cloud2.influxdata.com");
66+
String token = resolveProperty("INFLUX_TOKEN", "my-token");
67+
String database = resolveProperty("INFLUX_DATABASE", "my-database");
68+
69+
Instant now = Instant.now();
70+
int dataSetSize = 100000;
71+
72+
LOG.info(String.format("Preparing to write %d records.\n", dataSetSize));
73+
74+
List<BatteryPack> packs = new ArrayList<>();
75+
List<Point> points = new ArrayList<>();
76+
for (int i = 0; i < dataSetSize; i++) {
77+
packs.add(BatteryPack.createRandom());
78+
points.add(packs.get(i).toPoint(now.minus((dataSetSize - i) * 10, ChronoUnit.MILLIS)));
79+
}
80+
81+
retries = 1;
82+
try (InfluxDBClient client = InfluxDBClient.getInstance(host, token.toCharArray(), database)) {
83+
writeWithRetry(client, points, retries);
84+
}
85+
}
86+
87+
/**
88+
* Resolves the retry value in milliseconds.
89+
*
90+
* @param retryVal - retry value from the HTTP header "retry-after"
91+
* @return - wait interval in milliseconds
92+
*/
93+
private static long resolveRetry(final String retryVal) {
94+
try {
95+
return (Long.parseLong(retryVal) * 1000);
96+
} catch (NumberFormatException nfe) {
97+
try {
98+
Instant now = Instant.now();
99+
Instant retry = LocalDateTime.parse(retryVal).toInstant(ZoneOffset.UTC);
100+
return retry.toEpochMilli() - now.toEpochMilli();
101+
} catch (DateTimeParseException dtpe) {
102+
throw new RuntimeException("Unable to parse retry time: " + retryVal, dtpe);
103+
}
104+
}
105+
}
106+
107+
/**
108+
* Helper method to be called recursively in the event a retry is required.
109+
*
110+
* @param client - InfluxDBClient used to make the request.
111+
* @param points - Data to be written.
112+
* @param retryCount - Number of times to retry write requests.
113+
* @throws InterruptedException - if wait is interrupted.
114+
*/
115+
private static void writeWithRetry(final InfluxDBClient client,
116+
final List<Point> points,
117+
final int retryCount) throws InterruptedException {
118+
try {
119+
client.writePoints(points);
120+
LOG.info(String.format("Succeeded on write %d\n", (retries - retryCount) + 1));
121+
} catch (InfluxDBApiHttpException e) {
122+
if ((e.statusCode() == 429 || e.statusCode() == 503)
123+
&& e.getHeader("retry-after") != null
124+
&& retryCount > 0) {
125+
long wait = resolveRetry(e.getHeader("retry-after").get(0)) + 1000;
126+
LOG.warning(
127+
String.format("Failed to write with HTTP %d waiting %d secs to retry.\n", e.statusCode(), wait / 1000)
128+
);
129+
keepBusy(wait);
130+
LOG.info(String.format("Write attempt %d", ((retries - (retryCount - 1)) + 1)));
131+
writeWithRetry(client, points, retryCount - 1);
132+
} else {
133+
LOG.severe(String.format("Failed to write in %d tries. Giving up with HTTP %d\n",
134+
retries,
135+
e.statusCode()));
136+
}
137+
}
138+
}
139+
140+
/**
141+
* Convenience method for a nicer wait experience.
142+
*
143+
* @param wait interval to wait before retrying.
144+
* @throws InterruptedException in the event wait is interrupted.
145+
*/
146+
private static void keepBusy(final long wait) throws InterruptedException {
147+
long ttl = Instant.now().toEpochMilli() + wait;
148+
String[] spinner = {"-", "\\", "|", "/"};
149+
int count = 0;
150+
while (Instant.now().toEpochMilli() < ttl) {
151+
System.out.printf("\r%s", spinner[count++ % 4]);
152+
System.out.flush();
153+
TimeUnit.MILLISECONDS.sleep(500);
154+
}
155+
System.out.println();
156+
}
157+
158+
/**
159+
* An example data type roughly modeling battery packs in an EV.
160+
*/
161+
static class BatteryPack {
162+
String id;
163+
String vehicle;
164+
int emptyCells;
165+
int cellRack;
166+
double percent;
167+
168+
public BatteryPack(final String id,
169+
final String vehicle,
170+
final int emptyCells,
171+
final int cellRack,
172+
final double percent) {
173+
this.id = id;
174+
this.vehicle = vehicle;
175+
this.emptyCells = emptyCells;
176+
this.cellRack = cellRack;
177+
this.percent = percent;
178+
}
179+
180+
public Point toPoint(final Instant time) {
181+
return Point.measurement("bpack")
182+
.setTag("id", id)
183+
.setField("vehicle", vehicle)
184+
.setField("emptyCells", emptyCells)
185+
.setField("totalCells", cellRack)
186+
.setField("percent", percent)
187+
.setTimestamp(time);
188+
}
189+
190+
static List<String> idSet = List.of(UUID.randomUUID().toString(),
191+
UUID.randomUUID().toString(),
192+
UUID.randomUUID().toString(),
193+
UUID.randomUUID().toString(),
194+
UUID.randomUUID().toString());
195+
196+
static List<String> vehicleSet = List.of(
197+
"THX1138",
198+
"VC81072",
199+
"J007O981",
200+
"NTSL1856",
201+
"TOURDF24"
202+
);
203+
204+
static List<Integer> cellsSet = List.of(100, 200, 500);
205+
206+
public static BatteryPack createRandom() {
207+
int index = (int) Math.floor(Math.random() * 5);
208+
int totalCells = cellsSet.get((int) Math.floor(Math.random() * 3));
209+
int emptyCells = (int) Math.floor(Math.random() * totalCells);
210+
double percent = ((((double) emptyCells / (double) totalCells)) * 100);
211+
percent += Math.random() * ((100 - percent) * 0.15);
212+
percent = 100 - percent;
213+
return new BatteryPack(idSet.get(index), vehicleSet.get(index), emptyCells, totalCells, percent);
214+
}
215+
216+
@Override
217+
public String toString() {
218+
return String.format("id: %s, vehicle: %s, cellRack: %d, emptyCells: %d, percent: %f",
219+
id, vehicle, cellRack, emptyCells, percent);
220+
}
221+
}
222+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
* THE SOFTWARE.
21+
*/
22+
package com.influxdb.v3.client;
23+
24+
import java.net.http.HttpHeaders;
25+
import java.util.List;
26+
import javax.annotation.Nullable;
27+
28+
/**
29+
* The InfluxDBApiHttpException gets thrown whenever an error status is returned
30+
* in the HTTP response. It facilitates recovering from such errors whenever possible.
31+
*/
32+
public class InfluxDBApiHttpException extends InfluxDBApiException {
33+
34+
HttpHeaders headers;
35+
int statusCode;
36+
37+
/**
38+
* Construct a new InfluxDBApiHttpException with statusCode and headers.
39+
*
40+
* @param message the detail message.
41+
* @param headers headers returned in the response.
42+
* @param statusCode statusCode of the response.
43+
*/
44+
public InfluxDBApiHttpException(
45+
@Nullable final String message,
46+
@Nullable final HttpHeaders headers,
47+
final int statusCode) {
48+
super(message);
49+
this.headers = headers;
50+
this.statusCode = statusCode;
51+
}
52+
53+
/**
54+
* Construct a new InfluxDBApiHttpException with statusCode and headers.
55+
*
56+
* @param cause root cause of the exception.
57+
* @param headers headers returned in the response.
58+
* @param statusCode status code of the response.
59+
*/
60+
public InfluxDBApiHttpException(
61+
@Nullable final Throwable cause,
62+
@Nullable final HttpHeaders headers,
63+
final int statusCode) {
64+
super(cause);
65+
this.headers = headers;
66+
this.statusCode = statusCode;
67+
}
68+
69+
/**
70+
* Gets the HTTP headers property associated with the error.
71+
*
72+
* @return - the headers object.
73+
*/
74+
public HttpHeaders headers() {
75+
return headers;
76+
}
77+
78+
/**
79+
* Helper method to simplify retrieval of specific headers.
80+
*
81+
* @param name - name of the header.
82+
* @return - value matching the header key, or null if the key does not exist.
83+
*/
84+
public List<String> getHeader(final String name) {
85+
return headers.map().get(name);
86+
}
87+
88+
/**
89+
* Gets the HTTP statusCode associated with the error.
90+
* @return - the HTTP statusCode.
91+
*/
92+
public int statusCode() {
93+
return statusCode;
94+
}
95+
96+
}

src/main/java/com/influxdb/v3/client/internal/RestClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.slf4j.LoggerFactory;
4646

4747
import com.influxdb.v3.client.InfluxDBApiException;
48+
import com.influxdb.v3.client.InfluxDBApiHttpException;
4849
import com.influxdb.v3.client.config.ClientConfig;
4950

5051
final class RestClient implements AutoCloseable {
@@ -212,7 +213,7 @@ void request(@Nonnull final String path,
212213
}
213214

214215
String message = String.format("HTTP status code: %d; Message: %s", statusCode, reason);
215-
throw new InfluxDBApiException(message);
216+
throw new InfluxDBApiHttpException(message, response.headers(), response.statusCode());
216217
}
217218
}
218219

0 commit comments

Comments
 (0)