Skip to content

Commit 9089c62

Browse files
committed
[fix] [ml] There are two same-named managed ledgers in the one broker
1 parent 02ef9ce commit 9089c62

2 files changed

Lines changed: 199 additions & 3 deletions

File tree

managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/ManagedLedgerFactoryImpl.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -504,9 +504,19 @@ public void openReadOnlyManagedLedgerFailed(ManagedLedgerException exception, Ob
504504
}
505505

506506
void close(ManagedLedger ledger) {
507-
// Remove the ledger from the internal factory cache
508-
ledgers.remove(ledger.getName());
509-
entryCacheManager.removeEntryCache(ledger.getName());
507+
// If the future in map is not done or has exceptionally complete, it means that @param-ledger is not in the
508+
// map.
509+
CompletableFuture<ManagedLedgerImpl> ledgerFuture = ledgers.get(ledger.getName());
510+
if (!ledgerFuture.isDone() || ledgerFuture.isCompletedExceptionally()){
511+
return;
512+
}
513+
if (ledgerFuture.join() != ledger){
514+
return;
515+
}
516+
// Remove the ledger from the internal factory cache.
517+
if (ledgers.remove(ledger.getName(), ledgerFuture)) {
518+
entryCacheManager.removeEntryCache(ledger.getName());
519+
}
510520
}
511521

