/**
* Copyright 2016 Yahoo Inc.
*
* Licensed 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.bookkeeper.mledger.impl;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.fail;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.bookkeeper.client.BKException;
import org.apache.bookkeeper.client.BookKeeper.DigestType;
import org.apache.bookkeeper.mledger.AsyncCallbacks.AddEntryCallback;
import org.apache.bookkeeper.mledger.AsyncCallbacks.CloseCallback;
import org.apache.bookkeeper.mledger.Entry;
import org.apache.bookkeeper.mledger.ManagedCursor;
import org.apache.bookkeeper.mledger.ManagedLedger;
import org.apache.bookkeeper.mledger.ManagedLedgerConfig;
import org.apache.bookkeeper.mledger.ManagedLedgerException;
import org.apache.bookkeeper.mledger.ManagedLedgerException.BadVersionException;
import org.apache.bookkeeper.mledger.ManagedLedgerException.ManagedLedgerFencedException;
import org.apache.bookkeeper.mledger.ManagedLedgerFactory;
import org.apache.bookkeeper.mledger.Position;
import org.apache.bookkeeper.test.MockedBookKeeperTestCase;
import org.apache.zookeeper.KeeperException.Code;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.Test;
public class ManagedLedgerErrorsTest extends MockedBookKeeperTestCase {
@Test
public void removingCursor() throws Exception {
ManagedLedger ledger = factory.open("my_test_ledger");
ManagedCursor c1 = ledger.openCursor("c1");
assertEquals(zkc.exists("/managed-ledgers/my_test_ledger/c1", false) != null, true);
zkc.failNow(Code.BADVERSION);
try {
c1.close();
fail("should fail");
} catch (ManagedLedgerException e) {
// ok
}
bkc.failNow(BKException.Code.NoSuchLedgerExistsException);
// Cursor ledger deletion will fail, but that should not prevent the deleteCursor to fail
ledger.deleteCursor("c1");
assertEquals(zkc.exists("/managed-ledgers/my_test_ledger/c1", false) != null, false);
assertEquals(bkc.getLedgers().size(), 2);
}
@Test
public void removingCursor2() throws Exception {
ManagedLedger ledger = factory.open("my_test_ledger");
ledger.openCursor("c1");
zkc.failNow(Code.CONNECTIONLOSS);
try {
ledger.deleteCursor("c1");
fail("should fail");
} catch (ManagedLedgerException e) {
// ok
}
}
@Test
public void closingManagedLedger() throws Exception {
ManagedLedger ledger = factory.open("my_test_ledger");
ledger.openCursor("c1");
ledger.addEntry("entry".getBytes());
bkc.failNow(BKException.Code.NoSuchLedgerExistsException);
try {
ledger.close();
fail("should fail");
} catch (ManagedLedgerException e) {
// ok
}
// ML should be closed even if it failed before
try {
ledger.addEntry("entry".getBytes());
fail("managed ledger was closed");
} catch (ManagedLedgerException e) {
// ok
}
}
@Test
public void asyncClosingManagedLedger() throws Exception {
ManagedLedger ledger = factory.open("my_test_ledger");
ledger.openCursor("c1");
bkc.failNow(BKException.Code.NoSuchLedgerExistsException);
final CountDownLatch latch = new CountDownLatch(1);
ledger.asyncClose(new CloseCallback() {
public void closeFailed(ManagedLedgerException exception, Object ctx) {
latch.countDown();
}
public void closeComplete(Object ctx) {
fail("should have failed");
}
}, null);
latch.await();
}
@Test
public void errorInRecovering() throws Exception {
ManagedLedger ledger = factory.open("my_test_ledger");
ledger.addEntry("entry".getBytes());
ledger.close();
factory = new ManagedLedgerFactoryImpl(bkc, zkc);
bkc.failNow(BKException.Code.LedgerFencedException);
try {
ledger = factory.open("my_test_ledger");
fail("should fail");
} catch (ManagedLedgerException e) {
// ok
}
// It should be fine now
ledger = factory.open("my_test_ledger");
}
@Test
public void errorInRecovering2() throws Exception {
ManagedLedger ledger = factory.open("my_test_ledger");
ledger.addEntry("entry".getBytes());
ledger.close();
factory = new ManagedLedgerFactoryImpl(bkc, zkc);
bkc.failAfter(1, BKException.Code.LedgerFencedException);
try {
ledger = factory.open("my_test_ledger");
fail("should fail");
} catch (ManagedLedgerException e) {
// ok
}
// It should be fine now
ledger = factory.open("my_test_ledger");
}
@Test
public void errorInRecovering3() throws Exception {
ManagedLedger ledger = factory.open("my_test_ledger");
ledger.addEntry("entry".getBytes());
ledger.close();
factory = new ManagedLedgerFactoryImpl(bkc, zkc);
bkc.failAfter(1, BKException.Code.LedgerFencedException);
try {
ledger = factory.open("my_test_ledger");
fail("should fail");
} catch (ManagedLedgerException e) {
// ok
}
// It should be fine now
ledger = factory.open("my_test_ledger");
}
@Test
public void errorInRecovering4() throws Exception {
ManagedLedger ledger = factory.open("my_test_ledger");
ledger.addEntry("entry".getBytes());
ledger.close();
factory = new ManagedLedgerFactoryImpl(bkc, zkc);
zkc.failAfter(1, Code.CONNECTIONLOSS);
try {
ledger = factory.open("my_test_ledger");
fail("should fail");
} catch (ManagedLedgerException e) {
// ok
}
// It should be fine now
ledger = factory.open("my_test_ledger");
}
@Test
public void errorInRecovering5() throws Exception {
ManagedLedger ledger = factory.open("my_test_ledger");
ledger.addEntry("entry".getBytes());
ledger.close();
factory = new ManagedLedgerFactoryImpl(bkc, zkc);
zkc.failAfter(2, Code.CONNECTIONLOSS);
try {
ledger = factory.open("my_test_ledger");
fail("should fail");
} catch (ManagedLedgerException e) {
// ok
}
// It should be fine now
ledger = factory.open("my_test_ledger");
}
@Test
public void errorInRecovering6() throws Exception {
ManagedLedger ledger = factory.open("my_test_ledger");
ledger.openCursor("c1");
ledger.addEntry("entry".getBytes());
ledger.close();
factory = new ManagedLedgerFactoryImpl(bkc, zkc);
zkc.failAfter(3, Code.CONNECTIONLOSS);
try {
ledger = factory.open("my_test_ledger");
fail("should fail");
} catch (ManagedLedgerException e) {
// ok
}
// It should be fine now
ledger = factory.open("my_test_ledger");
}
@Test
public void passwordError() throws Exception {
ManagedLedger ledger = factory.open("my_test_ledger", new ManagedLedgerConfig().setPassword("password"));
ledger.openCursor("c1");
ledger.addEntry("entry".getBytes());
ledger.close();
try {
ledger = factory.open("my_test_ledger", new ManagedLedgerConfig().setPassword("wrong-password"));
fail("should fail for password error");
} catch (ManagedLedgerException e) {
// ok
}
}
@Test
public void digestError() throws Exception {
ManagedLedger ledger = factory.open("my_test_ledger",
new ManagedLedgerConfig().setDigestType(DigestType.CRC32));
ledger.openCursor("c1");
ledger.addEntry("entry".getBytes());
ledger.close();
try {
ledger = factory.open("my_test_ledger", new ManagedLedgerConfig().setDigestType(DigestType.MAC));
fail("should fail for digest error");
} catch (ManagedLedgerException e) {
// ok
}
}
@Test(timeOut = 20000, invocationCount = 1, skipFailedInvocations = true, enabled = false)
public void errorInUpdatingLedgersList() throws Exception {
ManagedLedger ledger = factory.open("my_test_ledger", new ManagedLedgerConfig().setMaxEntriesPerLedger(1));
final CountDownLatch latch = new CountDownLatch(1);
zkc.failAfter(0, Code.CONNECTIONLOSS);
ledger.asyncAddEntry("entry".getBytes(), new AddEntryCallback() {
public void addFailed(ManagedLedgerException exception, Object ctx) {
// not-ok
}
public void addComplete(Position position, Object ctx) {
// ok
}
}, null);
ledger.asyncAddEntry("entry".getBytes(), new AddEntryCallback() {
public void addFailed(ManagedLedgerException exception, Object ctx) {
latch.countDown();
}
public void addComplete(Position position, Object ctx) {
fail("should have failed");
}
}, null);
latch.await();
}
@Test
public void recoverAfterWriteError() throws Exception {
ManagedLedger ledger = factory.open("my_test_ledger");
ManagedCursor cursor = ledger.openCursor("c1");
bkc.failNow(BKException.Code.BookieHandleNotAvailableException);
// With one single error, the write should succeed
ledger.addEntry("entry".getBytes());
assertEquals(cursor.getNumberOfEntriesInBacklog(), 1);
bkc.failNow(BKException.Code.BookieHandleNotAvailableException);
zkc.failNow(Code.CONNECTIONLOSS);
try {
ledger.addEntry("entry".getBytes());
fail("should fail");
} catch (ManagedLedgerException e) {
// ok
}
assertEquals(cursor.getNumberOfEntriesInBacklog(), 1);
// Next add will fail as well
try {
ledger.addEntry("entry".getBytes());
fail("should fail");
} catch (ManagedLedgerException e) {
// ok
}
assertEquals(cursor.getNumberOfEntriesInBacklog(), 1);
}
@Test
public void recoverAfterZnodeVersionError() throws Exception {
ManagedLedger ledger = factory.open("my_test_ledger", new ManagedLedgerConfig().setMaxEntriesPerLedger(1));
zkc.failNow(Code.BADVERSION);
// First write will succeed
ledger.addEntry("test".getBytes());
try {
// This write will try to create a ledger and it will fail at it
ledger.addEntry("entry".getBytes());
fail("should fail");
} catch (BadVersionException e) {
// ok
}
try {
// At this point the ledger should be fenced for good
ledger.addEntry("entry".getBytes());
fail("should fail");
} catch (ManagedLedgerFencedException e) {
// ok
}
}
@Test
public void recoverLongTimeAfterWriteError() throws Exception {
ManagedLedger ledger = factory.open("my_test_ledger");
ManagedCursor cursor = ledger.openCursor("c1");
bkc.failNow(BKException.Code.BookieHandleNotAvailableException);
// With one single error, the write should succeed
ledger.addEntry("entry-1".getBytes());
assertEquals(cursor.getNumberOfEntriesInBacklog(), 1);
bkc.failNow(BKException.Code.BookieHandleNotAvailableException);
zkc.failNow(Code.CONNECTIONLOSS);
try {
ledger.addEntry("entry-2".getBytes());
fail("should fail");
} catch (ManagedLedgerException e) {
// ok
}
Thread.sleep(ManagedLedgerImpl.WaitTimeAfterLedgerCreationFailureMs / 2);
try {
ledger.addEntry("entry-3".getBytes());
fail("should fail");
} catch (ManagedLedgerException e) {
// ok
}
// After some time, the managed ledger will be available for writes again
Thread.sleep(ManagedLedgerImpl.WaitTimeAfterLedgerCreationFailureMs / 2 + 10);
// Next add should succeed, and the previous write should not appear
ledger.addEntry("entry-4".getBytes());
assertEquals(cursor.getNumberOfEntriesInBacklog(), 2);
List<Entry> entries = cursor.readEntries(10);
assertEquals(entries.size(), 2);
assertEquals(new String(entries.get(0).getData()), "entry-1");
assertEquals(new String(entries.get(1).getData()), "entry-4");
entries.forEach(e -> e.release());
}
@Test
public void recoverLongTimeAfterMultipleWriteErrors() throws Exception {
ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("recoverLongTimeAfterMultipleWriteErrors");
ManagedCursor cursor = ledger.openCursor("c1");
bkc.failNow(BKException.Code.BookieHandleNotAvailableException,
BKException.Code.BookieHandleNotAvailableException);
CountDownLatch counter = new CountDownLatch(2);
AtomicReference<ManagedLedgerException> ex = new AtomicReference<>();
// Write 2 entries, both should fail the first time and get re-tried internally in a new ledger
AddEntryCallback cb = new AddEntryCallback() {
@Override
public void addComplete(Position position, Object ctx) {
counter.countDown();
}
@Override
public void addFailed(ManagedLedgerException exception, Object ctx) {
log.warn("Error in write", exception);
ex.set(exception);
counter.countDown();
}
};
ledger.asyncAddEntry("entry-1".getBytes(), cb, null);
ledger.asyncAddEntry("entry-2".getBytes(), cb, null);
counter.await();
assertNull(ex.get());
assertEquals(cursor.getNumberOfEntriesInBacklog(), 2);
// Ensure that we are only creating one new ledger
// even when there are multiple (here, 2) add entry failed ops
assertEquals(ledger.getLedgersInfoAsList().size(), 1);
ledger.addEntry("entry-3".getBytes());
List<Entry> entries = cursor.readEntries(10);
assertEquals(entries.size(), 3);
assertEquals(new String(entries.get(0).getData()), "entry-1");
assertEquals(new String(entries.get(1).getData()), "entry-2");
assertEquals(new String(entries.get(2).getData()), "entry-3");
entries.forEach(e -> e.release());
}
@Test
public void recoverAfterMarkDeleteError() throws Exception {
ManagedLedger ledger = factory.open("my_test_ledger");
ManagedCursor cursor = ledger.openCursor("my-cursor");
Position position = ledger.addEntry("entry".getBytes());
bkc.failNow(BKException.Code.BookieHandleNotAvailableException);
try {
cursor.markDelete(position);
fail("should fail");
} catch (ManagedLedgerException e) {
// ok
}
// The metadata ledger is reopened in background, until it's not reopened the mark-delete will fail
Thread.sleep(100);
// Next markDelete should succeed
cursor.markDelete(position);
}
@Test
public void handleCursorRecoveryFailure() throws Exception {
ManagedLedger ledger = factory.open("my_test_ledger");
ManagedCursor cursor = ledger.openCursor("my-cursor");
Position p0 = cursor.getMarkDeletedPosition();
Position p1 = ledger.addEntry("entry-1".getBytes());
cursor.markDelete(p1);
// Re-open from a different factory
ManagedLedgerFactory factory2 = new ManagedLedgerFactoryImpl(bkc, zkc);
bkc.failAfter(3, BKException.Code.LedgerRecoveryException);
ledger = factory2.open("my_test_ledger");
cursor = ledger.openCursor("my-cursor");
// Since the cursor was rewind, it will be back to p0
assertEquals(cursor.getMarkDeletedPosition(), p0);
factory2.shutdown();
}
private static final Logger log = LoggerFactory.getLogger(ManagedLedgerErrorsTest.class);
}