Skip to content

Commit 1ff1c14

Browse files
committed
[fix][ml] Fail queued adds when a managed ledger is terminated during a ledger rollover
asyncTerminate() sets the Terminated state and closes the current ledger regardless of a rollover in progress, and the rollover callbacks did not check for it: createComplete() only special-cased the Closed state, then either reset the state on a failed creation or went on to updateLedgersIdsComplete(), which set LedgerOpened and re-submitted the queued adds to the new ledger. A managed ledger terminated while a new ledger was being created became writable again, and the queued adds were persisted after the terminated position stored in the metadata, where no reader ever goes. Check for the Terminated state in createComplete() and around the ledgers-list update that follows the creation (before each attempt, and in its success and failure callbacks): keep the state, fail the queued adds with ManagedLedgerTerminatedException and discard the new ledger.
1 parent 1b5ec96 commit 1ff1c14

2 files changed

Lines changed: 309 additions & 5 deletions

File tree

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

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1753,6 +1753,12 @@ public synchronized void createComplete(int rc, final LedgerHandle lh, Object ct
17531753
}
17541754

17551755
mbean.endDataLedgerCreateOp();
1756+
if (STATE_UPDATER.get(this) == State.Terminated) {
1757+
// Terminated while the ledger was being created, whether the creation succeeded, failed or timed out
1758+
abortRolloverAfterTerminate(lh);
1759+
return;
1760+
}
1761+
17561762
if (rc != BKException.Code.OK) {
17571763
log.error().attr("rc", rc).attr("message", BKException.getMessage(rc)).log("Error creating ledger");
17581764
ManagedLedgerException status = createManagedLedgerException(rc);
@@ -1789,6 +1795,10 @@ public void operationComplete(Void v, Stat stat) {
17891795
}
17901796
return null;
17911797
});
1798+
} else if (state == State.Terminated) {
1799+
// Terminated while the ledgers list was being updated. The new ledger was not added
1800+
// to the in-memory list, so the metadata update of the terminate drops it again
1801+
abortRolloverAfterTerminate(lh);
17921802
} else {
17931803
LedgerHandle originalCurrentLedger = currentLedger;
17941804
ledgers.put(lh.getId(), newLedger);
@@ -1838,13 +1848,18 @@ public void operationFailed(MetaStoreException e) {
18381848

18391849
synchronized (ManagedLedgerImpl.this) {
18401850
lastLedgerCreationFailureTimestamp = clock.millis();
1841-
STATE_UPDATER.set(ManagedLedgerImpl.this, State.ClosedLedger);
1842-
clearPendingAddEntries(e);
1851+
if (STATE_UPDATER.get(ManagedLedgerImpl.this) == State.Terminated) {
1852+
// Terminated while the ledgers list was being updated. The new ledger is deleted above
1853+
abortRolloverAfterTerminate(null);
1854+
} else {
1855+
STATE_UPDATER.set(ManagedLedgerImpl.this, State.ClosedLedger);
1856+
clearPendingAddEntries(e);
1857+
}
18431858
}
18441859
}
18451860
};
18461861

1847-
updateLedgersListAfterRollover(cb, newLedger);
1862+
updateLedgersListAfterRollover(cb, lh, newLedger);
18481863
}
18491864
}
18501865

