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 2a30744c443a CAMEL-25655: camel-zookeeper - consumer waits for ever
and producer fails every message after the ZooKeeper session expired (#27858)
2a30744c443a is described below
commit 2a30744c443a6d0cd871283993db46fd862de4f8
Author: allthingssecurity <[email protected]>
AuthorDate: Thu Oct 15 17:31:20 2026 +0530
CAMEL-25655: camel-zookeeper - consumer waits for ever and producer fails
every message after the ZooKeeper session expired (#27858)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
components/camel-zookeeper/pom.xml | 14 ++
.../component/zookeeper/ConnectionHolder.java | 81 +++++--
.../component/zookeeper/ZooKeeperConsumer.java | 32 ++-
.../component/zookeeper/ZooKeeperProducer.java | 13 +-
.../operations/ChildrenChangedOperation.java | 4 +-
.../zookeeper/operations/DataChangedOperation.java | 4 +-
.../operations/ExistenceChangedOperation.java | 5 +
.../operations/FutureEventDrivenOperation.java | 43 +++-
.../ZooKeeperConsumerSessionExpiryTest.java | 253 +++++++++++++++++++++
parent/pom.xml | 1 +
10 files changed, 415 insertions(+), 35 deletions(-)
diff --git a/components/camel-zookeeper/pom.xml
b/components/camel-zookeeper/pom.xml
index e7bff0878ebb..05c2cf15e453 100644
--- a/components/camel-zookeeper/pom.xml
+++ b/components/camel-zookeeper/pom.xml
@@ -84,6 +84,20 @@
<artifactId>camel-test-junit6</artifactId>
<scope>test</scope>
</dependency>
+ <!-- the ZooKeeper server that ZooKeeperConsumerSessionExpiryTest runs
in the test JVM needs these two
+ (both are "provided" dependencies of zookeeper) -->
+ <dependency>
+ <groupId>io.dropwizard.metrics</groupId>
+ <artifactId>metrics-core</artifactId>
+ <version>${metrics-version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.xerial.snappy</groupId>
+ <artifactId>snappy-java</artifactId>
+ <version>${snappy-java-version}</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-test-spring-junit6</artifactId>
diff --git
a/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/ConnectionHolder.java
b/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/ConnectionHolder.java
index f52a9419e37d..4037550c69e8 100644
---
a/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/ConnectionHolder.java
+++
b/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/ConnectionHolder.java
@@ -33,31 +33,60 @@ import org.slf4j.LoggerFactory;
public class ConnectionHolder implements Watcher {
private static final Logger LOG =
LoggerFactory.getLogger(ConnectionHolder.class);
- private CountDownLatch connectionLatch = new CountDownLatch(1);
+ private volatile CountDownLatch connectionLatch = new CountDownLatch(1);
private ZooKeeperConfiguration configuration;
- private ZooKeeper zookeeper;
+ private volatile ZooKeeper zookeeper;
public ConnectionHolder(ZooKeeperConfiguration configuration) {
this.configuration = configuration;
}
+ /**
+ * Returns the ZooKeeper handle, and creates one when there is none or
when the current one can no longer be used: a
+ * handle whose session expired (or that was closed) stays closed for
good, so a new handle with a new session is
+ * needed. Waits until the new handle is connected.
+ */
public ZooKeeper getZooKeeper() {
- if (zookeeper != null) {
- return zookeeper;
+ // fast path without the lock: the producer calls this for every
message
+ ZooKeeper current = zookeeper;
+ if (current != null && current.getState().isAlive()) {
+ return current;
}
- if (configuration.getConnectString() == null) {
- throw new RuntimeCamelException(
- "Cannot create ZooKeeper connection as connection string
is null. Have servers been configured?");
- }
- try {
- zookeeper = new ZooKeeper(configuration.getConnectString(),
configuration.getTimeout(), this);
- } catch (Exception e) {
- throw RuntimeCamelException.wrapRuntimeCamelException(e);
+ ZooKeeper created;
+ CountDownLatch latch;
+ synchronized (this) {
+ current = zookeeper;
+ if (current != null) {
+ if (current.getState().isAlive()) {
+ return current;
+ }
+ LOG.info("The ZooKeeper session 0x{} to {} can no longer be
used ({}). Creating a new session.",
+ Long.toHexString(current.getSessionId()),
configuration.getConnectString(), current.getState());
+ closeQuietly(current);
+ zookeeper = null;
+ }
+ if (configuration.getConnectString() == null) {
+ throw new RuntimeCamelException(
+ "Cannot create ZooKeeper connection as connection
string is null. Have servers been configured?");
+ }
+ latch = new CountDownLatch(1);
+ connectionLatch = latch;
+ try {
+ // each handle counts down its own latch, so that a late event
of a previous handle cannot release
+ // the wait for a new one
+ created = new ZooKeeper(configuration.getConnectString(),
configuration.getTimeout(), event -> {
+ process(event, latch);
+ });
+ zookeeper = created;
+ } catch (Exception e) {
+ throw RuntimeCamelException.wrapRuntimeCamelException(e);
+ }
}
- awaitConnection();
- return zookeeper;
+ // wait on the latch of this handle: another thread may already have
replaced the field
+ awaitConnection(latch);
+ return created;
}
public boolean isConnected() {
@@ -65,11 +94,15 @@ public class ConnectionHolder implements Watcher {
}
public void awaitConnection() {
+ awaitConnection(connectionLatch);
+ }
+
+ private void awaitConnection(CountDownLatch latch) {
if (LOG.isDebugEnabled()) {
LOG.debug("Awaiting Connection event from Zookeeper cluster {}",
configuration.getConnectString());
}
try {
- connectionLatch.await();
+ latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw RuntimeCamelException.wrapRuntimeCamelException(e);
@@ -78,13 +111,25 @@ public class ConnectionHolder implements Watcher {
@Override
public void process(WatchedEvent event) {
+ process(event, connectionLatch);
+ }
+
+ private static void process(WatchedEvent event, CountDownLatch latch) {
if (event.getState() == KeeperState.SyncConnected) {
- connectionLatch.countDown();
+ latch.countDown();
+ }
+ latch.countDown();
+ }
+
+ private void closeQuietly(ZooKeeper handle) {
+ try {
+ handle.close();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
}
- connectionLatch.countDown();
}
- public void closeConnection() {
+ public synchronized void closeConnection() {
try {
if (zookeeper != null) {
zookeeper.close();
diff --git
a/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/ZooKeeperConsumer.java
b/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/ZooKeeperConsumer.java
index 9e73fc6cffe5..275c661ecf2e 100644
---
a/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/ZooKeeperConsumer.java
+++
b/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/ZooKeeperConsumer.java
@@ -50,7 +50,7 @@ public class ZooKeeperConsumer extends DefaultConsumer {
private static final Logger LOG =
LoggerFactory.getLogger(ZooKeeperConsumer.class);
private final ZooKeeperConnectionManager zkm;
- private ZooKeeper connection;
+ private volatile ZooKeeper connection;
private ZooKeeperConfiguration configuration;
private LinkedBlockingQueue<ZooKeeperOperation> operations = new
LinkedBlockingQueue<>();
private ExecutorService executor;
@@ -154,6 +154,7 @@ public class ZooKeeperConsumer extends DefaultConsumer {
continue;
}
String node = current.getNode();
+ boolean restarted = false;
try {
OperationResult result = current.get();
if (ZooKeeperUtils.hasWatchedEvent(current)) {
@@ -166,12 +167,14 @@ public class ZooKeeperConsumer extends DefaultConsumer {
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
handleException(e);
- backoffAndThenRestart();
+ restarted = backoffAndThenRestart();
} catch (Exception e) {
handleException(e);
- backoffAndThenRestart();
+ restarted = backoffAndThenRestart();
} finally {
- if (configuration.isRepeat()) {
+ // after a restart the queue holds the operations of the
new sequence; a copy of the failed one
+ // (which may use a handle whose session expired) must not
be added to them
+ if (configuration.isRepeat() && !restarted) {
try {
operations.offer(current.createCopy());
} catch (Exception e) {
@@ -182,7 +185,13 @@ public class ZooKeeperConsumer extends DefaultConsumer {
}
}
- private void backoffAndThenRestart() {
+ /**
+ * Waits for the backoff, then starts the sequence of operations again
on the current connection, which is a new
+ * one when the session of the previous one expired.
+ *
+ * @return true when the sequence was started again
+ */
+ private boolean backoffAndThenRestart() {
try {
if (isRunAllowed()) {
// Use Camel's task API for reconnection backoff delay
instead of Thread.sleep()
@@ -199,11 +208,22 @@ public class ZooKeeperConsumer extends DefaultConsumer {
.build()
.run(getEndpoint().getCamelContext(), () -> true);
- initializeConsumer();
+ if (isRunAllowed()) {
+ connection = zkm.getConnection();
+ if (!isRunAllowed()) {
+ // stopped while the connection was created:
doStop may already have closed the holder,
+ // so close the new handle (and its client
threads) here
+ zkm.shutdown();
+ return false;
+ }
+ initializeConsumer();
+ return true;
+ }
}
} catch (Exception e) {
// ignore
}
+ return false;
}
}
diff --git
a/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/ZooKeeperProducer.java
b/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/ZooKeeperProducer.java
index aad9c2d99ffb..e8c01cba654c 100644
---
a/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/ZooKeeperProducer.java
+++
b/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/ZooKeeperProducer.java
@@ -66,10 +66,11 @@ public class ZooKeeperProducer extends DefaultProducer {
@Override
public void process(Exchange exchange) throws Exception {
- if (connection == null) {
- connection = this.zkm.getConnection();
- }
- ProductionContext context = new ProductionContext(connection,
exchange);
+ // a new connection when the session of the previous one expired; a
local variable keeps this message on the
+ // handle it got here, as other threads may send concurrently
+ ZooKeeper zk = this.zkm.getConnection();
+ connection = zk;
+ ProductionContext context = new ProductionContext(zk, exchange);
String operation =
exchange.getIn().getHeader(ZooKeeperMessage.ZOOKEEPER_OPERATION, String.class);
boolean isDelete = ZK_OPERATION_DELETE.equals(operation);
@@ -94,9 +95,9 @@ public class ZooKeeperProducer extends DefaultProducer {
}
} else {
if (isDelete) {
- asynchronouslyDeleteNode(connection, context);
+ asynchronouslyDeleteNode(zk, context);
} else {
- asynchronouslySetDataOnNode(connection, context);
+ asynchronouslySetDataOnNode(zk, context);
}
}
diff --git
a/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/operations/ChildrenChangedOperation.java
b/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/operations/ChildrenChangedOperation.java
index 1600ab35900d..b460b8760a3f 100644
---
a/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/operations/ChildrenChangedOperation.java
+++
b/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/operations/ChildrenChangedOperation.java
@@ -41,8 +41,8 @@ public class ChildrenChangedOperation extends
FutureEventDrivenOperation<List<St
@Override
protected void installWatch() {
- connection.getChildren(getNode(), this, (int rc, String path, Object
ctx, List<String> children, Stat stat) -> {
- }, null);
+ connection.getChildren(getNode(), this,
+ (int rc, String path, Object ctx, List<String> children, Stat
stat) -> watchRequestCompleted(rc), null);
}
@Override
diff --git
a/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/operations/DataChangedOperation.java
b/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/operations/DataChangedOperation.java
index ed9d5a3264ea..85fe2f325822 100644
---
a/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/operations/DataChangedOperation.java
+++
b/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/operations/DataChangedOperation.java
@@ -44,8 +44,8 @@ public class DataChangedOperation extends
FutureEventDrivenOperation<byte[]> {
@Override
protected void installWatch() {
- connection.getData(getNode(), this, (int rc, String path, Object ctx,
byte[] data, Stat stat) -> {
- }, null);
+ connection.getData(getNode(), this,
+ (int rc, String path, Object ctx, byte[] data, Stat stat) ->
watchRequestCompleted(rc), null);
}
@Override
diff --git
a/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/operations/ExistenceChangedOperation.java
b/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/operations/ExistenceChangedOperation.java
index 24c4cd54cbaf..13a1f9a62369 100644
---
a/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/operations/ExistenceChangedOperation.java
+++
b/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/operations/ExistenceChangedOperation.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.component.zookeeper.operations;
+import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
@@ -33,6 +34,10 @@ public class ExistenceChangedOperation extends
FutureEventDrivenOperation<String
@Override
protected void installWatch() {
connection.exists(getNode(), this, (int rc, String path, Object ctx,
Stat stat) -> {
+ // a node that does not exist is what this operation waits for:
ZooKeeper registers the watch then too
+ if (rc != KeeperException.Code.NONODE.intValue()) {
+ watchRequestCompleted(rc);
+ }
}, null);
if (LOG.isDebugEnabled()) {
LOG.debug("Installed exists watch");
diff --git
a/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/operations/FutureEventDrivenOperation.java
b/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/operations/FutureEventDrivenOperation.java
index 771ca5627978..86a819c6ca3e 100644
---
a/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/operations/FutureEventDrivenOperation.java
+++
b/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/operations/FutureEventDrivenOperation.java
@@ -22,9 +22,11 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
+import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooKeeper;
import static java.lang.String.format;
@@ -42,6 +44,8 @@ public abstract class FutureEventDrivenOperation<ResultType>
extends ZooKeeperOp
private WatchedEvent event;
+ private volatile ExecutionException failure;
+
protected FutureEventDrivenOperation(ZooKeeper connection, String node,
EventType... awaitedTypes) {
super(connection, node);
this.awaitedTypes = awaitedTypes;
@@ -55,6 +59,16 @@ public abstract class FutureEventDrivenOperation<ResultType>
extends ZooKeeperOp
LOG.debug(format("Received event of type %s for node '%s'",
received, event.getPath()));
}
+ // When the session expires, ZooKeeper tells every watcher with an
event of type None and state Expired. The
+ // awaited event can no longer come (the handle is closed for good),
so stop waiting and fail.
+ if (received == EventType.None && event.getState() ==
KeeperState.Expired) {
+ failure = new ExecutionException(
+ "The ZooKeeper session expired while waiting for an event
on node '" + getNode() + "'",
+ new KeeperException.SessionExpiredException());
+ waitForAnyWatchedType.countDown();
+ return;
+ }
+
for (EventType watched : awaitedTypes) {
if (watched.equals(received)) {
result = getResult();
@@ -74,6 +88,7 @@ public abstract class FutureEventDrivenOperation<ResultType>
extends ZooKeeperOp
installWatch();
waitingThreads.add(Thread.currentThread());
waitForAnyWatchedType.await();
+ throwIfFailed();
return result;
}
@@ -83,13 +98,39 @@ public abstract class
FutureEventDrivenOperation<ResultType> extends ZooKeeperOp
installWatch();
waitingThreads.add(Thread.currentThread());
waitForAnyWatchedType.await(timeout, unit);
+ throwIfFailed();
return result;
}
+ private void throwIfFailed() throws ExecutionException {
+ ExecutionException e = failure;
+ if (e != null) {
+ throw e;
+ }
+ }
+
+ /**
+ * To be called from the asynchronous callback of the request made by
{@link #installWatch()}, with its result code.
+ * When the request failed (for example with SESSIONEXPIRED on a handle
whose session expired, or CONNECTIONLOSS),
+ * ZooKeeper did not register the watch, so the awaited event will never
come: the operation stops waiting and fails
+ * with the {@link KeeperException} of that result code.
+ *
+ * @param rc the result code of the request
+ */
+ protected void watchRequestCompleted(int rc) {
+ if (rc != KeeperException.Code.OK.intValue()) {
+ failure = new ExecutionException(
+ "Could not watch node '" + getNode() + "'",
+ KeeperException.create(KeeperException.Code.get(rc),
getNode()));
+ waitForAnyWatchedType.countDown();
+ }
+ }
+
/**
* Install the watcher to receive {@link WatchedEvent}s. It should use the
appropriate asynchronous ZooKeeper call
* to do this so as not to block the route from starting. Once one of the
watched for types of event is received a
- * call is made to getResult, which can use the appropriate synchronous
call to retrieve the actual data.
+ * call is made to getResult, which can use the appropriate synchronous
call to retrieve the actual data. The
+ * callback of the asynchronous call must pass its result code to {@link
#watchRequestCompleted(int)}.
*/
protected abstract void installWatch();
diff --git
a/components/camel-zookeeper/src/test/java/org/apache/camel/component/zookeeper/ZooKeeperConsumerSessionExpiryTest.java
b/components/camel-zookeeper/src/test/java/org/apache/camel/component/zookeeper/ZooKeeperConsumerSessionExpiryTest.java
new file mode 100644
index 000000000000..16c87f767270
--- /dev/null
+++
b/components/camel-zookeeper/src/test/java/org/apache/camel/component/zookeeper/ZooKeeperConsumerSessionExpiryTest.java
@@ -0,0 +1,253 @@
+/*
+ * 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.zookeeper;
+
+import java.io.File;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import
org.apache.camel.component.zookeeper.operations.FutureEventDrivenOperation;
+import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.apache.zookeeper.CreateMode;
+import org.apache.zookeeper.Watcher.Event.KeeperState;
+import org.apache.zookeeper.ZooDefs.Ids;
+import org.apache.zookeeper.ZooKeeper;
+import org.apache.zookeeper.server.NIOServerCnxnFactory;
+import org.apache.zookeeper.server.ZooKeeperServer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A ZooKeeper consumer must keep consuming after the server expired its
session. The server runs in the test JVM. The
+ * session is expired the usual way: a second client opens the same session
and closes it; the consumer's client then
+ * gets the "Expired" event when it reconnects, and its handle is closed for
good. Every wait is bounded (20 seconds).
+ */
+class ZooKeeperConsumerSessionExpiryTest extends CamelTestSupport {
+
+ private static final String NODE = "/camel-session-expiry";
+ private static final String BLOCKED_NODE = "/camel-session-expiry-blocked";
+ private static final String ONCE_NODE = "/camel-session-expiry-once";
+
+ @TempDir
+ Path dataDir;
+
+ private final int port = AvailablePortFinder.getNextAvailable();
+
+ private final CountDownLatch processing = new CountDownLatch(1);
+ private final CountDownLatch release = new CountDownLatch(1);
+
+ private ZooKeeperServer server;
+ private NIOServerCnxnFactory factory;
+ private ZooKeeper client;
+
+ @Override
+ protected void doPreSetup() throws Exception {
+ File dir = dataDir.toFile();
+ server = new ZooKeeperServer(dir, dir, 2000);
+ factory = new NIOServerCnxnFactory();
+ factory.configure(new InetSocketAddress("127.0.0.1", port), 60);
+ factory.startup(server);
+ client = connect(0, null);
+ client.create(NODE, bytes("one"), Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
+ client.create(BLOCKED_NODE, bytes("one"), Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
+ client.create(ONCE_NODE, bytes("one"), Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
+ }
+
+ @Override
+ protected void doPostTearDown() throws Exception {
+ release.countDown();
+ if (client != null) {
+ client.close();
+ }
+ if (factory != null) {
+ factory.shutdown();
+ }
+ if (server != null) {
+ server.shutdown();
+ }
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ // each test starts the route it needs, so that only one
consumer runs at a time
+ from("zookeeper://127.0.0.1:" + port + NODE +
"?repeat=true&backoff=200").routeId("watch")
+ .autoStartup(false)
+ .to("mock:data");
+
+ // the first exchange holds the operations thread of the
consumer until the test releases it
+ from("zookeeper://127.0.0.1:" + port + BLOCKED_NODE +
"?repeat=true&backoff=200").routeId("blocked")
+ .autoStartup(false)
+ .process(e -> {
+ if (processing.getCount() > 0) {
+ processing.countDown();
+ assertTrue(release.await(20,
TimeUnit.SECONDS), "the test did not release the route");
+ }
+ })
+ .to("mock:blocked");
+
+ from("zookeeper://127.0.0.1:" + port + ONCE_NODE +
"?backoff=200").routeId("once")
+ .autoStartup(false)
+ .to("mock:once");
+ }
+ };
+ }
+
+ @Test
+ void consumesAgainAfterTheSessionExpired() throws Exception {
+ context.getRouteController().startRoute("watch");
+ MockEndpoint mock = getMockEndpoint("mock:data");
+ await().atMost(20, TimeUnit.SECONDS).until(() -> received(mock,
"one"));
+
+ ZooKeeperEndpoint endpoint = (ZooKeeperEndpoint)
context.getRoute("watch").getEndpoint();
+ ZooKeeper consumerHandle =
endpoint.getConnectionManager().getConnection();
+ expire(consumerHandle);
+ await().atMost(20, TimeUnit.SECONDS).until(() ->
!consumerHandle.getState().isAlive());
+
+ client.setData(NODE, bytes("two"), -1);
+
+ await().atMost(20, TimeUnit.SECONDS).untilAsserted(() ->
assertTrue(received(mock, "two"),
+ "the consumer did not consume the update made after its
session had expired"));
+
+ // "two" may come from the read of the restarted sequence; "three" can
only come from its new watch
+ updateWhenTheConsumerWaitsForAChange(endpoint, NODE, "three");
+ await().atMost(20, TimeUnit.SECONDS).untilAsserted(() ->
assertTrue(received(mock, "three"),
+ "the consumer did not watch the node again on its new
session"));
+ }
+
+ @Test
+ void consumesAgainAfterTheSessionExpiredWhileAnExchangeWasProcessed()
throws Exception {
+ context.getRouteController().startRoute("blocked");
+ assertTrue(processing.await(20, TimeUnit.SECONDS), "the route did not
receive the first data");
+
+ // the session expires while the operations thread routes the first
exchange: no watch of the consumer is
+ // registered at that time, so the consumer gets no Expired event; the
next watch request fails instead
+ ZooKeeperEndpoint endpoint = (ZooKeeperEndpoint)
context.getRoute("blocked").getEndpoint();
+ ZooKeeper consumerHandle =
endpoint.getConnectionManager().getConnection();
+ expire(consumerHandle);
+ await().atMost(20, TimeUnit.SECONDS).until(() ->
!consumerHandle.getState().isAlive());
+ client.setData(BLOCKED_NODE, bytes("two"), -1);
+ release.countDown();
+
+ MockEndpoint mock = getMockEndpoint("mock:blocked");
+ await().atMost(20, TimeUnit.SECONDS).untilAsserted(() ->
assertTrue(received(mock, "two"),
+ "the consumer did not consume the update made after its
session had expired during an exchange"));
+
+ updateWhenTheConsumerWaitsForAChange(endpoint, BLOCKED_NODE, "three");
+ await().atMost(20, TimeUnit.SECONDS).untilAsserted(() ->
assertTrue(received(mock, "three"),
+ "the consumer did not watch the node again on its new
session"));
+ }
+
+ @Test
+ void emitsTheCurrentDataAgainAfterTheSessionExpiredWithoutRepeat() throws
Exception {
+ context.getRouteController().startRoute("once");
+ MockEndpoint mock = getMockEndpoint("mock:once");
+ await().atMost(20, TimeUnit.SECONDS).until(() ->
mock.getReceivedCounter() == 1);
+
+ ZooKeeperEndpoint endpoint = (ZooKeeperEndpoint)
context.getRoute("once").getEndpoint();
+ ZooKeeper consumerHandle =
endpoint.getConnectionManager().getConnection();
+ expire(consumerHandle);
+
+ // the consumer starts its sequence again on a new session, which
reads the (unchanged) data once more
+ await().atMost(20, TimeUnit.SECONDS).untilAsserted(() ->
assertEquals(2, mock.getReceivedCounter(),
+ "the consumer did not read the data again after its session
had expired"));
+ assertTrue(mock.getReceivedExchanges().stream()
+ .allMatch(e -> "one".equals(new
String(e.getIn().getBody(byte[].class), StandardCharsets.UTF_8))));
+ }
+
+ @Test
+ void producerWritesAgainAfterTheSessionExpired() throws Exception {
+ template.sendBody("zookeeper://127.0.0.1:" + port + NODE, "three");
+ ZooKeeperEndpoint endpoint =
context.getEndpoint("zookeeper://127.0.0.1:" + port + NODE,
ZooKeeperEndpoint.class);
+ ZooKeeper producerHandle =
endpoint.getConnectionManager().getConnection();
+ expire(producerHandle);
+ await().atMost(20, TimeUnit.SECONDS).until(() ->
!producerHandle.getState().isAlive());
+
+ Exchange reply = template.request("zookeeper://127.0.0.1:" + port +
NODE, e -> e.getIn().setBody("four"));
+ assertNull(reply.getException(), "the producer failed after its
session had expired");
+ assertEquals("four", new String(client.getData(NODE, false, null),
StandardCharsets.UTF_8));
+ }
+
+ /**
+ * Waits until the consumer's operations thread waits for a change of a
node (its watch request is sent), then
+ * updates the node through the consumer's own session: ZooKeeper handles
the requests of one session in order, so
+ * the update comes after the watch is registered and the change cannot be
missed.
+ */
+ private static void updateWhenTheConsumerWaitsForAChange(ZooKeeperEndpoint
endpoint, String node, String data)
+ throws Exception {
+ await().atMost(20,
TimeUnit.SECONDS).until(ZooKeeperConsumerSessionExpiryTest::operationsThreadWaitsForAChange);
+ endpoint.getConnectionManager().getConnection().setData(node,
bytes(data), -1);
+ }
+
+ private static boolean operationsThreadWaitsForAChange() {
+ return Thread.getAllStackTraces().entrySet().stream()
+ .filter(e -> e.getKey().getName().contains("Camel-Zookeeper
OperationsExecutor"))
+ .filter(e -> e.getKey().getState() == Thread.State.WAITING)
+ .anyMatch(e -> Arrays.stream(e.getValue())
+ .anyMatch(f ->
FutureEventDrivenOperation.class.getName().equals(f.getClassName())
+ && "get".equals(f.getMethodName())));
+ }
+
+ private static boolean received(MockEndpoint mock, String body) {
+ return mock.getReceivedExchanges().stream()
+ .anyMatch(e -> body.equals(new
String(e.getIn().getBody(byte[].class), StandardCharsets.UTF_8)));
+ }
+
+ private ZooKeeper connect(long sessionId, byte[] password) throws
Exception {
+ CountDownLatch connected = new CountDownLatch(1);
+ ZooKeeper zk = password == null
+ ? new ZooKeeper("127.0.0.1:" + port, 10000, e ->
countDownWhenConnected(e.getState(), connected))
+ : new ZooKeeper(
+ "127.0.0.1:" + port, 10000, e ->
countDownWhenConnected(e.getState(), connected), sessionId,
+ password);
+ assertTrue(connected.await(20, TimeUnit.SECONDS), "the test client did
not connect");
+ return zk;
+ }
+
+ private static void countDownWhenConnected(KeeperState state,
CountDownLatch connected) {
+ if (state == KeeperState.SyncConnected) {
+ connected.countDown();
+ }
+ }
+
+ /**
+ * Closes the session of the given handle from a second client, so that
the server expires it for the handle.
+ */
+ private void expire(ZooKeeper handle) throws Exception {
+ ZooKeeper other = connect(handle.getSessionId(),
handle.getSessionPasswd());
+ other.close();
+ }
+
+ private static byte[] bytes(String s) {
+ return s.getBytes(StandardCharsets.UTF_8);
+ }
+}
diff --git a/parent/pom.xml b/parent/pom.xml
index e9a4b88d7dc0..bcbddcb349ef 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -501,6 +501,7 @@
<smooks-version>2.2.1</smooks-version>
<snakeyaml-version>2.7</snakeyaml-version>
<snakeyaml-engine-version>3.2</snakeyaml-engine-version>
+ <snappy-java-version>1.1.10.8</snappy-java-version>
<snmp4j-version>3.13.1</snmp4j-version>
<solr-version>10.0.0</solr-version>
<splunk-version>1.9.5_1</splunk-version>