Skip to content

Commit 119d3ea

Browse files
authored
docs: add durable example (#253)
* docs: start durable example * docs: new DurableExample passes checkstyle. * docs: add license headers to new example. * docs: add command line run to examples/README.md * docs: fix typo in javadoc comment. * docs: update CHANGELOG.md * docs: fix markdown linter issues. * docs: fix durable-example based on feedback. * tests: tweek flakey tests. * tests: tweek flakey tests (remove unsued import)
1 parent 845dfb2 commit 119d3ea

File tree

6 files changed

+684
-2
lines changed

6 files changed

+684
-2
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
1. [#250](https://github.com/InfluxCommunity/influxdb3-java/pull/250) Upgrade Netty version to 4.2.3.Final.
66
2. [#251](https://github.com/InfluxCommunity/influxdb3-java/pull/251) Add comment warning null when calling getMeasurement function.
77

8+
### Documentation
9+
10+
1. [#253](https://github.com/InfluxCommunity/influxdb3-java/pull/253) New Durable example showing client reuse for better resource management.
11+
812
## 1.2.0 [2025-06-26]
913

1014
### Features

examples/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,30 @@ mvn compile exec:java -Dexec.main="com.influxdb.v3.RetryExample"
3030
```
3131

3232
- Repeat previous step to force an HTTP 429 response and rewrite attempt.
33+
34+
## Durable example
35+
36+
This example illustrates one approach to ensuring clients, once initialized, are long-lived and reused.
37+
38+
The underlying write (HTTP/REST) and query (Apache arrow Flight/GRPC) transports are designed to be robust and to be able to recover from most errors. The InfluxDBClient query API is based on GRPC stubs and channels. [GRPC best practices](https://grpc.io/docs/guides/performance/) recommends reusing them and their resources for the life of an application if at all possible. Unnecessary frequent regeneration of InfluxDBClient instances is wasteful of system resources. Recreating the query transport means fully recreating a GRPC channel, its connection pool and its management API. Fully recreating a client only to use it for a single query also means recreating an unused write transport alongside the query transport. This example attempts to show a more resource friendly use of the API by leveraging already used client instances.
39+
40+
- [DurableExample](src/main/java/com/influxdb/v3/durable/DurableExample.java)
41+
- [InfluxClientPool](src/main/java/com/influxdb/v3/durable/InfluxClientPool.java)
42+
43+
### Command line run
44+
45+
- Set environment variables
46+
47+
```bash
48+
49+
export INFLUX_HOST=<INFLUX_HOST_URL>
50+
export INFLUX_TOKEN=<ORGANIZATION_TOKEN>
51+
export INFLUX_DATABASE=<TARGET_DATABASE>
52+
53+
```
54+
55+
- Run with maven
56+
57+
```bash
58+
MAVEN_OPTS="--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED" mvn compile exec:java -Dexec.main="com.influxdb.v3.durable.DurableExample"
59+
```
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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.durable;
23+
24+
import java.util.ArrayList;
25+
import java.util.List;
26+
import java.util.concurrent.ExecutorService;
27+
import java.util.concurrent.Executors;
28+
import java.util.concurrent.TimeUnit;
29+
import java.util.concurrent.atomic.AtomicBoolean;
30+
import java.util.concurrent.locks.LockSupport;
31+
import java.util.logging.Logger;
32+
import java.util.stream.Stream;
33+
34+
import com.influxdb.v3.client.InfluxDBApiException;
35+
import com.influxdb.v3.client.InfluxDBClient;
36+
import com.influxdb.v3.client.Point;
37+
import com.influxdb.v3.client.PointValues;
38+
import com.influxdb.v3.client.config.ClientConfig;
39+
40+
/**
41+
* The example depends on the "influxdb3-java" module and this module should be built first
42+
* by running "mvn install" in the root directory.
43+
* <p>
44+
* This example illustrates how to reuse InfluxDBClient instances. The underlying write (REST) and
45+
* query (apache arrow Flight/GRPC) transports are designed to be robust and long-lived. Frequent creation or
46+
* recreation of InfluxDBClients and then discarding them and their underlying transports is
47+
* inefficient. GRPC best practices recommends trying to use their channels, on which the InfluxDBClient
48+
* query transport is based, for the life of an application, if at all possible. The write transport is
49+
* also designed to recover from most errors and to be reusable. This example is one approach to reusing
50+
* InfluxDBClient instances for as long as possible.
51+
* <p>
52+
* At its core this example uses a client pool and four processing threads. The threads borrow
53+
* clients from the pool as needed and then return them once they are no longer needed. Two threads
54+
* are used for writing data and two additional threads are used for executing queries.
55+
* One write thread is designed to occasionally force an error response from the server. Like wise one
56+
* query thread is designed to occasionally elicit error responses in the GRPC channel. Even though
57+
* errors occur in these transactions, the clients involved can continue to be used for later writes
58+
* and queries. Furthermore, while four processing threads are running, the pool need only instantiate three
59+
* clients, if handled properly.
60+
*/
61+
public final class DurableExample {
62+
63+
static Logger logger = Logger.getLogger(DurableExample.class.getName());
64+
65+
public static ClientConfig clientConfig;
66+
67+
private DurableExample() {
68+
}
69+
70+
public static void setup() {
71+
72+
String influxHost = System.getenv("INFLUX_HOST") != null
73+
? System.getenv("INFLUX_HOST") : "http://localhost:8181";
74+
String influxToken = System.getenv("INFLUX_TOKEN") != null
75+
? System.getenv("INFLUX_TOKEN") : "my-token";
76+
String influxDatabase = System.getenv("INFLUX_DATABASE") != null
77+
? System.getenv("INFLUX_DATABASE") : "my-db";
78+
79+
clientConfig = new ClientConfig.Builder()
80+
.host(influxHost)
81+
.token(influxToken.toCharArray())
82+
.database(influxDatabase)
83+
.build();
84+
}
85+
86+
public static void main(final String[] args) {
87+
88+
setup();
89+
90+
// A basic control signal
91+
AtomicBoolean shutdownAll = new AtomicBoolean(false);
92+
93+
// time to run the example in minutes
94+
int runTime = 2;
95+
96+
// a set of sensors as a source of data
97+
List<Sensor> sensors = List.of(
98+
new Sensor("Alfa", "Univac51", "libava"),
99+
new Sensor("Bravo", "Eniac45", "brezina"),
100+
new Sensor("Charlie", "Ordvac52", "boletice"),
101+
new Sensor("Delta", "HAL2001", "hradiste"),
102+
new Sensor("Echo", "BESM68", "brdy")
103+
);
104+
105+
// standard query string
106+
String query = String.format("SELECT * FROM %s ORDER BY time DESC", Sensor.class.getSimpleName());
107+
108+
// query string to elicit error response
109+
String badQuery = String.format("SELECT * FOO %s ORDER BY time DESC", Sensor.class.getSimpleName());
110+
111+
// Set up the autoclosable client pool
112+
try (InfluxClientPool clientPool = new InfluxClientPool(clientConfig)) {
113+
114+
// thread controller
115+
final ExecutorService executors = Executors.newFixedThreadPool(5);
116+
117+
// an error free write thread
118+
final Runnable writeOK = () -> {
119+
int count = 0;
120+
while (!shutdownAll.get()) {
121+
List<Point> points = new ArrayList<>();
122+
for (Sensor sensor : sensors) {
123+
points.add(sensor.randomPoint().toPoint());
124+
}
125+
126+
// borrow then return a client
127+
InfluxDBClient client = clientPool.borrowClient();
128+
try {
129+
logger.info(" [writeTaskPointsOK " + count + "] Writing " + points.size()
130+
+ " points with client " + client.hashCode());
131+
client.writePoints(points);
132+
} catch (Exception e) {
133+
logger.severe(" [writeTaskPointsOK " + count + "] Unexpected Error writing points "
134+
+ e.getMessage());
135+
} finally {
136+
clientPool.returnClient(client);
137+
}
138+
139+
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(10));
140+
count++;
141+
}
142+
logger.info(" [writeTaskPointsOK] shutting down");
143+
};
144+
145+
// An error-prone write thread
146+
final Runnable writeErrorRecover = () -> {
147+
// delay start by 2 seconds
148+
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2));
149+
int count = 0;
150+
while (!shutdownAll.get()) {
151+
List<String> lps = new ArrayList<>();
152+
for (Sensor sensor : sensors) {
153+
// every fourth write attempt uses an invalid Line protocol line
154+
if (count > 0 && count % 4 == 0 && sensor.getName().equals("Charlie")) {
155+
// add the invalid LP line
156+
lps.add(sensor.randomPoint().toLPBroken());
157+
} else {
158+
lps.add(sensor.randomPoint().toLP());
159+
}
160+
}
161+
// borrow a client from the pool
162+
InfluxDBClient client = clientPool.borrowClient();
163+
try {
164+
logger.info("[writeErrorRecover " + count + "] Writing " + lps.size()
165+
+ " lps with client " + client.hashCode());
166+
client.writeRecords(lps);
167+
} catch (InfluxDBApiException ie) {
168+
logger.warning("[writeErrorRecover " + count + "] Write Error " + ie.getMessage());
169+
} finally {
170+
// make sure the client is returned to the pool even after an error
171+
clientPool.returnClient(client);
172+
}
173+
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(10));
174+
count++;
175+
}
176+
logger.info(" [writeErrorRecover] shutting down");
177+
};
178+
179+
// an error free query thread
180+
final Runnable queryOK = () -> {
181+
// delay start by 4 seconds
182+
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(4));
183+
int count = 0;
184+
while (!shutdownAll.get()) {
185+
// borrow a client from the pool
186+
InfluxDBClient client = clientPool.borrowClient();
187+
188+
// initiate the query and process the results
189+
try (Stream<PointValues> pvs = client.queryPoints(query)) {
190+
logger.info("[queryOK " + count + "] with client " + client.hashCode()
191+
+ ": query returned " + pvs.toArray().length + " records");
192+
} catch (Exception e) {
193+
logger.severe("[queryOK " + count + "] unexpected query Error " + e.getMessage());
194+
} finally {
195+
// ensure the client is returned to the pool
196+
clientPool.returnClient(client);
197+
}
198+
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(10));
199+
count++;
200+
}
201+
logger.info(" [queryOK] shutting down");
202+
};
203+
204+
// an error-prone query thread
205+
final Runnable queryErrorRecover = () -> {
206+
// delay start by 6 seconds
207+
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(6));
208+
int count = 0;
209+
while (!shutdownAll.get()) {
210+
// borrow a client from the pool
211+
InfluxDBClient client = clientPool.borrowClient();
212+
// every third query attempt results in an error
213+
String effectiveQuery = count > 0 && count % 3 == 0 ? badQuery : query;
214+
215+
// attempt to execute the query and process the results
216+
try (Stream<PointValues> pvs = client.queryPoints(effectiveQuery)) {
217+
logger.info("[queryErrorRecover " + count + "] with client " + client.hashCode()
218+
+ ": query returned " + pvs.toArray().length + " records");
219+
} catch (Exception e) {
220+
logger.warning("[queryErrorRecover " + count + "] with client " + client.hashCode()
221+
+ ": query Error " + e.getMessage());
222+
} finally {
223+
// ensure the client is returned to the pool even after an error
224+
clientPool.returnClient(client);
225+
}
226+
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(10));
227+
count++;
228+
}
229+
logger.info(" [queryErrorRecover] shutting down");
230+
};
231+
232+
// control how long the example runs
233+
final Runnable timer = () -> {
234+
LockSupport.parkNanos(TimeUnit.MINUTES.toNanos(runTime));
235+
shutdownAll.set(true);
236+
logger.info(" [timer] Shutting down");
237+
logger.info("clientPool clients: active "
238+
+ clientPool.activeCount()
239+
+ " idle: " + clientPool.idleCount());
240+
executors.shutdown();
241+
};
242+
243+
// trigger all threads
244+
executors.execute(writeOK);
245+
executors.execute(writeErrorRecover);
246+
executors.execute(queryOK);
247+
executors.execute(queryErrorRecover);
248+
executors.execute(timer);
249+
250+
// ensure termination
251+
boolean returned;
252+
try {
253+
returned = executors.awaitTermination(runTime + 1, TimeUnit.MINUTES);
254+
} catch (InterruptedException e) {
255+
throw new RuntimeException(e);
256+
}
257+
258+
logger.info("executors terminated cleanly: " + returned);
259+
260+
} catch (Exception e) {
261+
throw new RuntimeException(e);
262+
}
263+
}
264+
}

0 commit comments

Comments
 (0)