@@ -1853,10 +1868,35 @@ protected void handleBadVersion(Throwable e) {
18531868
setFenced();
18541869
}
18551870
}
1856-
private void updateLedgersListAfterRollover(MetaStoreCallback<Void> callback, LedgerInfo newLedger) {
1871+
1872+
/**
1873+
* Aborts a ledger rollover that was overtaken by the termination of the managed ledger. Nothing can be written
1874+
* past the terminated position: the state is left untouched, the adds that were waiting for the new ledger are
1875+
* failed, and the new ledger is discarded.
1876+
*
1877+
* @param lh the ledger that was just created, or null if there is no ledger to discard
1878+
*/
1879+
private synchronized void abortRolloverAfterTerminate(@Nullable LedgerHandle lh) {
1880+
log.info().attr("ledgerId", lh != null ? lh.getId() : -1)
1881+
.log("Managed ledger was terminated during the ledger rollover, failing the pending adds");
1882+
clearPendingAddEntries(new ManagedLedgerTerminatedException("Managed ledger was terminated"));
1883+
if (lh != null) {
1884+
// Close the write handle before deleting the ledger, so that the handle is not leaked
1885+
lh.closeAsync().whenComplete((ignore, ex) -> asyncDeleteLedger(lh.getId(), DEFAULT_LEDGER_DELETE_RETRIES));
1886+
}
1887+
}
1888+
1889+
private synchronized void updateLedgersListAfterRollover(MetaStoreCallback<Void> callback, LedgerHandle lh,
1890+
LedgerInfo newLedger) {
1891+
if (STATE_UPDATER.get(this) == State.Terminated) {
1892+
// Terminated while this update was deferred: the new ledger must not make it to the ledgers list
1893+
abortRolloverAfterTerminate(lh);
1894+
return;
1895+
}
1896+
18571897
if (!metadataMutex.tryLock()) {
18581898
// Defer update for later
1859-
scheduledExecutor.schedule(() -> updateLedgersListAfterRollover(callback, newLedger),
1899+
scheduledExecutor.schedule(() -> updateLedgersListAfterRollover(callback, lh, newLedger),
18601900
100, TimeUnit.MILLISECONDS);
18611901
return;
18621902
}

‎managed-ledger/src/test/java/org/apache/bookkeeper/mledger/impl/ManagedLedgerTerminationTest.java‎

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,43 @@
1919
package org.apache.bookkeeper.mledger.impl;
2020

2121
import static org.apache.bookkeeper.mledger.util.ManagedLedgerTestUtil.defaultConfig;
22+
import static org.mockito.ArgumentMatchers.any;
23+
import static org.mockito.ArgumentMatchers.eq;
24+
import static org.mockito.Mockito.doAnswer;
25+
import static org.mockito.Mockito.spy;
2226
import static org.testng.Assert.assertEquals;
2327
import static org.testng.Assert.assertFalse;
2428
import static org.testng.Assert.assertTrue;
2529
import static org.testng.Assert.fail;
30+
import io.netty.buffer.ByteBuf;
2631
import java.util.Collections;
2732
import java.util.List;
33+
import java.util.Set;
34+
import java.util.concurrent.CompletableFuture;
35+
import java.util.concurrent.CountDownLatch;
36+
import java.util.concurrent.ExecutionException;
37+
import java.util.concurrent.TimeUnit;
38+
import java.util.concurrent.atomic.AtomicBoolean;
39+
import lombok.Cleanup;
40+
import org.apache.bookkeeper.client.BKException;
41+
import org.apache.bookkeeper.mledger.AsyncCallbacks.AddEntryCallback;
42+
import org.apache.bookkeeper.mledger.AsyncCallbacks.TerminateCallback;
2843
import org.apache.bookkeeper.mledger.Entry;
2944
import org.apache.bookkeeper.mledger.ManagedCursor;
3045
import org.apache.bookkeeper.mledger.ManagedLedger;
46+
import org.apache.bookkeeper.mledger.ManagedLedgerConfig;
47+
import org.apache.bookkeeper.mledger.ManagedLedgerException;
3148
import org.apache.bookkeeper.mledger.ManagedLedgerException.ManagedLedgerTerminatedException;
3249
import org.apache.bookkeeper.mledger.ManagedLedgerException.NoMoreEntriesToReadException;
50+
import org.apache.bookkeeper.mledger.ManagedLedgerInfo;
3351
import org.apache.bookkeeper.mledger.Position;
3452
import org.apache.bookkeeper.mledger.PositionFactory;
3553
import org.apache.bookkeeper.test.MockedBookKeeperTestCase;
54+
import org.apache.pulsar.metadata.api.MetadataStoreException;
55+
import org.apache.pulsar.metadata.api.Stat;
56+
import org.apache.pulsar.metadata.impl.FaultInjectionMetadataStore;
57+
import org.awaitility.Awaitility;
58+
import org.testng.annotations.DataProvider;
3659
import org.testng.annotations.Test;
3760

3861
public class ManagedLedgerTerminationTest extends MockedBookKeeperTestCase {
@@ -162,4 +185,245 @@ public void terminateWithNonDurableCursor() throws Exception {
162185
}
163186
}
164187

188+
@Test(timeOut = 20000)
189+
public void terminateWhileCreatingLedger() throws Exception {
190+
ManagedLedgerConfig config = initManagedLedgerConfig(defaultConfig());
191+
config.setMaxEntriesPerLedger(1);
192+
ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("my_test_ledger", config);
193+
ManagedCursor c1 = ledger.openCursor("c1");
194+
195+
// The first entry fills the ledger and triggers a rollover. The add and the close of the full ledger take the
196+
// first 2 steps of the mock BookKeeper client: hold the 3rd one, which is the creation of the next ledger
197+
CompletableFuture<Void> createLedgerGate = bkc.promiseAfter(2);
198+
Position p0 = ledger.addEntry("entry-0".getBytes());
199+
assertEquals(ledger.getState(), ManagedLedgerImpl.State.CreatingLedger);
200+
201+
// These adds are queued, waiting for the new ledger
202+
CompletableFuture<Position> add1 = addEntryAsync(ledger, "entry-1");
203+
CompletableFuture<Position> add2 = addEntryAsync(ledger, "entry-2");
204+
Awaitility.await().untilAsserted(() -> assertEquals(ledger.getPendingAddEntriesCount(), 2));
205+
206+
assertEquals(ledger.terminate(), p0);
207+
208+
// The ledger creation completes after the managed ledger was terminated
209+
createLedgerGate.complete(null);
210+
211+
assertFailedWithTerminated(add1);
212+
assertFailedWithTerminated(add2);
213+
assertTerminatedAt(factory, ledger, p0);
214+
215+
List<Entry> entries = c1.readEntries(10);
216+
assertEquals(entries.size(), 1);
217+
assertEquals(entries.get(0).getPosition(), p0);
218+
entries.forEach(Entry::release);
219+
assertEquals(c1.readEntries(10), Collections.emptyList());
220+
221+
// The terminated state is what gets recovered
222+
ledger.close();
223+
ManagedLedger reopened = factory.open("my_test_ledger", config);
224+
assertTrue(reopened.isTerminated());
225+
assertEquals(reopened.getLastConfirmedEntry(), p0);
226+
}
227+
228+
@Test(timeOut = 20000)
229+
public void terminateWhileCreatingLedgerFails() throws Exception {
230+
ManagedLedgerConfig config = initManagedLedgerConfig(defaultConfig());
231+
config.setMaxEntriesPerLedger(1);
232+
ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("my_test_ledger", config);
233+
234+
// Hold the creation of the next ledger, as in terminateWhileCreatingLedger()
235+
CompletableFuture<Void> createLedgerGate = bkc.promiseAfter(2);
236+
Position p0 = ledger.addEntry("entry-0".getBytes());
237+
assertEquals(ledger.getState(), ManagedLedgerImpl.State.CreatingLedger);
238+
239+
CompletableFuture<Position> add1 = addEntryAsync(ledger, "entry-1");
240+
Awaitility.await().untilAsserted(() -> assertEquals(ledger.getPendingAddEntriesCount(), 1));
241+
242+
assertEquals(ledger.terminate(), p0);
243+
244+
// The ledger creation fails after the managed ledger was terminated
245+
createLedgerGate.completeExceptionally(new BKException.BKNotEnoughBookiesException());
246+
247+
assertFailedWithTerminated(add1);
248+
assertTerminatedAt(factory, ledger, p0);
249+
}
250+
251+
@Test(timeOut = 20000)
252+
public void terminateWhileCreatingLedgerTimesOut() throws Exception {
253+
ManagedLedgerConfig config = initManagedLedgerConfig(defaultConfig());
254+
config.setMaxEntriesPerLedger(1);
255+
config.setMetadataOperationsTimeoutSeconds(1);
256+
ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("my_test_ledger", config);
257+
258+
// Hold the creation of the next ledger, as in terminateWhileCreatingLedger()
259+
CompletableFuture<Void> createLedgerGate = bkc.promiseAfter(2);
260+
Position p0 = ledger.addEntry("entry-0".getBytes());
261+
assertEquals(ledger.getState(), ManagedLedgerImpl.State.CreatingLedger);
262+
263+
CompletableFuture<Position> add1 = addEntryAsync(ledger, "entry-1");
264+
Awaitility.await().untilAsserted(() -> assertEquals(ledger.getPendingAddEntriesCount(), 1));
265+
266+
assertEquals(ledger.terminate(), p0);
267+
268+
// The ledger creation times out after the managed ledger was terminated
269+
assertFailedWithTerminated(add1);
270+
assertEquals(ledger.getState(), ManagedLedgerImpl.State.Terminated);
271+
272+
// ... and the ledger still gets created later on. Hold its deletion, which is the next step of the mock
273+
// BookKeeper client, to observe that the ledger was created
274+
CompletableFuture<Void> deleteLedgerGate = bkc.promiseAfter(0);
275+
createLedgerGate.complete(null);
276+
Awaitility.await().untilAsserted(() -> assertEquals(bkc.getLedgers().size(), 2));
277+
278+
deleteLedgerGate.complete(null);
279+
assertTerminatedAt(factory, ledger, p0);
280+
}
281+
282+
@Test(timeOut = 20000)
283+
public void terminateWhileLedgersListUpdateIsDeferred() throws Exception {
284+
ManagedLedgerConfig config = initManagedLedgerConfig(defaultConfig());
285+
config.setMaxEntriesPerLedger(1);
286+
ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("my_test_ledger", config);
287+
288+
// Another metadata operation is in progress: after the rollover, the new ledger is created but the update of
289+
// the ledgers list keeps being deferred
290+
assertTrue(ledger.metadataMutex.tryLock());
291+
Position p0 = ledger.addEntry("entry-0".getBytes());
292+
Awaitility.await().untilAsserted(
293+
() -> assertEquals(ledger.getStats().getPendingBookieOpsStats().dataLedgerCreateOp, 0));
294+
assertEquals(ledger.getState(), ManagedLedgerImpl.State.CreatingLedger);
295+
296+
CompletableFuture<Position> add1 = addEntryAsync(ledger, "entry-1");
297+
Awaitility.await().untilAsserted(() -> assertEquals(ledger.getPendingAddEntriesCount(), 1));
298+
299+
assertEquals(ledger.terminate(), p0);
300+
301+
// The deferred update of the ledgers list gets its turn after the managed ledger was terminated
302+
ledger.metadataMutex.unlock();
303+
304+
assertFailedWithTerminated(add1);
305+
assertTerminatedAt(factory, ledger, p0);
306+
}
307+
308+
@DataProvider(name = "ledgersListUpdateFails")
309+
public Object[][] ledgersListUpdateFails() {
310+
return new Object[][] {{false}, {true}};
311+
}
312+
313+
@Test(timeOut = 20000, dataProvider = "ledgersListUpdateFails")
314+
@SuppressWarnings("unchecked")
315+
public void terminateWhileUpdatingLedgersList(boolean updateFails) throws Exception {
316+
String mlPath = "/managed-ledgers/my_test_ledger";
317+
318+
// Holds the response of a ledgers list update, which either is applied right away or fails. The spy does not
319+
// block, since the update is triggered while holding the managed ledger monitor
320+
CompletableFuture<Void> updateResponseGate = new CompletableFuture<>();
321+
AtomicBoolean interceptNextPut = new AtomicBoolean(false);
322+
CountDownLatch putIntercepted = new CountDownLatch(1);
323+
FaultInjectionMetadataStore spyStore = spy(metadataStore);
324+
doAnswer(inv -> {
325+
if (!interceptNextPut.compareAndSet(true, false)) {
326+
return inv.callRealMethod();
327+
}
328+
putIntercepted.countDown();
329+
CompletableFuture<Stat> response = updateFails
330+
? CompletableFuture.failedFuture(new MetadataStoreException("injected failure"))
331+
: (CompletableFuture<Stat>) inv.callRealMethod();
332+
return updateResponseGate.thenCompose(ignore -> response);
333+
}).when(spyStore).put(eq(mlPath), any(byte[].class), any());
334+
335+
@Cleanup("shutdown")
336+
ManagedLedgerFactoryImpl spyStoreFactory = new ManagedLedgerFactoryImpl(spyStore, bkc);
337+
ManagedLedgerConfig config = initManagedLedgerConfig(defaultConfig());
338+
config.setMaxEntriesPerLedger(1);
339+
ManagedLedgerImpl ledger = (ManagedLedgerImpl) spyStoreFactory.open("my_test_ledger", config);
340+
341+
// The first entry fills the ledger and triggers a rollover: the new ledger is created and the update of the
342+
// ledgers list is sent to the metadata store, though the response is still in flight
343+
interceptNextPut.set(true);
344+
Position p0 = ledger.addEntry("entry-0".getBytes());
345+
assertTrue(putIntercepted.await(10, TimeUnit.SECONDS));
346+
assertEquals(ledger.getState(), ManagedLedgerImpl.State.CreatingLedger);
347+
348+
CompletableFuture<Position> add1 = addEntryAsync(ledger, "entry-1");
349+
Awaitility.await().untilAsserted(() -> assertEquals(ledger.getPendingAddEntriesCount(), 1));
350+
351+
// Terminate, holding its ledger close, which is the next step of the mock BookKeeper client
352+
CompletableFuture<Void> closeLedgerGate = bkc.promiseAfter(0);
353+
CompletableFuture<Position> terminated = new CompletableFuture<>();
354+
ledger.asyncTerminate(new TerminateCallback() {
355+
@Override
356+
public void terminateComplete(Position lastCommittedPosition, Object ctx) {
357+
terminated.complete(lastCommittedPosition);
358+
}
359+
360+
@Override
361+
public void terminateFailed(ManagedLedgerException exception, Object ctx) {
362+
terminated.completeExceptionally(exception);
363+
}
364+
}, null);
365+
assertTrue(ledger.isTerminated());
366+
367+
// The response of the ledgers list update arrives after the managed ledger was terminated
368+
updateResponseGate.complete(null);
369+
assertFailedWithTerminated(add1);
370+
371+
closeLedgerGate.complete(null);
372+
assertEquals(terminated.get(), p0);
373+
assertTerminatedAt(spyStoreFactory, ledger, p0);
374+
}
375+
376+
private static CompletableFuture<Position> addEntryAsync(ManagedLedger ledger, String data) {
377+
CompletableFuture<Position> future = new CompletableFuture<>();
378+
ledger.asyncAddEntry(data.getBytes(), new AddEntryCallback() {
379+
@Override
380+
public void addComplete(Position position, ByteBuf entryData, Object ctx) {
381+
future.complete(position);
382+
}
383+
384+
@Override
385+
public void addFailed(ManagedLedgerException exception, Object ctx) {
386+
future.completeExceptionally(exception);
387+
}
388+
}, null);
389+
return future;
390+
}
391+
392+
private static void assertFailedWithTerminated(CompletableFuture<Position> add) throws Exception {
393+
try {
394+
Position position = add.get();
395+
fail("Add should have failed, it was written at " + position);
396+
} catch (ExecutionException e) {
397+
assertTrue(e.getCause() instanceof ManagedLedgerTerminatedException, "Unexpected failure: " + e.getCause());
398+
}
399+
}
400+
401+
/**
402+
* Asserts that the managed ledger stayed terminated at the given position, after a ledger rollover that was in
403+
* progress during the terminate: nothing was written past that position, in memory, in the metadata store or in
404+
* BookKeeper, where the ledger created by the rollover must not be leaked.
405+
*/
406+
private void assertTerminatedAt(ManagedLedgerFactoryImpl factory, ManagedLedgerImpl ledger, Position lastPosition)
407+
throws Exception {
408+
assertEquals(ledger.getState(), ManagedLedgerImpl.State.Terminated);
409+
assertEquals(ledger.getPendingAddEntriesCount(), 0);
410+
assertEquals(ledger.getLastConfirmedEntry(), lastPosition);
411+
assertEquals(ledger.getLedgersInfoAsList().size(), 1);
412+
413+
ManagedLedgerInfo info = factory.getManagedLedgerInfo(ledger.getName());
414+
assertEquals(info.ledgers.size(), 1);
415+
assertEquals(info.ledgers.get(0).ledgerId, lastPosition.getLedgerId());
416+
assertEquals(info.terminatedPosition.ledgerId, lastPosition.getLedgerId());
417+
assertEquals(info.terminatedPosition.entryId, lastPosition.getEntryId());
418+
419+
Awaitility.await().untilAsserted(() -> assertEquals(bkc.getLedgers(), Set.of(lastPosition.getLedgerId())));
420+
421+
try {
422+
ledger.addEntry("entry-after-terminate".getBytes());
423+
fail("Should have thrown exception");
424+
} catch (ManagedLedgerTerminatedException e) {
425+
// Expected
426+
}
427+
}
428+
165429
}

0 commit comments

Comments
 (0)