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 e670025a49a4 CAMEL-25315: camel-zookeeper-master - retry a failed
start of the master consumer, and do not start it after the leadership was lost
or the route stopped (#27391)
e670025a49a4 is described below
commit e670025a49a4aeeffb4b4f8b81a99c7e2bf78646
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Oct 6 14:52:24 2026 +0530
CAMEL-25315: camel-zookeeper-master - retry a failed start of the master
consumer, and do not start it after the leadership was lost or the route
stopped (#27391)
* CAMEL-25315: camel-zookeeper-master - retry a failed start of the master
consumer, and do not start it after the leadership was lost or the route stopped
MasterConsumer starts the delegate consumer from the CHANGED event of the
group thread (onLockOwned) and stops it from DISCONNECTED (the Curator
connection thread, and the closing thread when the group is closed) and from
doStop, without any synchronisation, and only if the delegate field is null.
- A delegate whose start failed (the broker or server is not reachable when
the node is elected) stayed in the field and was never started again: the
node kept the leadership and nobody consumed until the connection was lost
or the route was restarted.
- A disconnect (or a stop) between the null check and the assignment of the
new delegate stopped nothing, and the delegate was then created and
started:
it kept consuming on a node that was no longer the master (also after it
reconnected as a standby, as standby events stop nothing), or after the
route was stopped (when the start took longer than the 5 seconds the group
waits for its thread on close).
The consumer now guards the delegate with a lock (like the MasterConsumer of
camel-master): a start is marked under the lock (after checking again that
the group is connected and this node is the master), the delegate is created
and started without holding it, and it is published under the lock only if
no
disconnect or stop happened meanwhile (a generation counter), otherwise it
is
stopped. A failed start is retried after 5 seconds while this node is still
the master; the started state is published to the group once per leadership
term, so that a retry does not trigger another group event.
delegate and delegateService are also volatile: doSuspend/doResume read
them without the lock.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* CAMEL-25315: camel-zookeeper-master - start the consumer again when
elected while a start was in flight
A leadership event that arrives while a start is in flight is skipped. When
that start then ends after a disconnect
(or a stop of the consumer), it stopped its consumer and scheduled nothing,
so a node elected again meanwhile held the
leadership with no consumer: a reconnect while a retry runs on the retry
thread, or a stop and start of the same
consumer instance while the old group thread is still in a slow start. Such
a start now schedules a start for the
current generation without delay when the node is the leader, runs and has
no consumer or scheduled start.
Review follow-ups: a failed start goes to getExceptionHandler() (as
camel-master), with the attempt number; a
package-private setRetryDelay for the tests; a stale retry task no longer
clears the reference of a newer one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* CAMEL-25315: camel-zookeeper-master - Read the test gate before
signalling, and make the retry executor and group listener volatile
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../component/zookeepermaster/MasterConsumer.java | 188 +++++++++--
.../MasterConsumerLeadershipTest.java | 348 +++++++++++++++++++++
2 files changed, 509 insertions(+), 27 deletions(-)
diff --git
a/components/camel-zookeeper-master/src/main/java/org/apache/camel/component/zookeepermaster/MasterConsumer.java
b/components/camel-zookeeper-master/src/main/java/org/apache/camel/component/zookeepermaster/MasterConsumer.java
index a35136ab889e..8a42c3bdf347 100644
---
a/components/camel-zookeeper-master/src/main/java/org/apache/camel/component/zookeepermaster/MasterConsumer.java
+++
b/components/camel-zookeeper-master/src/main/java/org/apache/camel/component/zookeepermaster/MasterConsumer.java
@@ -16,6 +16,12 @@
*/
package org.apache.camel.component.zookeepermaster;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
@@ -36,13 +42,31 @@ import org.slf4j.LoggerFactory;
@ManagedResource(description = "Managed ZooKeeper Master Consumer")
public class MasterConsumer extends DefaultConsumer {
private static final transient Logger LOG =
LoggerFactory.getLogger(MasterConsumer.class);
+ // delay before the master tries again to start a consumer whose start
failed
+ private static final long RETRY_DELAY_MILLIS = 5000;
- private ZookeeperGroupListenerSupport groupListener;
+ private volatile ZookeeperGroupListenerSupport groupListener;
private final MasterEndpoint endpoint;
private final Processor processor;
- private Consumer delegate;
- private SuspendableService delegateService;
+ private volatile Consumer delegate;
+ private volatile SuspendableService delegateService;
private volatile CamelNodeState thisNodeState;
+ // the leadership events come from the group thread (CHANGED) and the
ZooKeeper connection thread
+ // (DISCONNECTED), and the consumer can be stopped meanwhile: the delegate
and these fields are guarded by the lock
+ // (delegate and delegateService are also volatile, as doSuspend/doResume
read them without it)
+ private final Lock leadershipLock = new ReentrantLock();
+ // incremented when the leadership may be lost or the consumer stops: a
start that began before does not publish
+ // its consumer, it stops it
+ private long generation;
+ private boolean starting;
+ // the state published when this node starts its consumer, once per
leadership term (a new state is a change of
+ // the group, which would trigger another leadership event)
+ private CamelNodeState startedState;
+ private volatile ScheduledExecutorService retryExecutor;
+ private ScheduledFuture<?> retryTask;
+ private volatile long retryDelay = RETRY_DELAY_MILLIS;
+ // failed starts in a row, for the log
+ private int failedStarts;
public MasterConsumer(MasterEndpoint endpoint, Processor processor) {
super(endpoint, processor);
@@ -94,6 +118,8 @@ public class MasterConsumer extends DefaultConsumer {
this.groupListener.setZooKeeperPassword(endpoint.getComponent().getZooKeeperPassword());
this.groupListener.setCurator(endpoint.getComponent().getCurator());
this.groupListener.setMaximumConnectionTimeout(endpoint.getComponent().getMaximumConnectionTimeout());
+ this.retryExecutor =
endpoint.getCamelContext().getExecutorServiceManager()
+ .newSingleThreadScheduledExecutor(this,
"ZooKeeperMasterRetry");
ServiceHelper.startService(groupListener);
LOG.info("Attempting to become master for endpoint: {} in {} with
singletonID: {}", endpoint,
@@ -105,9 +131,21 @@ public class MasterConsumer extends DefaultConsumer {
@Override
protected void doStop() throws Exception {
try {
- stopConsumer();
+ leadershipLock.lock();
+ try {
+ generation++;
+ failedStarts = 0;
+ cancelRetry();
+ stopConsumer();
+ } finally {
+ leadershipLock.unlock();
+ }
} finally {
ServiceHelper.stopAndShutdownServices(groupListener);
+ if (retryExecutor != null) {
+
endpoint.getCamelContext().getExecutorServiceManager().shutdownNow(retryExecutor);
+ retryExecutor = null;
+ }
}
super.doStop();
}
@@ -125,6 +163,21 @@ public class MasterConsumer extends DefaultConsumer {
delegate = null;
delegateService = null;
thisNodeState = null;
+ startedState = null;
+ }
+
+ /**
+ * The delay before the master tries again to start a consumer whose start
failed (5 seconds). For tests.
+ */
+ void setRetryDelay(long retryDelay) {
+ this.retryDelay = retryDelay;
+ }
+
+ private void cancelRetry() {
+ if (retryTask != null) {
+ retryTask.cancel(false);
+ retryTask = null;
+ }
}
@Override
@@ -144,40 +197,121 @@ public class MasterConsumer extends DefaultConsumer {
}
protected Runnable onLockOwned() {
- return () -> {
- if (delegate == null) {
- try {
- // ensure endpoint is also started
- LOG.info("Elected as master. Starting consumer: {}",
endpoint.getConsumerEndpoint());
- ServiceHelper.startService(endpoint.getConsumerEndpoint());
-
- delegate =
endpoint.getConsumerEndpoint().createConsumer(processor);
- delegateService = null;
- if (delegate instanceof SuspendableService) {
- delegateService = (SuspendableService) delegate;
- }
-
- // Lets show we are starting the consumer.
- thisNodeState = createNodeState();
- thisNodeState.setStarted(true);
- groupListener.updateState(thisNodeState);
-
- ServiceHelper.startService(delegate);
- } catch (Exception e) {
- LOG.error("Failed to start master consumer for: {}",
endpoint, e);
- }
+ return () -> startConsumer(-1);
+ }
+
+ /**
+ * Starts the delegate consumer, as this node holds the leadership.
+ *
+ * @param retryGeneration -1 for a leadership event, else the generation
of the start that failed
+ */
+ private void startConsumer(long retryGeneration) {
+ long startGeneration;
+ CamelNodeState state = null;
+ leadershipLock.lock();
+ try {
+ if (retryGeneration >= 0 && retryGeneration == generation) {
+ // this is the scheduled task (a task of an older generation
was cancelled, and a newer one may be
+ // scheduled meanwhile)
+ retryTask = null;
+ }
+ if (delegate != null || starting || retryTask != null ||
!isRunAllowed()
+ || retryGeneration >= 0 && retryGeneration != generation
|| !isLeader()) {
+ return;
+ }
+ starting = true;
+ startGeneration = generation;
+ if (startedState == null) {
+ startedState = createNodeState();
+ startedState.setStarted(true);
+ state = startedState;
+ }
+ } finally {
+ leadershipLock.unlock();
+ }
+
+ // the consumer is created and started without holding the lock, as a
start can take long (it may connect to
+ // a broker or a server), and the lock is needed to handle a
disconnect or a stop meanwhile
+ Consumer consumer = null;
+ Exception cause = null;
+ try {
+ // ensure endpoint is also started
+ LOG.info("Elected as master. Starting consumer: {}",
endpoint.getConsumerEndpoint());
+ ServiceHelper.startService(endpoint.getConsumerEndpoint());
+
+ consumer =
endpoint.getConsumerEndpoint().createConsumer(processor);
+
+ // Lets show we are starting the consumer.
+ if (state != null) {
+ thisNodeState = state;
+ groupListener.updateState(state);
+ }
+ ServiceHelper.startService(consumer);
+ } catch (Exception e) {
+ cause = e;
+ }
+
+ leadershipLock.lock();
+ try {
+ starting = false;
+ if (startGeneration != generation || !isRunAllowed()) {
+ // disconnected (no longer the master) or stopping while the
consumer started: no event is coming to
+ // stop it later
+ LOG.info("Lost the leadership or stopping while the consumer
started. Stopping consumer: {}",
+ endpoint.getConsumerEndpoint());
+ ServiceHelper.stopAndShutdownServices(consumer);
+ // a leadership event that came meanwhile (reconnected, or the
route was started again) was skipped,
+ // as this start was in flight: start again if this node is
the master now
+ if (isRunAllowed() && isLeader() && delegate == null &&
retryTask == null) {
+ LOG.info("Elected as master again while the consumer
started. Starting consumer again: {}",
+ endpoint.getConsumerEndpoint());
+ scheduleStart(0);
+ }
+ } else if (cause != null) {
+ // forget the consumer (its start stopped it) and try again
later, as long as this node is the master
+ failedStarts++;
+ getExceptionHandler().handleException("Failed to start master
consumer for: " + endpoint + " (attempt #"
+ + failedStarts + ").
Trying again in " + retryDelay + " millis.",
+ cause);
+ scheduleStart(retryDelay);
+ } else {
+ failedStarts = 0;
+ delegate = consumer;
+ delegateService = consumer instanceof SuspendableService
suspendable ? suspendable : null;
LOG.info("Elected as master. Consumer started: {}",
endpoint.getConsumerEndpoint());
}
- };
+ } finally {
+ leadershipLock.unlock();
+ }
+ }
+
+ // under the lock: starts the consumer for the current generation, unless
a disconnect or a stop comes first
+ private void scheduleStart(long delay) {
+ if (retryExecutor != null) {
+ final long scheduledGeneration = generation;
+ retryTask = retryExecutor.schedule(() ->
startConsumer(scheduledGeneration), delay, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ private boolean isLeader() {
+ // the group sets connected to false before it tells the listeners
that it is disconnected
+ return groupListener != null && groupListener.getGroup() != null &&
groupListener.getGroup().isConnected()
+ && groupListener.getGroup().isMaster();
}
protected Runnable onDisconnected() {
return () -> {
+ leadershipLock.lock();
try {
+ generation++;
+ failedStarts = 0;
+ cancelRetry();
stopConsumer();
} catch (Exception e) {
LOG.warn("Failed to stop master consumer for: {}", endpoint,
e);
+ } finally {
+ leadershipLock.unlock();
}
};
}
diff --git
a/components/camel-zookeeper-master/src/test/java/org/apache/camel/component/zookeepermaster/MasterConsumerLeadershipTest.java
b/components/camel-zookeeper-master/src/test/java/org/apache/camel/component/zookeepermaster/MasterConsumerLeadershipTest.java
new file mode 100644
index 000000000000..fe81be182d2f
--- /dev/null
+++
b/components/camel-zookeeper-master/src/test/java/org/apache/camel/component/zookeepermaster/MasterConsumerLeadershipTest.java
@@ -0,0 +1,348 @@
+/*
+ * 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.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.Consumer;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.zookeepermaster.group.Group;
+import org.apache.camel.component.zookeepermaster.group.GroupListener;
+import org.apache.camel.component.zookeepermaster.group.ManagedGroupFactory;
+import org.apache.camel.component.zookeepermaster.group.NodeState;
+import org.apache.camel.support.DefaultConsumer;
+import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.apache.curator.framework.CuratorFramework;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The leadership events of the group against the consumer of the master
endpoint, with a fake group (no ZooKeeper): the
+ * CHANGED events are delivered on the group thread, DISCONNECTED on the
Curator connection thread.
+ */
+class MasterConsumerLeadershipTest extends CamelTestSupport {
+
+ @BindToRegistry("fakeGroupFactory")
+ private final FakeGroupFactory groupFactory = new FakeGroupFactory();
+
+ private final ControlledEndpoint delegateEndpoint = new
ControlledEndpoint();
+
+ @Override
+ public boolean isUseRouteBuilder() {
+ return false;
+ }
+
+ @Test
+ void testFailedStartIsRetried() throws Exception {
+ startRoute();
+ masterConsumer().setRetryDelay(10);
+ delegateEndpoint.failStarts.set(1);
+
+ // elected: the start of the consumer fails (broker or server not
available yet)
+ groupFactory.group.fireChanged();
+ assertEquals(0, delegateEndpoint.running.get());
+
+ // the master must try again, otherwise this node holds the leadership
and nobody consumes
+ await().atMost(20, TimeUnit.SECONDS).untilAsserted(() ->
assertEquals(1, delegateEndpoint.running.get(),
+ "The master must start its consumer after a failed start"));
+ }
+
+ @Test
+ void testDisconnectedWhileCreatingConsumer() throws Exception {
+ startRoute();
+ delegateEndpoint.createGate = new CountDownLatch(1);
+
+ Thread groupThread = new Thread(groupFactory.group::fireChanged,
"group");
+ groupThread.start();
+ assertTrue(delegateEndpoint.creating.await(20, TimeUnit.SECONDS));
+
+ // the connection to ZooKeeper is lost: this node is no longer the
master
+ groupFactory.group.connected = false;
+ groupFactory.group.master = false;
+ groupFactory.group.fire(GroupListener.GroupEvent.DISCONNECTED);
+
+ delegateEndpoint.createGate.countDown();
+ groupThread.join(20000);
+ assertEquals(0, delegateEndpoint.running.get(), "A node that lost the
leadership must not consume");
+ }
+
+ @Test
+ void testStoppedWhileCreatingConsumer() throws Exception {
+ startRoute();
+ delegateEndpoint.createGate = new CountDownLatch(1);
+
+ Thread groupThread = new Thread(groupFactory.group::fireChanged,
"group");
+ groupThread.start();
+ assertTrue(delegateEndpoint.creating.await(20, TimeUnit.SECONDS));
+
+ // the start takes longer than the 5 seconds the group waits for its
thread when it is closed
+ context.getRouteController().stopRoute("master");
+
+ delegateEndpoint.createGate.countDown();
+ groupThread.join(20000);
+ assertEquals(0, delegateEndpoint.running.get(), "A stopped route must
not consume");
+ }
+
+ @Test
+ void testReconnectedWhileRetryIsStarting() throws Exception {
+ startRoute();
+ masterConsumer().setRetryDelay(10);
+ delegateEndpoint.failStarts.set(1);
+ // the first start fails, the retry (on the retry thread) is held
while it creates its consumer
+ delegateEndpoint.gateFrom = 2;
+ delegateEndpoint.createGate = new CountDownLatch(1);
+
+ groupFactory.group.fireChanged();
+ assertTrue(delegateEndpoint.creating.await(20, TimeUnit.SECONDS));
+
+ // ZooKeeper suspends and reconnects meanwhile, and this node is
elected again
+ groupFactory.group.connected = false;
+ groupFactory.group.master = false;
+ groupFactory.group.fire(GroupListener.GroupEvent.DISCONNECTED);
+ groupFactory.group.connected = true;
+ groupFactory.group.master = true;
+ groupFactory.group.fireChanged();
+
+ delegateEndpoint.createGate.countDown();
+ // the retry stops its consumer (it began before the disconnect), and
the master must start one again
+ await().atMost(20, TimeUnit.SECONDS).untilAsserted(() ->
assertEquals(1, delegateEndpoint.running.get(),
+ "A master elected again while a start was in flight must
consume"));
+ }
+
+ @Test
+ void testConsumerRestartedWhileCreatingConsumer() throws Exception {
+ startRoute();
+ CountDownLatch gate = new CountDownLatch(1);
+ delegateEndpoint.createGate = gate;
+
+ Thread groupThread = new Thread(groupFactory.group::fireChanged,
"group");
+ groupThread.start();
+ assertTrue(delegateEndpoint.creating.await(20, TimeUnit.SECONDS));
+ // only that create is slow
+ delegateEndpoint.createGate = null;
+
+ // the consumer is stopped and started again (as by JMX; a route
restart creates a new consumer) while the old
+ // group thread is still in the slow start, and the new group elects
this node
+ MasterConsumer consumer = masterConsumer();
+ consumer.stop();
+ consumer.start();
+ groupFactory.group.fireChanged();
+
+ gate.countDown();
+ groupThread.join(20000);
+ await().atMost(20, TimeUnit.SECONDS).untilAsserted(() ->
assertEquals(1, delegateEndpoint.running.get(),
+ "A master started again while its old start was in flight must
consume"));
+ }
+
+ private MasterConsumer masterConsumer() {
+ return (MasterConsumer) context.getRoute("master").getConsumer();
+ }
+
+ private void startRoute() throws Exception {
+ context.addEndpoint("controlled:delegate", delegateEndpoint);
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("zookeeper-master:test:controlled:delegate").routeId("master").to("mock:result");
+ }
+ });
+ context.start();
+ assertTrue(groupFactory.group != null &&
groupFactory.group.listeners.size() == 1);
+ }
+
+ private final class ControlledEndpoint extends DefaultEndpoint {
+ final AtomicInteger failStarts = new AtomicInteger();
+ final AtomicInteger running = new AtomicInteger();
+ final AtomicInteger creates = new AtomicInteger();
+ final CountDownLatch creating = new CountDownLatch(1);
+ // the createGate holds the creates from this one on (1 = the first)
+ volatile int gateFrom = 1;
+ volatile CountDownLatch createGate;
+
+ @Override
+ protected String createEndpointUri() {
+ return "controlled:delegate";
+ }
+
+ @Override
+ public Producer createProducer() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Consumer createConsumer(Processor processor) throws Exception {
+ if (creates.incrementAndGet() < gateFrom) {
+ return newConsumer(processor);
+ }
+ // read the gate before signalling, as the test may clear it right
after that signal
+ CountDownLatch gate = createGate;
+ creating.countDown();
+ if (gate != null) {
+ assertTrue(gate.await(20, TimeUnit.SECONDS));
+ }
+ return newConsumer(processor);
+ }
+
+ private Consumer newConsumer(Processor processor) {
+ return new DefaultConsumer(this, processor) {
+ private boolean up;
+
+ @Override
+ protected void doStart() throws Exception {
+ if (failStarts.getAndDecrement() > 0) {
+ throw new IllegalStateException("Simulated start
failure");
+ }
+ super.doStart();
+ up = true;
+ running.incrementAndGet();
+ }
+
+ @Override
+ protected void doStop() throws Exception {
+ if (up) {
+ up = false;
+ running.decrementAndGet();
+ }
+ super.doStop();
+ }
+ };
+ }
+ }
+
+ private static final class FakeGroupFactory implements ManagedGroupFactory
{
+ volatile FakeGroup group;
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T extends NodeState> Group<T> createGroup(String path,
Class<T> clazz) {
+ group = new FakeGroup();
+ return (Group<T>) group;
+ }
+
+ @Override
+ public <T extends NodeState> Group<T> createGroup(String path,
Class<T> clazz, ThreadFactory threadFactory) {
+ return createGroup(path, clazz);
+ }
+
+ @Override
+ public <T extends NodeState> Group<T> createMultiGroup(String path,
Class<T> clazz) {
+ return createGroup(path, clazz);
+ }
+
+ @Override
+ public <T extends NodeState> Group<T> createMultiGroup(String path,
Class<T> clazz, ThreadFactory threadFactory) {
+ return createGroup(path, clazz);
+ }
+
+ @Override
+ public CuratorFramework getCurator() {
+ return null;
+ }
+
+ @Override
+ public void close() {
+ // noop
+ }
+ }
+
+ private static final class FakeGroup implements Group<CamelNodeState> {
+ final List<GroupListener<CamelNodeState>> listeners = new
CopyOnWriteArrayList<>();
+ volatile boolean connected = true;
+ volatile boolean master = true;
+
+ void fireChanged() {
+ fire(GroupListener.GroupEvent.CHANGED);
+ }
+
+ void fire(GroupListener.GroupEvent event) {
+ listeners.forEach(l -> l.groupEvent(this, event));
+ }
+
+ @Override
+ public boolean isConnected() {
+ return connected;
+ }
+
+ @Override
+ public void start() {
+ // noop
+ }
+
+ @Override
+ public void close() {
+ // as ZooKeeperGroup: a group closed while connected tells its
listeners it is disconnected
+ if (connected) {
+ fire(GroupListener.GroupEvent.DISCONNECTED);
+ }
+ listeners.clear();
+ }
+
+ @Override
+ public void add(GroupListener<CamelNodeState> listener) {
+ listeners.add(listener);
+ }
+
+ @Override
+ public void remove(GroupListener<CamelNodeState> listener) {
+ listeners.remove(listener);
+ }
+
+ @Override
+ public void update(CamelNodeState state) {
+ // noop
+ }
+
+ @Override
+ public Map<String, CamelNodeState> members() {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public boolean isMaster() {
+ return master;
+ }
+
+ @Override
+ public CamelNodeState master() {
+ return null;
+ }
+
+ @Override
+ public List<CamelNodeState> slaves() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public CamelNodeState getLastState() {
+ return null;
+ }
+ }
+}