Skip to content

Commit b439bcd

Browse files
committed
[fix] [ml] There are two same-named managed ledgers in the one broker
1 parent b1f9e35 commit b439bcd

2 files changed

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

0 commit comments

Comments
 (0)