This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 7663963b0e8a CAMEL-25610: camel-influxdb2 - Fail the exchange when 
InfluxDB rejects the write (#27797)
7663963b0e8a is described below

commit 7663963b0e8a2c560a8c504d778a67d6295360ca
Author: allthingssecurity <[email protected]>
AuthorDate: Wed Oct 14 17:16:32 2026 +0530

    CAMEL-25610: camel-influxdb2 - Fail the exchange when InfluxDB rejects the 
write (#27797)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/catalog/docs/influxdb2-component.adoc    |  11 ++
 .../src/main/docs/influxdb2-component.adoc         |  11 ++
 .../component/influxdb2/InfluxDb2Producer.java     |  10 +-
 .../InfluxDb2ProducerWriteFailureTest.java         | 213 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  13 ++
 5 files changed, 255 insertions(+), 3 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/influxdb2-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/influxdb2-component.adoc
index 9fb2383ee7d9..9fd774433882 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/influxdb2-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/influxdb2-component.adoc
@@ -85,3 +85,14 @@ YAML::
               bucket: myBucket
 ----
 ====
+
+== Writes are synchronous
+
+The producer writes with the synchronous write API of the InfluxDB client 
(`WriteApiBlocking`): the exchange
+continues when InfluxDB has answered the write request.
+A write that InfluxDB rejects (for example, a line it cannot parse, an unknown 
bucket or a token that is not
+authorized), or a request that cannot be sent, fails the exchange with a 
`CamelInfluxDb2Exception` whose cause is the
+`InfluxException` of the client, so the error handler of the route can 
redeliver it or move it to a dead letter channel.
+
+Each exchange is one write request. To write many points with one request, 
send a `Points`, `Measurements` or
+`Records` body, for example, from an xref:eips:aggregate-eip.adoc[Aggregate] 
EIP.
diff --git a/components/camel-influxdb2/src/main/docs/influxdb2-component.adoc 
b/components/camel-influxdb2/src/main/docs/influxdb2-component.adoc
index 9fb2383ee7d9..9fd774433882 100644
--- a/components/camel-influxdb2/src/main/docs/influxdb2-component.adoc
+++ b/components/camel-influxdb2/src/main/docs/influxdb2-component.adoc
@@ -85,3 +85,14 @@ YAML::
               bucket: myBucket
 ----
 ====
+
+== Writes are synchronous
+
+The producer writes with the synchronous write API of the InfluxDB client 
(`WriteApiBlocking`): the exchange
+continues when InfluxDB has answered the write request.
+A write that InfluxDB rejects (for example, a line it cannot parse, an unknown 
bucket or a token that is not
+authorized), or a request that cannot be sent, fails the exchange with a 
`CamelInfluxDb2Exception` whose cause is the
+`InfluxException` of the client, so the error handler of the route can 
redeliver it or move it to a dead letter channel.
+
+Each exchange is one write request. To write many points with one request, 
send a `Points`, `Measurements` or
+`Records` body, for example, from an xref:eips:aggregate-eip.adoc[Aggregate] 
EIP.
diff --git 
a/components/camel-influxdb2/src/main/java/org/apache/camel/component/influxdb2/InfluxDb2Producer.java
 
b/components/camel-influxdb2/src/main/java/org/apache/camel/component/influxdb2/InfluxDb2Producer.java
index 417414a552d0..ff525fef6a7d 100644
--- 
a/components/camel-influxdb2/src/main/java/org/apache/camel/component/influxdb2/InfluxDb2Producer.java
+++ 
b/components/camel-influxdb2/src/main/java/org/apache/camel/component/influxdb2/InfluxDb2Producer.java
@@ -17,7 +17,7 @@
 package org.apache.camel.component.influxdb2;
 
 import com.influxdb.client.InfluxDBClient;
-import com.influxdb.client.WriteApi;
+import com.influxdb.client.WriteApiBlocking;
 import com.influxdb.client.domain.WritePrecision;
 import com.influxdb.client.write.Point;
 import org.apache.camel.Exchange;
@@ -39,13 +39,17 @@ public class InfluxDb2Producer extends DefaultProducer {
 
     InfluxDBClient connection;
 
-    WriteApi writeApi;
+    /**
+     * The synchronous write API of the client: a write returns when InfluxDB 
has answered and throws when the answer is
+     * an error or the request could not be sent, so the outcome of the write 
is the outcome of the exchange.
+     */
+    WriteApiBlocking writeApi;
 
     public InfluxDb2Producer(InfluxDb2Endpoint endpoint) {
         super(endpoint);
         this.connection = endpoint.getInfluxDBClient();
         this.endpoint = endpoint;
-        this.writeApi = connection.makeWriteApi();
+        this.writeApi = connection.getWriteApiBlocking();
     }
 
     /**
diff --git 
a/components/camel-influxdb2/src/test/java/org/apache/camel/component/influxdb2/InfluxDb2ProducerWriteFailureTest.java
 
b/components/camel-influxdb2/src/test/java/org/apache/camel/component/influxdb2/InfluxDb2ProducerWriteFailureTest.java
new file mode 100644
index 000000000000..4242cc7e889a
--- /dev/null
+++ 
b/components/camel-influxdb2/src/test/java/org/apache/camel/component/influxdb2/InfluxDb2ProducerWriteFailureTest.java
@@ -0,0 +1,213 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.influxdb2;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.influxdb.client.InfluxDBClient;
+import com.influxdb.client.InfluxDBClientFactory;
+import com.influxdb.client.write.Point;
+import com.influxdb.exceptions.InfluxException;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.influxdb2.data.Record;
+import org.apache.camel.component.influxdb2.data.Records;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The producer writes with the real InfluxDB client against a local HTTP 
server that plays the write API of InfluxDB
+ * 2.x: a write that the server rejects, or that never reaches it, must fail 
the exchange, and an accepted write must be
+ * at the server when the exchange completes.
+ */
+class InfluxDb2ProducerWriteFailureTest extends CamelTestSupport {
+
+    private static final String ORG_ID = "00000000000000a1";
+
+    private final AtomicInteger writeStatus = new AtomicInteger(204);
+    private final List<String> writes = new CopyOnWriteArrayList<>();
+    private final List<String> writeQueries = new CopyOnWriteArrayList<>();
+
+    private HttpServer server;
+    private InfluxDBClient client;
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        server = HttpServer.create(new 
InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
+        server.createContext("/api/v2/orgs",
+                exchange -> respond(exchange, 200, "{\"orgs\":[{\"id\":\"" + 
ORG_ID + "\",\"name\":\"myOrg\"}]}"));
+        server.createContext("/api/v2/buckets",
+                exchange -> respond(exchange, 200,
+                        
"{\"buckets\":[{\"id\":\"00000000000000b1\",\"orgID\":\"" + ORG_ID
+                                                   + 
"\",\"name\":\"myBucket\",\"retentionRules\":[]}]}"));
+        server.createContext("/api/v2/write", exchange -> {
+            int status = writeStatus.get();
+            String lines = new 
String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
+            if (status == 204) {
+                
writeQueries.add(String.valueOf(exchange.getRequestURI().getQuery()));
+                writes.add(lines);
+                respond(exchange, 204, null);
+            } else {
+                respond(exchange, status, 
"{\"code\":\"rejected\",\"message\":\"write rejected with " + status + "\"}");
+            }
+        });
+        server.start();
+
+        client = InfluxDBClientFactory.create("http://127.0.0.1:"; + 
server.getAddress().getPort(), "token".toCharArray());
+
+        CamelContext context = super.createCamelContext();
+        context.getRegistry().bind("influxDbBean", client);
+        return context;
+    }
+
+    @Override
+    protected void cleanupResources() {
+        if (server != null) {
+            server.stop(0);
+        }
+        if (client != null) {
+            client.close();
+        }
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:write")
+                        .to("influxdb2:influxDbBean?org=myOrg&bucket=myBucket")
+                        .to("mock:after");
+            }
+        };
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = { 400, 401, 404, 503 })
+    void pointRejectedByTheServerFailsTheExchange(int status) throws Exception 
{
+        writeStatus.set(status);
+        MockEndpoint after = getMockEndpoint("mock:after");
+        after.expectedMessageCount(0);
+
+        Exchange exchange = template.send("direct:write", e -> 
e.getIn().setBody(point()));
+
+        CamelInfluxDb2Exception failure
+                = assertInstanceOf(CamelInfluxDb2Exception.class, 
exchange.getException(),
+                        "exception of an exchange whose write was answered 
with " + status);
+        InfluxException cause = assertInstanceOf(InfluxException.class, 
failure.getCause());
+        assertEquals(status, cause.status());
+        after.assertIsSatisfied();
+    }
+
+    @Test
+    void recordsRejectedByTheServerFailTheExchange() throws Exception {
+        writeStatus.set(400);
+        MockEndpoint after = getMockEndpoint("mock:after");
+        after.expectedMessageCount(0);
+
+        Exchange exchange = template.send("direct:write",
+                e -> 
e.getIn().setBody(Records.create("temperature,location=north value=60.0")
+                        .addRecord("temperature,location=south value=61.0")));
+
+        assertInstanceOf(CamelInfluxDb2Exception.class, 
exchange.getException(),
+                "exception of an exchange whose records were answered with 
400");
+        after.assertIsSatisfied();
+    }
+
+    @Test
+    void writeToServerThatIsDownFailsTheExchange() throws Exception {
+        server.stop(0);
+        MockEndpoint after = getMockEndpoint("mock:after");
+        after.expectedMessageCount(0);
+
+        Exchange exchange = template.send("direct:write", e -> 
e.getIn().setBody(point()));
+
+        assertInstanceOf(CamelInfluxDb2Exception.class, 
exchange.getException(),
+                "exception of an exchange whose write could not be sent");
+        after.assertIsSatisfied();
+    }
+
+    @Test
+    void acceptedPointIsAtTheServerWhenTheExchangeCompletes() throws Exception 
{
+        MockEndpoint after = getMockEndpoint("mock:after");
+        after.expectedMessageCount(1);
+
+        Exchange exchange = template.send("direct:write", e -> 
e.getIn().setBody(point()));
+
+        assertNull(exchange.getException());
+        assertEquals(1, writes.size(), "write requests at the server when the 
exchange has completed");
+        assertTrue(writes.get(0).startsWith("temperature,location=north 
value=60.0"), writes.get(0));
+        after.assertIsSatisfied();
+    }
+
+    @Test
+    void 
acceptedRecordIsWrittenWithTheOrganizationBucketAndPrecisionOfTheEndpoint() 
throws Exception {
+        MockEndpoint after = getMockEndpoint("mock:after");
+        after.expectedMessageCount(1);
+
+        Exchange exchange = template.send("direct:write",
+                e -> 
e.getIn().setBody(Record.fromString("temperature,location=north value=60.0")));
+
+        assertNull(exchange.getException());
+        assertEquals(List.of("temperature,location=north value=60.0"), writes,
+                "write requests at the server when the exchange has 
completed");
+        // the parameters of the write request, in any order; ms is the 
default writePrecision of the endpoint
+        List<String> parameters = List.of(writeQueries.get(0).split("&"));
+        assertTrue(parameters.contains("org=myOrg"), writeQueries.get(0));
+        assertTrue(parameters.contains("bucket=myBucket"), 
writeQueries.get(0));
+        assertTrue(parameters.contains("precision=ms"), writeQueries.get(0));
+        after.assertIsSatisfied();
+    }
+
+    private static Point point() {
+        return Point.measurement("temperature").addTag("location", 
"north").addField("value", 60.0);
+    }
+
+    private static void respond(HttpExchange exchange, int status, String 
json) throws IOException {
+        // read the request completely before answering
+        exchange.getRequestBody().readAllBytes();
+        if (json == null) {
+            exchange.sendResponseHeaders(status, -1);
+        } else {
+            byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
+            exchange.getResponseHeaders().add("Content-Type", 
"application/json");
+            exchange.sendResponseHeaders(status, bytes.length);
+            try (OutputStream out = exchange.getResponseBody()) {
+                out.write(bytes);
+            }
+        }
+        exchange.close();
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 81ba4af58a03..ff5caf83b25d 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -245,6 +245,19 @@ nothing was stored and the exchange completed without an 
error. The requests are
 the answer like the other state operations: the states are stored, and a 
request that Dapr rejects (or a sidecar that
 cannot be reached) fails the exchange.
 
+=== camel-influxdb2 - a write is synchronous and a rejected write fails the 
exchange
+
+The producer wrote with the batching write API of the InfluxDB client: the 
point was put into a buffer of the client
+and sent later by a background thread, so the exchange completed before 
anything was written, and a write that
+InfluxDB rejected (a line it cannot parse, an unknown bucket, a token that is 
not authorized) or could not receive was
+only logged by the client while the route continued as if the data had been 
stored. The producer now writes with the
+synchronous write API: the exchange continues when InfluxDB has answered, and 
a failed write fails the exchange with a
+`CamelInfluxDb2Exception` (cause: the `InfluxException` of the client), which 
the error handler can redeliver.
+
+Each exchange is now one write request, where the client used to collect the 
points of up to one second into one
+request, and the client no longer retries a write in the background. Routes 
with a high message rate can send a
+`Points`, `Measurements` or `Records` body (for example from an aggregator) to 
write many points with one request.
+
 === camel-jira - the producers wait for the answer of Jira
 
 The `addComment`, `addIssueLink`, `addWorkLog`, `attach`, `deleteIssue`, 
`transitionIssue` and `watchers` producers

Reply via email to