Skip to content

Commit fc60ec0

Browse files
[fix][broker] Fail fast if the extensible load manager failed to start (#23297)
1 parent 5599699 commit fc60ec0

4 files changed

Lines changed: 179 additions & 82 deletions

File tree

pulsar-broker-common/src/main/java/org/apache/pulsar/broker/PulsarServerException.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package org.apache.pulsar.broker;
2020

2121
import java.io.IOException;
22+
import java.util.concurrent.CompletionException;
2223

2324
public class PulsarServerException extends IOException {
2425
private static final long serialVersionUID = 1;
@@ -44,4 +45,20 @@ public NotFoundException(Throwable t) {
4445
super(t);
4546
}
4647
}
48+
49+
public static PulsarServerException from(Throwable throwable) {
50+
if (throwable instanceof CompletionException) {
51+
return from(throwable.getCause());
52+
}
53+
if (throwable instanceof PulsarServerException pulsarServerException) {
54+
return pulsarServerException;
55+
} else {
56+
return new PulsarServerException(throwable);
57+
}
58+
}
59+
60+
// Wrap this checked exception into a specific unchecked exception
61+
public static CompletionException toUncheckedException(PulsarServerException e) {
62+
return new CompletionException(e);
63+
}
4764
}

pulsar-broker/src/main/java/org/apache/pulsar/broker/PulsarService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1080,7 +1080,7 @@ public void start() throws PulsarServerException {
10801080
state = State.Started;
10811081
} catch (Exception e) {
10821082
LOG.error("Failed to start Pulsar service: {}", e.getMessage(), e);
1083-
PulsarServerException startException = new PulsarServerException(e);
1083+
PulsarServerException startException = PulsarServerException.from(e);
10841084
readyForIncomingRequestsFuture.completeExceptionally(startException);
10851085
throw startException;
10861086
} finally {

pulsar-broker/src/main/java/org/apache/pulsar/broker/loadbalance/extensions/ExtensibleLoadManagerImpl.java

Lines changed: 41 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@
8181
import org.apache.pulsar.broker.loadbalance.extensions.scheduler.SplitScheduler;
8282
import org.apache.pulsar.broker.loadbalance.extensions.scheduler.UnloadScheduler;
8383
import org.apache.pulsar.broker.loadbalance.extensions.store.LoadDataStore;
84-
import org.apache.pulsar.broker.loadbalance.extensions.store.LoadDataStoreException;
8584
import org.apache.pulsar.broker.loadbalance.extensions.store.LoadDataStoreFactory;
8685
import org.apache.pulsar.broker.loadbalance.extensions.strategy.BrokerSelectionStrategy;
8786
import org.apache.pulsar.broker.loadbalance.extensions.strategy.BrokerSelectionStrategyFactory;
@@ -99,10 +98,7 @@
9998
import org.apache.pulsar.common.naming.TopicDomain;
10099
import org.apache.pulsar.common.naming.TopicName;
101100
import org.apache.pulsar.common.stats.Metrics;
102-
import org.apache.pulsar.common.util.Backoff;
103-
import org.apache.pulsar.common.util.BackoffBuilder;
104101
import org.apache.pulsar.common.util.FutureUtil;
105-
import org.apache.pulsar.metadata.api.MetadataStoreException;
106102
import org.apache.pulsar.metadata.api.coordination.LeaderElectionState;
107103
import org.slf4j.Logger;
108104

@@ -125,10 +121,6 @@ public class ExtensibleLoadManagerImpl implements ExtensibleLoadManager, BrokerS
125121

126122
public static final long COMPACTION_THRESHOLD = 5 * 1024 * 1024;
127123

128-
public static final int STARTUP_TIMEOUT_SECONDS = 30;
129-
130-
public static final int MAX_RETRY = 5;
131-
132124
private static final String ELECTION_ROOT = "/loadbalance/extension/leader";
133125

134126
public static final Set<String> INTERNAL_TOPICS =
@@ -212,7 +204,7 @@ public class ExtensibleLoadManagerImpl implements ExtensibleLoadManager, BrokerS
212204

213205
private final ConcurrentHashMap<String, CompletableFuture<Optional<BrokerLookupData>>>
214206
lookupRequests = new ConcurrentHashMap<>();
215-
private final CompletableFuture<Void> initWaiter = new CompletableFuture<>();
207+
private final CompletableFuture<Boolean> initWaiter = new CompletableFuture<>();
216208

217209
/**
218210
* Get all the bundles that are owned by this broker.
@@ -385,7 +377,7 @@ public void start() throws PulsarServerException {
385377
return;
386378
}
387379
try {
388-
this.brokerRegistry = new BrokerRegistryImpl(pulsar);
380+
this.brokerRegistry = createBrokerRegistry(pulsar);
389381
this.leaderElectionService = new LeaderElectionService(
390382
pulsar.getCoordinationService(), pulsar.getBrokerId(),
391383
pulsar.getSafeWebServiceAddress(), ELECTION_ROOT,
@@ -400,53 +392,14 @@ public void start() throws PulsarServerException {
400392
});
401393
});
402394
});
403-
this.serviceUnitStateChannel = new ServiceUnitStateChannelImpl(pulsar);
395+
this.serviceUnitStateChannel = createServiceUnitStateChannel(pulsar);
404396
this.brokerRegistry.start();
405397
this.splitManager = new SplitManager(splitCounter);
406398
this.unloadManager = new UnloadManager(unloadCounter, pulsar.getBrokerId());
407399
this.serviceUnitStateChannel.listen(unloadManager);
408400
this.serviceUnitStateChannel.listen(splitManager);
409401
this.leaderElectionService.start();
410-
pulsar.runWhenReadyForIncomingRequests(() -> {
411-
Backoff backoff = new BackoffBuilder()
412-
.setInitialTime(100, TimeUnit.MILLISECONDS)
413-
.setMax(STARTUP_TIMEOUT_SECONDS, TimeUnit.SECONDS)
414-
.create();
415-
int retry = 0;
416-
while (!Thread.currentThread().isInterrupted()) {
417-
try {
418-
brokerRegistry.register();
419-
this.serviceUnitStateChannel.start();
420-
break;
421-
} catch (Exception e) {
422-
log.warn("The broker:{} failed to start service unit state channel. Retrying {} th ...",
423-
pulsar.getBrokerId(), ++retry, e);
424-
try {
425-
Thread.sleep(backoff.next());
426-
} catch (InterruptedException ex) {
427-
log.warn("Interrupted while sleeping.");
428-
// preserve thread's interrupt status
429-
Thread.currentThread().interrupt();
430-
try {
431-
pulsar.close();
432-
} catch (PulsarServerException exc) {
433-
log.error("Failed to close pulsar service.", exc);
434-
}
435-
return;
436-
}
437-
failStarting(e);
438-
if (retry >= MAX_RETRY) {
439-
log.error("Failed to start the service unit state channel after retry {} th. "
440-
+ "Closing pulsar service.", retry, e);
441-
try {
442-
pulsar.close();
443-
} catch (PulsarServerException ex) {
444-
log.error("Failed to close pulsar service.", ex);
445-
}
446-
}
447-
}
448-
}
449-
});
402+
450403
this.antiAffinityGroupPolicyHelper =
451404
new AntiAffinityGroupPolicyHelper(pulsar, serviceUnitStateChannel);
452405
antiAffinityGroupPolicyHelper.listenFailureDomainUpdate();
@@ -455,15 +408,10 @@ public void start() throws PulsarServerException {
455408
SimpleResourceAllocationPolicies policies = new SimpleResourceAllocationPolicies(pulsar);
456409
this.isolationPoliciesHelper = new IsolationPoliciesHelper(policies);
457410
this.brokerFilterPipeline.add(new BrokerIsolationPoliciesFilter(isolationPoliciesHelper));
458-
459-
try {
460-
this.brokerLoadDataStore = LoadDataStoreFactory
461-
.create(pulsar, BROKER_LOAD_DATA_STORE_TOPIC, BrokerLoadData.class);
462-
this.topBundlesLoadDataStore = LoadDataStoreFactory
463-
.create(pulsar, TOP_BUNDLES_LOAD_DATA_STORE_TOPIC, TopBundlesLoadData.class);
464-
} catch (LoadDataStoreException e) {
465-
throw new PulsarServerException(e);
466-
}
411+
this.brokerLoadDataStore = LoadDataStoreFactory
412+
.create(pulsar, BROKER_LOAD_DATA_STORE_TOPIC, BrokerLoadData.class);
413+
this.topBundlesLoadDataStore = LoadDataStoreFactory
414+
.create(pulsar, TOP_BUNDLES_LOAD_DATA_STORE_TOPIC, TopBundlesLoadData.class);
467415

468416
this.context = LoadManagerContextImpl.builder()
469417
.configuration(conf)
@@ -487,6 +435,7 @@ public void start() throws PulsarServerException {
487435

488436
pulsar.runWhenReadyForIncomingRequests(() -> {
489437
try {
438+
this.serviceUnitStateChannel.start();
490439
var interval = conf.getLoadBalancerReportUpdateMinIntervalMillis();
491440

492441
this.brokerLoadDataReportTask = this.pulsar.getLoadManagerExecutor()
@@ -521,38 +470,33 @@ public void start() throws PulsarServerException {
521470
MONITOR_INTERVAL_IN_MILLIS, TimeUnit.MILLISECONDS);
522471

523472
this.splitScheduler.start();
524-
this.initWaiter.complete(null);
473+
this.initWaiter.complete(true);
525474
this.started = true;
526475
log.info("Started load manager.");
527-
} catch (Exception ex) {
528-
failStarting(ex);
476+
} catch (Throwable e) {
477+
failStarting(e);
529478
}
530479
});
531-
} catch (Exception ex) {
480+
} catch (Throwable ex) {
532481
failStarting(ex);
533482
}
534483
}
535484

536-
private void failStarting(Exception ex) {
537-
log.error("Failed to start the extensible load balance and close broker registry {}.",
538-
this.brokerRegistry, ex);
485+
private void failStarting(Throwable throwable) {
539486
if (this.brokerRegistry != null) {
540487
try {
541-
brokerRegistry.unregister();
542-
} catch (MetadataStoreException e) {
543-
// ignore
544-
}
545-
}
546-
if (this.serviceUnitStateChannel != null) {
547-
try {
548-
serviceUnitStateChannel.close();
549-
} catch (IOException e) {
550-
// ignore
488+
brokerRegistry.close();
489+
} catch (PulsarServerException e) {
490+
// If close failed, this broker might still exist in the metadata store. Then it could be found by other
491+
// brokers as an available broker. Hence, print a warning log for it.
492+
log.warn("Failed to close the broker registry: {}", e.getMessage());
551493
}
552494
}
553-
initWaiter.completeExceptionally(ex);
495+
initWaiter.complete(false); // exit the background thread gracefully
496+
throw PulsarServerException.toUncheckedException(PulsarServerException.from(throwable));
554497
}
555498

499+
556500
@Override
557501
public void initialize(PulsarService pulsar) {
558502
this.pulsar = pulsar;
@@ -897,7 +841,9 @@ synchronized void playLeader() {
897841
boolean becameFollower = false;
898842
while (!Thread.currentThread().isInterrupted()) {
899843
try {
900-
initWaiter.get();
844+
if (!initWaiter.get()) {
845+
return;
846+
}
901847
if (!serviceUnitStateChannel.isChannelOwner()) {
902848
becameFollower = true;
903849
break;
@@ -947,7 +893,9 @@ synchronized void playFollower() {
947893
boolean becameLeader = false;
948894
while (!Thread.currentThread().isInterrupted()) {
949895
try {
950-
initWaiter.get();
896+
if (!initWaiter.get()) {
897+
return;
898+
}
951899
if (serviceUnitStateChannel.isChannelOwner()) {
952900
becameLeader = true;
953901
break;
@@ -1018,7 +966,9 @@ private List<Metrics> getIgnoredCommandMetrics(String advertisedBrokerAddress) {
1018966
@VisibleForTesting
1019967
protected void monitor() {
1020968
try {
1021-
initWaiter.get();
969+
if (!initWaiter.get()) {
970+
return;
971+
}
1022972

1023973
// Monitor role
1024974
// Periodically check the role in case ZK watcher fails.
@@ -1073,4 +1023,14 @@ private void closeInternalTopics() {
10731023
log.warn("Failed to wait for closing internal topics", e);
10741024
}
10751025
}
1026+
1027+
@VisibleForTesting
1028+
protected BrokerRegistry createBrokerRegistry(PulsarService pulsar) {
1029+
return new BrokerRegistryImpl(pulsar);
1030+
}
1031+
1032+
@VisibleForTesting
1033+
protected ServiceUnitStateChannel createServiceUnitStateChannel(PulsarService pulsar) {
1034+
return new ServiceUnitStateChannelImpl(pulsar);
1035+
}
10761036
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.pulsar.broker.loadbalance.extensions;
20+
21+
import java.util.Optional;
22+
import lombok.Cleanup;
23+
import org.apache.pulsar.broker.PulsarServerException;
24+
import org.apache.pulsar.broker.PulsarService;
25+
import org.apache.pulsar.broker.ServiceConfiguration;
26+
import org.apache.pulsar.broker.loadbalance.LoadManager;
27+
import org.apache.pulsar.broker.loadbalance.extensions.channel.ServiceUnitStateChannel;
28+
import org.apache.pulsar.broker.loadbalance.extensions.channel.ServiceUnitStateChannelImpl;
29+
import org.apache.pulsar.common.util.PortManager;
30+
import org.apache.pulsar.zookeeper.LocalBookkeeperEnsemble;
31+
import org.awaitility.Awaitility;
32+
import org.mockito.Mockito;
33+
import org.testng.Assert;
34+
import org.testng.annotations.AfterClass;
35+
import org.testng.annotations.BeforeClass;
36+
import org.testng.annotations.Test;
37+
38+
public class LoadManagerFailFastTest {
39+
40+
private static final String cluster = "test";
41+
private final int zkPort = PortManager.nextLockedFreePort();
42+
private final LocalBookkeeperEnsemble bk = new LocalBookkeeperEnsemble(2, zkPort, PortManager::nextLockedFreePort);
43+
private final ServiceConfiguration config = new ServiceConfiguration();
44+
45+
@BeforeClass
46+
protected void setup() throws Exception {
47+
bk.start();
48+
config.setClusterName(cluster);
49+
config.setAdvertisedAddress("localhost");
50+
config.setBrokerServicePort(Optional.of(0));
51+
config.setWebServicePort(Optional.of(0));
52+
config.setMetadataStoreUrl("zk:localhost:" + zkPort);
53+
}
54+
55+
@AfterClass
56+
protected void cleanup() throws Exception {
57+
bk.stop();
58+
}
59+
60+
@Test(timeOut = 30000)
61+
public void testBrokerRegistryFailure() throws Exception {
62+
config.setLoadManagerClassName(BrokerRegistryLoadManager.class.getName());
63+
@Cleanup final var pulsar = new PulsarService(config);
64+
try {
65+
pulsar.start();
66+
Assert.fail();
67+
} catch (PulsarServerException e) {
68+
Assert.assertNull(e.getCause());
69+
Assert.assertEquals(e.getMessage(), "Cannot start BrokerRegistry");
70+
}
71+
Assert.assertTrue(pulsar.getLocalMetadataStore().getChildren(LoadManager.LOADBALANCE_BROKERS_ROOT).get()
72+
.isEmpty());
73+
}
74+
75+
@Test(timeOut = 30000)
76+
public void testServiceUnitStateChannelFailure() throws Exception {
77+
config.setLoadManagerClassName(ChannelLoadManager.class.getName());
78+
@Cleanup final var pulsar = new PulsarService(config);
79+
try {
80+
pulsar.start();
81+
Assert.fail();
82+
} catch (PulsarServerException e) {
83+
Assert.assertNull(e.getCause());
84+
Assert.assertEquals(e.getMessage(), "Cannot start ServiceUnitStateChannel");
85+
}
86+
Awaitility.await().untilAsserted(() -> Assert.assertTrue(pulsar.getLocalMetadataStore()
87+
.getChildren(LoadManager.LOADBALANCE_BROKERS_ROOT).get().isEmpty()));
88+
}
89+
90+
private static class BrokerRegistryLoadManager extends ExtensibleLoadManagerImpl {
91+
92+
@Override
93+
protected BrokerRegistry createBrokerRegistry(PulsarService pulsar) {
94+
final var mockBrokerRegistry = Mockito.mock(BrokerRegistryImpl.class);
95+
try {
96+
Mockito.doThrow(new PulsarServerException("Cannot start BrokerRegistry")).when(mockBrokerRegistry)
97+
.start();
98+
} catch (PulsarServerException e) {
99+
throw new RuntimeException(e);
100+
}
101+
return mockBrokerRegistry;
102+
}
103+
}
104+
105+
private static class ChannelLoadManager extends ExtensibleLoadManagerImpl {
106+
107+
@Override
108+
protected ServiceUnitStateChannel createServiceUnitStateChannel(PulsarService pulsar) {
109+
final var channel = Mockito.mock(ServiceUnitStateChannelImpl.class);
110+
try {
111+
Mockito.doThrow(new PulsarServerException("Cannot start ServiceUnitStateChannel")).when(channel)
112+
.start();
113+
} catch (PulsarServerException e) {
114+
throw new RuntimeException(e);
115+
}
116+
Mockito.doAnswer(__ -> null).when(channel).listen(Mockito.any());
117+
return channel;
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)