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 28f3170dcc6a CAMEL-26887: camel-zookeeper-master -
ZookeeperGroupSupport closes only what it created, and reads the
zookeeper.password property (#29791)
28f3170dcc6a is described below
commit 28f3170dcc6a16ac2d1b89e242bc4589ba5b8b65
Author: allthingssecurity <[email protected]>
AuthorDate: Fri Nov 13 20:23:36 2026 +0530
CAMEL-26887: camel-zookeeper-master - ZookeeperGroupSupport closes only
what it created, and reads the zookeeper.password property (#29791)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../catalog/docs/zookeeper-master-component.adoc | 2 +
.../src/main/docs/zookeeper-master-component.adoc | 2 +
.../zookeepermaster/ZookeeperGroupSupport.java | 30 +++-
.../ZookeeperGroupSupportLifecycleTest.java | 184 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 11 ++
5 files changed, 225 insertions(+), 4 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/zookeeper-master-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/zookeeper-master-component.adoc
index 5f67f1d03de9..857e0803fc14 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/zookeeper-master-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/zookeeper-master-component.adoc
@@ -135,6 +135,8 @@ However, you can also configure the url of the ZooKeeper
ensemble using environm
export ZOOKEEPER_URL = "myzookeeper:2181"
----
+When no `zooKeeperPassword` is configured, the password can also be set with
the `zookeeper.password` system property.
+
=== Master RoutePolicy
You can also use a `RoutePolicy` to control routes in master/slave mode.
diff --git
a/components/camel-zookeeper-master/src/main/docs/zookeeper-master-component.adoc
b/components/camel-zookeeper-master/src/main/docs/zookeeper-master-component.adoc
index 5f67f1d03de9..857e0803fc14 100644
---
a/components/camel-zookeeper-master/src/main/docs/zookeeper-master-component.adoc
+++
b/components/camel-zookeeper-master/src/main/docs/zookeeper-master-component.adoc
@@ -135,6 +135,8 @@ However, you can also configure the url of the ZooKeeper
ensemble using environm
export ZOOKEEPER_URL = "myzookeeper:2181"
----
+When no `zooKeeperPassword` is configured, the password can also be set with
the `zookeeper.password` system property.
+
=== Master RoutePolicy
You can also use a `RoutePolicy` to control routes in master/slave mode.
diff --git
a/components/camel-zookeeper-master/src/main/java/org/apache/camel/component/zookeepermaster/ZookeeperGroupSupport.java
b/components/camel-zookeeper-master/src/main/java/org/apache/camel/component/zookeepermaster/ZookeeperGroupSupport.java
index ec5fe0b5fbf0..4a0120331ddd 100644
---
a/components/camel-zookeeper-master/src/main/java/org/apache/camel/component/zookeepermaster/ZookeeperGroupSupport.java
+++
b/components/camel-zookeeper-master/src/main/java/org/apache/camel/component/zookeepermaster/ZookeeperGroupSupport.java
@@ -59,6 +59,10 @@ public class ZookeeperGroupSupport<T extends NodeState>
extends ServiceSupport
// the component, or the "curator" bean of the registry, which others
share), so the listener is removed from it
// when this stops
private volatile CuratorFramework listenedCurator;
+ // the client that call() created, and the group factory that doStart
created: only these are closed when this stops
+ // (a client or a group factory of the registry, or set by the user, may
be shared, and is kept)
+ private volatile CuratorFramework createdCurator;
+ private volatile ManagedGroupFactory createdManagedGroupFactory;
@Metadata(defaultValue = "10000")
private int maximumConnectionTimeout = 10 * 1000;
@Metadata(defaultValue = "localhost:2181")
@@ -194,6 +198,7 @@ public class ZookeeperGroupSupport<T extends NodeState>
extends ServiceSupport
if (managedGroupFactory == null) {
setManagedGroupFactory(getManagedGroupFactoryStrategy().createGroupFactory(curator,
getClass().getClassLoader(),
getCamelContext(), this));
+ createdManagedGroupFactory = managedGroupFactory;
}
}
@@ -215,7 +220,7 @@ public class ZookeeperGroupSupport<T extends NodeState>
extends ServiceSupport
}
String password = getZooKeeperPassword();
if (password == null) {
- System.getProperty(ZOOKEEPER_PASSWORD);
+ password = System.getProperty(ZOOKEEPER_PASSWORD);
}
LOG.info("Creating new CuratorFramework with connection: {}",
connectString);
CuratorFrameworkFactory.Builder builder =
CuratorFrameworkFactory.builder()
@@ -228,6 +233,7 @@ public class ZookeeperGroupSupport<T extends NodeState>
extends ServiceSupport
}
curator = builder.build();
+ createdCurator = curator;
LOG.debug("Starting CuratorFramework {}", curator);
curator.start();
return curator;
@@ -236,11 +242,27 @@ public class ZookeeperGroupSupport<T extends NodeState>
extends ServiceSupport
@Override
protected void doStop() throws Exception {
try {
- if (managedGroupFactory != null) {
- managedGroupFactory.close();
- managedGroupFactory = null;
+ // close only the group factory this created (the one of
DefaultGroupFactoryStrategy also closes the client
+ // that call() created): a factory of the registry may be shared
by other consumers and route policies
+ ManagedGroupFactory created = createdManagedGroupFactory;
+ createdManagedGroupFactory = null;
+ if (created != null) {
+ if (created == managedGroupFactory) {
+ managedGroupFactory = null;
+ }
+ created.close();
}
} finally {
+ // the client call() created is closed (again, if its group
factory did): a start after this stop creates a
+ // new one
+ CuratorFramework client = createdCurator;
+ createdCurator = null;
+ if (client != null) {
+ if (client == curator) {
+ curator = null;
+ }
+ client.close();
+ }
// the client may be shared (the curator of the component, or the
"curator" bean of the registry): a listener
// left on it would keep this (and the consumer or route policy
that created it) for as long as the client
// lives, one more at each start
diff --git
a/components/camel-zookeeper-master/src/test/java/org/apache/camel/component/zookeepermaster/ZookeeperGroupSupportLifecycleTest.java
b/components/camel-zookeeper-master/src/test/java/org/apache/camel/component/zookeepermaster/ZookeeperGroupSupportLifecycleTest.java
new file mode 100644
index 000000000000..3cd35799d98f
--- /dev/null
+++
b/components/camel-zookeeper-master/src/test/java/org/apache/camel/component/zookeepermaster/ZookeeperGroupSupportLifecycleTest.java
@@ -0,0 +1,184 @@
+/*
+ * 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.zookeepermaster;
+
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.component.zookeepermaster.group.Group;
+import org.apache.camel.component.zookeepermaster.group.ManagedGroupFactory;
+import org.apache.camel.component.zookeepermaster.group.NodeState;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.apache.curator.framework.AuthInfo;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.imps.CuratorFrameworkState;
+import org.apache.curator.retry.RetryOneTime;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * The client and the group factory of a ZookeeperGroupSupport across a stop
and a start (no ZooKeeper: the client is
+ * created and started, and does not connect).
+ */
+class ZookeeperGroupSupportLifecycleTest extends CamelTestSupport {
+
+ ZookeeperGroupSupportLifecycleTest() {
+ testConfiguration().withUseRouteBuilder(false);
+ }
+
+ @Test
+ void testCreatedClientIsNotReusedAfterAStop() {
+ ZookeeperGroupSupport<CamelNodeState> support = newSupport();
+ support.start();
+ CuratorFramework first = support.getCurator();
+ assertEquals(CuratorFrameworkState.STARTED, first.getState());
+
+ support.stop();
+ assertEquals(CuratorFrameworkState.STOPPED, first.getState());
+
+ // started again, it creates a new client, not the one its stop closed
+ support.start();
+ try {
+ CuratorFramework second = support.getCurator();
+ assertNotSame(first, second);
+ assertEquals(CuratorFrameworkState.STARTED, second.getState());
+ } finally {
+ support.stop();
+ }
+ }
+
+ @Test
+ void testClientSetByTheUserIsKeptAfterAStop() throws Exception {
+ CuratorFramework client =
CuratorFrameworkFactory.builder().connectString("localhost:1")
+ .retryPolicy(new RetryOneTime(100)).build();
+ client.start();
+ try {
+ ZookeeperGroupSupport<CamelNodeState> support = newSupport();
+ support.setCurator(client);
+ support.start();
+ assertSame(client, support.getCurator());
+ support.stop();
+
+ // the client of the user is not closed, and is used again by the
next start
+ assertEquals(CuratorFrameworkState.STARTED, client.getState());
+ support.start();
+ assertSame(client, support.getCurator());
+ support.stop();
+ assertEquals(CuratorFrameworkState.STARTED, client.getState());
+ } finally {
+ client.close();
+ }
+ }
+
+ @Test
+ void testGroupFactoryOfTheRegistryIsNotClosedByAStop() {
+ FakeGroupFactory factory = new FakeGroupFactory();
+ context.getRegistry().bind("groupFactory", factory);
+
+ ZookeeperGroupSupport<CamelNodeState> support = newSupport();
+ support.start();
+ assertSame(factory, support.getManagedGroupFactory());
+ support.stop();
+
+ // the factory of the registry may be shared by other consumers and
route policies: it stays open
+ assertEquals(0, factory.closes.get());
+ support.start();
+ assertSame(factory, support.getManagedGroupFactory());
+ support.stop();
+ assertEquals(0, factory.closes.get());
+ }
+
+ @Test
+ void testPasswordFromTheSystemProperty() throws Exception {
+ String old = System.getProperty("zookeeper.password");
+ System.setProperty("zookeeper.password", "secret");
+ CuratorFramework curator = null;
+ try {
+ ZookeeperGroupSupport<CamelNodeState> support = newSupport();
+ curator = support.call();
+
+ // reads the private authInfos field of CuratorFrameworkImpl
(curator-framework 5.9.x) by reflection: this
+ // fails loudly when a Curator upgrade renames or changes the field
+ Field field = curator.getClass().getDeclaredField("authInfos");
+ field.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ List<AuthInfo> authInfos = (List<AuthInfo>) field.get(curator);
+ assertEquals(1, authInfos.size());
+ assertEquals("digest", authInfos.get(0).getScheme());
+ assertEquals("fabric:secret", new
String(authInfos.get(0).getAuth(), StandardCharsets.UTF_8));
+ } finally {
+ if (curator != null) {
+ curator.close();
+ }
+ if (old == null) {
+ System.clearProperty("zookeeper.password");
+ } else {
+ System.setProperty("zookeeper.password", old);
+ }
+ }
+ }
+
+ private ZookeeperGroupSupport<CamelNodeState> newSupport() {
+ ZookeeperGroupSupport<CamelNodeState> support = new
ZookeeperGroupSupport<>();
+ support.setCamelContext(context);
+ // no ZooKeeper there: the client starts and does not connect
+ support.setZooKeeperUrl("localhost:1");
+ support.setMaximumConnectionTimeout(100);
+ return support;
+ }
+
+ private static final class FakeGroupFactory implements ManagedGroupFactory
{
+ final AtomicInteger closes = new AtomicInteger();
+
+ @Override
+ public <T extends NodeState> Group<T> createGroup(String path,
Class<T> clazz) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T extends NodeState> Group<T> createGroup(String path,
Class<T> clazz, ThreadFactory threadFactory) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T extends NodeState> Group<T> createMultiGroup(String path,
Class<T> clazz) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T extends NodeState> Group<T> createMultiGroup(String path,
Class<T> clazz, ThreadFactory threadFactory) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public CuratorFramework getCurator() {
+ return null;
+ }
+
+ @Override
+ public void close() {
+ closes.incrementAndGet();
+ }
+ }
+}
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 1f74fee69eee..c50fc5208253 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
@@ -3994,6 +3994,17 @@ the consumer of such a route kept running next to the
one of the master. This in
consume while the node cannot reach ZooKeeper. Setting the route to
`autoStartup=false` is still recommended, so the
consumer does not run for a moment when the CamelContext starts.
+=== camel-zookeeper-master - the zookeeper.password system property, and a
group factory of the application
+
+The `zookeeper.password` system property is now used when no
`zooKeeperPassword` is configured: the ZooKeeper client
+that the component creates then sends the `digest` authorization
`fabric:<password>`. Before, the property was read but
+not used, so a deployment that set it connected without that authorization.
+
+A `ManagedGroupFactory` bound in the registry, or set with
`setManagedGroupFactory`, is no longer closed when a
+`zookeeper-master` consumer, a `MasterRoutePolicy` or the component stops. The
application that created it owns its
+lifecycle, as for a `CuratorFramework` bound in the registry. A group factory
and a client that Camel created are
+still closed on stop, and created again on the next start.
+
=== camel-milo - each client connection has thread pools of its own
A `milo-client` or `milo-browse` connection now handles the outcome of its
write, method call, read and browse