512522
public CompletableFuture<Void> shutdownAsync() throws ManagedLedgerException {
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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.bookkeeper.mledger.impl;
20+
21+
import static org.mockito.Mockito.any;
22+
import static org.mockito.Mockito.doAnswer;
23+
import static org.mockito.Mockito.spy;
24+
import io.netty.buffer.ByteBuf;
25+
import java.nio.charset.Charset;
26+
import java.util.UUID;
27+
import java.util.concurrent.CompletableFuture;
28+
import java.util.concurrent.TimeUnit;
29+
import java.util.concurrent.atomic.AtomicBoolean;
30+
import java.util.concurrent.atomic.AtomicInteger;
31+
import lombok.extern.slf4j.Slf4j;
32+
import org.apache.bookkeeper.client.LedgerHandle;
33+
import org.apache.bookkeeper.mledger.AsyncCallbacks;
34+
import org.apache.bookkeeper.mledger.ManagedLedger;
35+
import org.apache.bookkeeper.mledger.ManagedLedgerConfig;
36+
import org.apache.bookkeeper.mledger.ManagedLedgerException;
37+
import org.apache.bookkeeper.mledger.Position;
38+
import org.apache.bookkeeper.test.MockedBookKeeperTestCase;
39+
import org.awaitility.Awaitility;
40+
import org.testng.Assert;
41+
import org.testng.annotations.Test;
42+
43+
@Slf4j
44+
public class LedgerLostTest extends MockedBookKeeperTestCase {
45+
46+
private void triggerLedgerRollover(ManagedLedger ledger, int maxEntriesPerLedger) {
47+
new Thread(() -> {
48+
int writeLedgerCount = 2;
49+
for (int i = 0; i < writeLedgerCount; i++) {
50+
for (int j = 0; j < maxEntriesPerLedger; j++) {
51+
byte[] data = String.format("%s_%s", i, j).getBytes(Charset.defaultCharset());
52+
Object ctx = "";
53+
ledger.asyncAddEntry(data, new AsyncCallbacks.AddEntryCallback() {
54+
@Override
55+
public void addComplete(Position position, ByteBuf entryData, Object ctx) {
56+
57+
}
58+
59+
@Override
60+
public void addFailed(ManagedLedgerException exception, Object ctx) {
61+
62+
}
63+
}, ctx);
64+
}
65+
}
66+
}).start();
67+
}
68+
69+
@Test
70+
public void testConcurrentCloseLedgerAndSwitchLedgerForReproduceIssue() throws Exception {
71+
String managedLedgerName = "lg_" + UUID.randomUUID().toString().replaceAll("-", "_");
72+
int maxEntriesPerLedger = 5;
73+
ManagedLedgerConfig config = new ManagedLedgerConfig();
74+
config.setThrottleMarkDelete(1);
75+
config.setMaximumRolloverTime(Integer.MAX_VALUE, TimeUnit.SECONDS);
76+
config.setMaxEntriesPerLedger(5);
77+
final ProcessCoordinator processCoordinator = new ProcessCoordinator();
78+
79+
// call "switch ledger" and "managedLedger.close" concurrently.
80+
final ManagedLedgerImpl managedLedger1 = (ManagedLedgerImpl) factory.open(managedLedgerName, config);
81+
waitManagedLedgerStateEquals(managedLedger1, ManagedLedgerImpl.State.LedgerOpened);
82+
processCoordinator.on();
83+
final ManagedLedgerImpl sequentiallyManagedLedger1 =
84+
makeManagedLedgerWorksWithStrictlySequentially(managedLedger1, processCoordinator);
85+
triggerLedgerRollover(sequentiallyManagedLedger1, maxEntriesPerLedger);
86+
sequentiallyManagedLedger1.close();
87+
waitManagedLedgerStateNotEquals(managedLedger1, ManagedLedgerImpl.State.Closed);
88+
89+
// create managedLedger2.
90+
final ManagedLedgerImpl managedLedger2 = (ManagedLedgerImpl) factory.open(managedLedgerName, config);
91+
Assert.assertEquals(factory.ledgers.size(), 1);
92+
Assert.assertNotEquals(managedLedger1, managedLedger2);
93+
waitManagedLedgerInFactoryEquals(managedLedger2);
94+
processCoordinator.off();
95+
managedLedger1.close();
96+
waitManagedLedgerStateEquals(managedLedger1, ManagedLedgerImpl.State.Closed);
97+
Assert.assertFalse(factory.ledgers.isEmpty());
98+
Assert.assertEquals(factory.ledgers.get(managedLedger2.getName()).join(), managedLedger2);
99+
100+
// cleanup.
101+
managedLedger2.close();
102+
}
103+
104+
private ManagedLedgerImpl makeManagedLedgerWorksWithStrictlySequentially(ManagedLedgerImpl originalManagedLedger,
105+
ProcessCoordinator processCoordinator)
106+
throws Exception {
107+
ManagedLedgerImpl sequentiallyManagedLedger = spy(originalManagedLedger);
108+
// step-1.
109+
doAnswer(invocation -> {
110+
synchronized (originalManagedLedger) {
111+
// step-3.
112+
// Wait for `managedLedger.close`, then do task: "asyncCreateLedger()".
113+
// Because the thread selector in "managedLedger.executor" is random logic, so it is possible to fail.
114+
// Adding 1000 tasks to stuck the executor gives a high chance of success.
115+
for (int i = 0; i < 1000; i++) {
116+
originalManagedLedger.getExecutor().execute(() -> {
117+
processCoordinator.waitPreviousAndSetStep(3);
118+
});
119+
}
120+
LedgerHandle lh = (LedgerHandle) invocation.getArguments()[0];
121+
processCoordinator.waitPreviousAndSetStep(1);
122+
originalManagedLedger.ledgerClosed(lh);
123+
}
124+
return null;
125+
}).when(sequentiallyManagedLedger).ledgerClosed(any(LedgerHandle.class));
126+
// step-2.
127+
doAnswer(invocation -> {
128+
processCoordinator.waitPreviousAndSetStep(2);
129+
originalManagedLedger.close();
130+
return null;
131+
}).when(sequentiallyManagedLedger).close();
132+
return sequentiallyManagedLedger;
133+
}
134+
135+
private void waitManagedLedgerInFactoryEquals(ManagedLedgerImpl managedLedger){
136+
Awaitility.await().until(() -> {
137+
CompletableFuture<ManagedLedgerImpl> managedLedgerFuture = factory.ledgers.get(managedLedger.getName());
138+
return managedLedgerFuture.join() == managedLedger;
139+
});
140+
}
141+
142+
private void waitManagedLedgerStateEquals(ManagedLedgerImpl managedLedger, ManagedLedgerImpl.State expectedStat){
143+
Awaitility.await().untilAsserted(() ->
144+
Assert.assertTrue(managedLedger.getState() == expectedStat));
145+
}
146+
147+
private void waitManagedLedgerStateNotEquals(ManagedLedgerImpl managedLedger, ManagedLedgerImpl.State expectedStat){
148+
Awaitility.await().untilAsserted(() ->
149+
Assert.assertTrue(managedLedger.getState() != expectedStat));
150+
}
151+
152+
private static class ProcessCoordinator {
153+
154+
private AtomicBoolean latch = new AtomicBoolean(true);
155+
156+
private AtomicInteger step = new AtomicInteger();
157+
158+
public boolean waitPreviousAndSetStep(int currentStep){
159+
if (!latch.get()){
160+
return false;
161+
}
162+
int previousStep = currentStep - 1;
163+
while (true){
164+
if (step.compareAndSet(previousStep, currentStep)){
165+
return true;
166+
}
167+
if (step.get() >= currentStep){
168+
return false;
169+
}
170+
try {
171+
Thread.sleep(20);
172+
} catch (InterruptedException e) {
173+
throw new RuntimeException(e);
174+
}
175+
}
176+
}
177+
178+
public void on() {
179+
latch.set(true);
180+
}
181+
182+
public void off() {
183+
latch.set(false);
184+
}
185+
}
186+
}

0 commit comments

Comments
 (0)