/** * 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.assertFalse; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; import org.apache.bookkeeper.client.BKException; import org.apache.bookkeeper.mledger.AsyncCallbacks.AddEntryCallback; import org.apache.bookkeeper.mledger.AsyncCallbacks.CloseCallback; import org.apache.bookkeeper.mledger.AsyncCallbacks.DeleteLedgerCallback; import org.apache.bookkeeper.mledger.AsyncCallbacks.MarkDeleteCallback; import org.apache.bookkeeper.mledger.AsyncCallbacks.OpenCursorCallback; import org.apache.bookkeeper.mledger.AsyncCallbacks.OpenLedgerCallback; import org.apache.bookkeeper.mledger.AsyncCallbacks.ReadEntriesCallback; import org.apache.bookkeeper.mledger.Entry; import org.apache.bookkeeper.mledger.ManagedCursor; import org.apache.bookkeeper.mledger.ManagedCursor.IndividualDeletedEntries; import org.apache.bookkeeper.mledger.ManagedLedger; import org.apache.bookkeeper.mledger.ManagedLedgerConfig; import org.apache.bookkeeper.mledger.ManagedLedgerException; import org.apache.bookkeeper.mledger.ManagedLedgerException.ManagedLedgerFencedException; import org.apache.bookkeeper.mledger.ManagedLedgerException.MetaStoreException; import org.apache.bookkeeper.mledger.ManagedLedgerFactory; import org.apache.bookkeeper.mledger.Position; import org.apache.bookkeeper.mledger.impl.MetaStore.MetaStoreCallback; import org.apache.bookkeeper.mledger.impl.MetaStore.Stat; import org.apache.bookkeeper.mledger.impl.MetaStoreImplZookeeper.ZNodeProtobufFormat; import org.apache.bookkeeper.mledger.proto.MLDataFormats.ManagedLedgerInfo; import org.apache.bookkeeper.mledger.proto.MLDataFormats.ManagedLedgerInfo.LedgerInfo; import org.apache.bookkeeper.mledger.util.Pair; import org.apache.bookkeeper.test.MockedBookKeeperTestCase; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException.Code; import org.apache.zookeeper.ZooDefs; import org.apache.zookeeper.ZooKeeper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.annotations.Factory; import org.testng.annotations.Test; import com.google.common.base.Charsets; import com.google.common.collect.Sets; import com.yahoo.pulsar.common.api.DoubleByteBuf; import com.yahoo.pulsar.common.api.proto.PulsarApi.MessageMetadata; import com.yahoo.pulsar.common.util.protobuf.ByteBufCodedOutputStream; import io.netty.buffer.ByteBuf; import io.netty.buffer.PooledByteBufAllocator; import io.netty.buffer.Unpooled; public class ManagedLedgerTest extends MockedBookKeeperTestCase { private static final Logger log = LoggerFactory.getLogger(ManagedLedgerTest.class); private static final Charset Encoding = Charsets.UTF_8; @Factory(dataProvider = "protobufFormat") public ManagedLedgerTest(ZNodeProtobufFormat protobufFormat) { super(); this.protobufFormat = protobufFormat; } @Test public void managedLedgerApi() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); ManagedCursor cursor = ledger.openCursor("c1"); for (int i = 0; i < 100; i++) { String content = "entry-" + i; ledger.addEntry(content.getBytes()); } // Reads all the entries in batches of 20 while (cursor.hasMoreEntries()) { List<Entry> entries = cursor.readEntries(20); log.debug("Read {} entries", entries.size()); // Acknowledge only on last entry Entry lastEntry = entries.get(entries.size() - 1); cursor.markDelete(lastEntry.getPosition()); for (Entry entry : entries) { log.info("Read entry. Position={} Content='{}'", entry.getPosition(), new String(entry.getData())); entry.release(); } log.info("-----------------------"); } log.info("Finished reading entries"); ledger.close(); } @Test(timeOut = 20000) public void simple() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); assertEquals(ledger.getNumberOfEntries(), 0); assertEquals(ledger.getNumberOfActiveEntries(), 0); assertEquals(ledger.getTotalSize(), 0); ledger.addEntry("dummy-entry-1".getBytes(Encoding)); assertEquals(ledger.getNumberOfEntries(), 1); assertEquals(ledger.getNumberOfActiveEntries(), 0); assertEquals(ledger.getTotalSize(), "dummy-entry-1".getBytes(Encoding).length); ManagedCursor cursor = ledger.openCursor("c1"); assertEquals(cursor.hasMoreEntries(), false); assertEquals(cursor.getNumberOfEntries(), 0); assertEquals(cursor.getNumberOfEntriesInBacklog(), 0); assertEquals(cursor.readEntries(100), new ArrayList<Entry>()); ledger.addEntry("dummy-entry-2".getBytes(Encoding)); assertEquals(cursor.hasMoreEntries(), true); assertEquals(cursor.getNumberOfEntries(), 1); assertEquals(cursor.getNumberOfEntriesInBacklog(), 1); assertEquals(ledger.getNumberOfActiveEntries(), 1); List<Entry> entries = cursor.readEntries(100); assertEquals(entries.size(), 1); entries.forEach(e -> e.release()); entries = cursor.readEntries(100); assertEquals(entries.size(), 0); ledger.close(); factory.shutdown(); } @Test(timeOut = 20000) public void closeAndReopen() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); ledger.addEntry("dummy-entry-1".getBytes(Encoding)); ManagedCursor cursor = ledger.openCursor("c1"); ledger.addEntry("dummy-entry-2".getBytes(Encoding)); ledger.close(); log.info("Closing ledger and reopening"); // / Reopen the same managed-ledger ManagedLedgerFactoryImpl factory2 = new ManagedLedgerFactoryImpl(bkc, bkc.getZkHandle()); ledger = factory2.open("my_test_ledger"); cursor = ledger.openCursor("c1"); assertEquals(ledger.getNumberOfEntries(), 2); assertEquals(ledger.getTotalSize(), "dummy-entry-1".getBytes(Encoding).length * 2); List<Entry> entries = cursor.readEntries(100); assertEquals(entries.size(), 1); entries.forEach(e -> e.release()); ledger.close(); factory2.shutdown(); } @Test(timeOut = 20000) public void acknowledge1() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); ManagedCursor cursor = ledger.openCursor("c1"); ledger.addEntry("dummy-entry-1".getBytes(Encoding)); ledger.addEntry("dummy-entry-2".getBytes(Encoding)); assertEquals(cursor.hasMoreEntries(), true); List<Entry> entries = cursor.readEntries(2); assertEquals(entries.size(), 2); assertEquals(cursor.getNumberOfEntries(), 0); assertEquals(cursor.getNumberOfEntriesInBacklog(), 2); assertEquals(cursor.hasMoreEntries(), false); assertEquals(ledger.getNumberOfEntries(), 2); assertEquals(ledger.getNumberOfActiveEntries(), 2); cursor.markDelete(entries.get(0).getPosition()); entries.forEach(e -> e.release()); assertEquals(cursor.getNumberOfEntries(), 0); assertEquals(cursor.getNumberOfEntriesInBacklog(), 1); assertEquals(cursor.hasMoreEntries(), false); assertEquals(ledger.getNumberOfActiveEntries(), 1); ledger.close(); // / Reopen the same managed-ledger ledger = factory.open("my_test_ledger"); cursor = ledger.openCursor("c1"); assertEquals(ledger.getNumberOfEntries(), 2); assertEquals(ledger.getTotalSize(), "dummy-entry-1".getBytes(Encoding).length * 2); assertEquals(cursor.getNumberOfEntries(), 1); assertEquals(cursor.getNumberOfEntriesInBacklog(), 1); assertEquals(cursor.hasMoreEntries(), true); entries = cursor.readEntries(100); assertEquals(entries.size(), 1); entries.forEach(e -> e.release()); ledger.close(); } @Test(timeOut = 20000) public void asyncAPI() throws Throwable { final CountDownLatch counter = new CountDownLatch(1); factory.asyncOpen("my_test_ledger", new ManagedLedgerConfig(), new OpenLedgerCallback() { @Override public void openLedgerComplete(ManagedLedger ledger, Object ctx) { ledger.asyncOpenCursor("test-cursor", new OpenCursorCallback() { @Override public void openCursorComplete(ManagedCursor cursor, Object ctx) { ManagedLedger ledger = (ManagedLedger) ctx; ledger.asyncAddEntry("test".getBytes(Encoding), new AddEntryCallback() { @Override public void addComplete(Position position, Object ctx) { @SuppressWarnings("unchecked") Pair<ManagedLedger, ManagedCursor> pair = (Pair<ManagedLedger, ManagedCursor>) ctx; ManagedLedger ledger = pair.first; ManagedCursor cursor = pair.second; assertEquals(ledger.getNumberOfEntries(), 1); assertEquals(ledger.getTotalSize(), "test".getBytes(Encoding).length); cursor.asyncReadEntries(2, new ReadEntriesCallback() { @Override public void readEntriesComplete(List<Entry> entries, Object ctx) { ManagedCursor cursor = (ManagedCursor) ctx; assertEquals(entries.size(), 1); Entry entry = entries.get(0); final Position position = entry.getPosition(); assertEquals(new String(entry.getDataAndRelease(), Encoding), "test"); log.debug("Mark-Deleting to position {}", position); cursor.asyncMarkDelete(position, new MarkDeleteCallback() { @Override public void markDeleteComplete(Object ctx) { log.debug("Mark delete complete"); ManagedCursor cursor = (ManagedCursor) ctx; assertEquals(cursor.hasMoreEntries(), false); counter.countDown(); } @Override public void markDeleteFailed(ManagedLedgerException exception, Object ctx) { fail(exception.getMessage()); } }, cursor); } @Override public void readEntriesFailed(ManagedLedgerException exception, Object ctx) { fail(exception.getMessage()); } }, cursor); } @Override public void addFailed(ManagedLedgerException exception, Object ctx) { fail(exception.getMessage()); } }, new Pair<ManagedLedger, ManagedCursor>(ledger, cursor)); } @Override public void openCursorFailed(ManagedLedgerException exception, Object ctx) { fail(exception.getMessage()); } }, ledger); } @Override public void openLedgerFailed(ManagedLedgerException exception, Object ctx) { fail(exception.getMessage()); } }, null); counter.await(); log.info("Test completed"); } @Test(timeOut = 20000) public void spanningMultipleLedgers() throws Exception { ManagedLedgerConfig config = new ManagedLedgerConfig().setMaxEntriesPerLedger(10); ManagedLedger ledger = factory.open("my_test_ledger", config); assertEquals(ledger.getNumberOfEntries(), 0); assertEquals(ledger.getTotalSize(), 0); ManagedCursor cursor = ledger.openCursor("c1"); for (int i = 0; i < 11; i++) ledger.addEntry(("dummy-entry-" + i).getBytes(Encoding)); List<Entry> entries = cursor.readEntries(100); assertEquals(entries.size(), 11); assertEquals(cursor.hasMoreEntries(), false); PositionImpl first = (PositionImpl) entries.get(0).getPosition(); PositionImpl last = (PositionImpl) entries.get(entries.size() - 1).getPosition(); entries.forEach(e -> e.release()); log.info("First={} Last={}", first, last); assertTrue(first.getLedgerId() < last.getLedgerId()); assertEquals(first.getEntryId(), 0); assertEquals(last.getEntryId(), 0); // Read again, from next ledger id entries = cursor.readEntries(100); assertEquals(entries.size(), 0); assertEquals(cursor.hasMoreEntries(), false); ledger.close(); } @Test(timeOut = 20000) public void spanningMultipleLedgersWithSize() throws Exception { ManagedLedgerConfig config = new ManagedLedgerConfig().setMaxEntriesPerLedger(1000000); config.setMaxSizePerLedgerMb(1); config.setEnsembleSize(1); config.setWriteQuorumSize(1).setAckQuorumSize(1); config.setMetadataWriteQuorumSize(1).setMetadataAckQuorumSize(1); ManagedLedger ledger = factory.open("my_test_ledger", config); assertEquals(ledger.getNumberOfEntries(), 0); assertEquals(ledger.getTotalSize(), 0); ManagedCursor cursor = ledger.openCursor("c1"); byte[] content = new byte[1023 * 1024]; for (int i = 0; i < 3; i++) ledger.addEntry(content); List<Entry> entries = cursor.readEntries(100); assertEquals(entries.size(), 3); assertEquals(cursor.hasMoreEntries(), false); PositionImpl first = (PositionImpl) entries.get(0).getPosition(); PositionImpl last = (PositionImpl) entries.get(entries.size() - 1).getPosition(); entries.forEach(e -> e.release()); // Read again, from next ledger id entries = cursor.readEntries(100); assertEquals(entries.size(), 0); assertEquals(cursor.hasMoreEntries(), false); entries.forEach(e -> e.release()); log.info("First={} Last={}", first, last); assertTrue(first.getLedgerId() < last.getLedgerId()); assertEquals(first.getEntryId(), 0); assertEquals(last.getEntryId(), 0); ledger.close(); } @Test(expectedExceptions = IllegalArgumentException.class) public void invalidReadEntriesArg1() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); ManagedCursor cursor = ledger.openCursor("c1"); ledger.addEntry("entry".getBytes()); cursor.readEntries(-1); fail("Should have thrown an exception in the above line"); } @Test(expectedExceptions = IllegalArgumentException.class) public void invalidReadEntriesArg2() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); ManagedCursor cursor = ledger.openCursor("c1"); ledger.addEntry("entry".getBytes()); cursor.readEntries(0); fail("Should have thrown an exception in the above line"); } @Test(timeOut = 20000) public void deleteAndReopen() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); ledger.addEntry("dummy-entry-1".getBytes(Encoding)); assertEquals(ledger.getNumberOfEntries(), 1); ledger.close(); // Reopen ledger = factory.open("my_test_ledger"); assertEquals(ledger.getNumberOfEntries(), 1); // Delete and reopen ledger.delete(); ledger = factory.open("my_test_ledger"); assertEquals(ledger.getNumberOfEntries(), 0); ledger.close(); } @Test(timeOut = 20000) public void deleteAndReopenWithCursors() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); ledger.openCursor("test-cursor"); ledger.addEntry("dummy-entry-1".getBytes(Encoding)); assertEquals(ledger.getNumberOfEntries(), 1); ledger.close(); // Reopen ledger = factory.open("my_test_ledger"); assertEquals(ledger.getNumberOfEntries(), 1); // Delete and reopen ledger.delete(); ledger = factory.open("my_test_ledger"); assertEquals(ledger.getNumberOfEntries(), 0); ManagedCursor cursor = ledger.openCursor("test-cursor"); assertEquals(cursor.hasMoreEntries(), false); ledger.close(); } @Test(timeOut = 20000) public void asyncDeleteWithError() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); ledger.openCursor("test-cursor"); ledger.addEntry("dummy-entry-1".getBytes(Encoding)); assertEquals(ledger.getNumberOfEntries(), 1); ledger.close(); // Reopen ledger = factory.open("my_test_ledger"); assertEquals(ledger.getNumberOfEntries(), 1); final CountDownLatch counter = new CountDownLatch(1); stopBookKeeper(); stopZooKeeper(); // Delete and reopen factory.open("my_test_ledger", new ManagedLedgerConfig()).asyncDelete(new DeleteLedgerCallback() { @Override public void deleteLedgerComplete(Object ctx) { assertNull(ctx); fail("The async-call should have failed"); } @Override public void deleteLedgerFailed(ManagedLedgerException exception, Object ctx) { counter.countDown(); } }, null); counter.await(); } @Test(timeOut = 20000) public void asyncAddEntryWithoutError() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); ledger.openCursor("test-cursor"); final CountDownLatch counter = new CountDownLatch(1); ledger.asyncAddEntry("dummy-entry-1".getBytes(Encoding), new AddEntryCallback() { @Override public void addComplete(Position position, Object ctx) { assertNull(ctx); counter.countDown(); } @Override public void addFailed(ManagedLedgerException exception, Object ctx) { fail(exception.getMessage()); } }, null); counter.await(); assertEquals(ledger.getNumberOfEntries(), 1); assertEquals(ledger.getTotalSize(), "dummy-entry-1".getBytes(Encoding).length); } @Test(timeOut = 20000) public void doubleAsyncAddEntryWithoutError() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); ledger.openCursor("test-cursor"); final CountDownLatch done = new CountDownLatch(10); for (int i = 0; i < 10; i++) { final String content = "dummy-entry-" + i; ledger.asyncAddEntry(content.getBytes(Encoding), new AddEntryCallback() { @Override public void addComplete(Position position, Object ctx) { assertNotNull(ctx); log.info("Successfully added {}", content); done.countDown(); } @Override public void addFailed(ManagedLedgerException exception, Object ctx) { fail(exception.getMessage()); } }, this); } done.await(); assertEquals(ledger.getNumberOfEntries(), 10); } @Test(timeOut = 20000) public void asyncAddEntryWithError() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); ledger.openCursor("test-cursor"); final CountDownLatch counter = new CountDownLatch(1); stopBookKeeper(); stopZooKeeper(); ledger.asyncAddEntry("dummy-entry-1".getBytes(Encoding), new AddEntryCallback() { @Override public void addComplete(Position position, Object ctx) { fail("Should have failed"); } @Override public void addFailed(ManagedLedgerException exception, Object ctx) { counter.countDown(); } }, null); counter.await(); } @Test(timeOut = 20000) public void asyncCloseWithoutError() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); ledger.openCursor("test-cursor"); ledger.addEntry("dummy-entry-1".getBytes(Encoding)); final CountDownLatch counter = new CountDownLatch(1); ledger.asyncClose(new CloseCallback() { @Override public void closeComplete(Object ctx) { assertNull(ctx); counter.countDown(); } @Override public void closeFailed(ManagedLedgerException exception, Object ctx) { fail(exception.getMessage()); } }, null); counter.await(); } @Test(timeOut = 20000) public void asyncOpenCursorWithoutError() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); final CountDownLatch counter = new CountDownLatch(1); ledger.asyncOpenCursor("test-cursor", new OpenCursorCallback() { @Override public void openCursorComplete(ManagedCursor cursor, Object ctx) { assertNull(ctx); assertNotNull(cursor); counter.countDown(); } @Override public void openCursorFailed(ManagedLedgerException exception, Object ctx) { fail(exception.getMessage()); } }, null); counter.await(); } @Test(timeOut = 20000) public void asyncOpenCursorWithError() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); final CountDownLatch counter = new CountDownLatch(1); stopBookKeeper(); stopZooKeeper(); ledger.asyncOpenCursor("test-cursor", new OpenCursorCallback() { @Override public void openCursorComplete(ManagedCursor cursor, Object ctx) { fail("The async-call should have failed"); } @Override public void openCursorFailed(ManagedLedgerException exception, Object ctx) { counter.countDown(); } }, null); counter.await(); } @Test(timeOut = 20000) public void readFromOlderLedger() throws Exception { ManagedLedgerConfig config = new ManagedLedgerConfig().setMaxEntriesPerLedger(1); ManagedLedger ledger = factory.open("my_test_ledger", config); ManagedCursor cursor = ledger.openCursor("test"); ledger.addEntry("entry-1".getBytes(Encoding)); ledger.addEntry("entry-2".getBytes(Encoding)); assertEquals(cursor.hasMoreEntries(), true); } @Test(timeOut = 20000) public void readFromOlderLedgers() throws Exception { ManagedLedgerConfig config = new ManagedLedgerConfig().setMaxEntriesPerLedger(1); ManagedLedger ledger = factory.open("my_test_ledger", config); ManagedCursor cursor = ledger.openCursor("test"); ledger.addEntry("entry-1".getBytes(Encoding)); ledger.addEntry("entry-2".getBytes(Encoding)); ledger.addEntry("entry-3".getBytes(Encoding)); assertEquals(cursor.hasMoreEntries(), true); cursor.readEntries(1).forEach(e -> e.release()); assertEquals(cursor.hasMoreEntries(), true); cursor.readEntries(1).forEach(e -> e.release()); assertEquals(cursor.hasMoreEntries(), true); cursor.readEntries(1).forEach(e -> e.release()); assertEquals(cursor.hasMoreEntries(), false); } @Test(timeOut = 20000) public void triggerLedgerDeletion() throws Exception { ManagedLedgerConfig config = new ManagedLedgerConfig().setMaxEntriesPerLedger(1); ManagedLedger ledger = factory.open("my_test_ledger", config); ManagedCursor cursor = ledger.openCursor("test"); ledger.addEntry("entry-1".getBytes(Encoding)); ledger.addEntry("entry-2".getBytes(Encoding)); ledger.addEntry("entry-3".getBytes(Encoding)); assertEquals(cursor.hasMoreEntries(), true); List<Entry> entries = cursor.readEntries(1); assertEquals(entries.size(), 1); assertEquals(ledger.getNumberOfEntries(), 3); entries.forEach(e -> e.release()); assertEquals(cursor.hasMoreEntries(), true); entries = cursor.readEntries(1); assertEquals(cursor.hasMoreEntries(), true); cursor.markDelete(entries.get(0).getPosition()); entries.forEach(e -> e.release()); } @Test(timeOut = 20000) public void testEmptyManagedLedgerContent() throws Exception { ZooKeeper zk = bkc.getZkHandle(); zk.create("/managed-ledger", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); zk.create("/managed-ledger/my_test_ledger", " ".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); ManagedLedger ledger = factory.open("my_test_ledger"); ledger.openCursor("test"); ledger.addEntry("entry-1".getBytes(Encoding)); assertEquals(ledger.getNumberOfEntries(), 1); } @Test(timeOut = 20000) public void testProducerAndNoConsumer() throws Exception { ManagedLedgerConfig config = new ManagedLedgerConfig().setMaxEntriesPerLedger(1); ManagedLedger ledger = factory.open("my_test_ledger", config); assertEquals(ledger.getNumberOfEntries(), 0); ledger.addEntry("entry-1".getBytes(Encoding)); assertEquals(ledger.getNumberOfEntries(), 1); // Since there are no consumers, older ledger will be deleted // in a short time (in a background thread) ledger.addEntry("entry-2".getBytes(Encoding)); while (ledger.getNumberOfEntries() > 1) { log.debug("entries={}", ledger.getNumberOfEntries()); Thread.sleep(100); } ledger.addEntry("entry-3".getBytes(Encoding)); while (ledger.getNumberOfEntries() > 1) { log.debug("entries={}", ledger.getNumberOfEntries()); Thread.sleep(100); } } @Test(timeOut = 20000) public void testTrimmer() throws Exception { ManagedLedgerConfig config = new ManagedLedgerConfig().setMaxEntriesPerLedger(1); ManagedLedger ledger = factory.open("my_test_ledger", config); ManagedCursor cursor = ledger.openCursor("c1"); assertEquals(ledger.getNumberOfEntries(), 0); ledger.addEntry("entry-1".getBytes(Encoding)); ledger.addEntry("entry-2".getBytes(Encoding)); ledger.addEntry("entry-3".getBytes(Encoding)); ledger.addEntry("entry-4".getBytes(Encoding)); assertEquals(ledger.getNumberOfEntries(), 4); cursor.readEntries(1).forEach(e -> e.release()); cursor.readEntries(1).forEach(e -> e.release()); List<Entry> entries = cursor.readEntries(1); Position lastPosition = entries.get(0).getPosition(); entries.forEach(e -> e.release()); assertEquals(ledger.getNumberOfEntries(), 4); cursor.markDelete(lastPosition); while (ledger.getNumberOfEntries() != 2) { Thread.sleep(10); } } @Test(timeOut = 20000) public void testAsyncAddEntryAndSyncClose() throws Exception { ManagedLedgerConfig config = new ManagedLedgerConfig().setMaxEntriesPerLedger(10); ManagedLedger ledger = factory.open("my_test_ledger", config); ledger.openCursor("c1"); assertEquals(ledger.getNumberOfEntries(), 0); final CountDownLatch counter = new CountDownLatch(100); for (int i = 0; i < 100; i++) { String content = "entry-" + i; ledger.asyncAddEntry(content.getBytes(Encoding), new AddEntryCallback() { @Override public void addComplete(Position position, Object ctx) { counter.countDown(); } @Override public void addFailed(ManagedLedgerException exception, Object ctx) { fail(exception.getMessage()); } }, null); } counter.await(); assertEquals(ledger.getNumberOfEntries(), 100); } @Test(timeOut = 20000) public void moveCursorToNextLedger() throws Exception { ManagedLedgerConfig config = new ManagedLedgerConfig().setMaxEntriesPerLedger(1); ManagedLedger ledger = factory.open("my_test_ledger", config); ManagedCursor cursor = ledger.openCursor("test"); ledger.addEntry("entry-1".getBytes(Encoding)); log.debug("Added 1st message"); List<Entry> entries = cursor.readEntries(1); log.debug("read message ok"); assertEquals(entries.size(), 1); entries.forEach(e -> e.release()); ledger.addEntry("entry-2".getBytes(Encoding)); log.debug("Added 2nd message"); ledger.addEntry("entry-3".getBytes(Encoding)); log.debug("Added 3nd message"); assertEquals(cursor.hasMoreEntries(), true); assertEquals(cursor.getNumberOfEntries(), 2); entries = cursor.readEntries(2); assertEquals(entries.size(), 2); entries.forEach(e -> e.release()); entries = cursor.readEntries(2); assertEquals(entries.size(), 0); entries = cursor.readEntries(2); assertEquals(entries.size(), 0); } @Test(timeOut = 20000) public void differentSessions() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); assertEquals(ledger.getNumberOfEntries(), 0); assertEquals(ledger.getTotalSize(), 0); ManagedCursor cursor = ledger.openCursor("c1"); ledger.addEntry("dummy-entry-1".getBytes(Encoding)); assertEquals(ledger.getNumberOfEntries(), 1); assertEquals(ledger.getTotalSize(), "dummy-entry-1".getBytes(Encoding).length); assertEquals(cursor.hasMoreEntries(), true); assertEquals(cursor.getNumberOfEntries(), 1); ledger.close(); // Create a new factory and re-open the same managed ledger factory = new ManagedLedgerFactoryImpl(bkc, zkc); ledger = factory.open("my_test_ledger"); assertEquals(ledger.getNumberOfEntries(), 1); assertEquals(ledger.getTotalSize(), "dummy-entry-1".getBytes(Encoding).length); cursor = ledger.openCursor("c1"); assertEquals(cursor.hasMoreEntries(), true); assertEquals(cursor.getNumberOfEntries(), 1); ledger.addEntry("dummy-entry-2".getBytes(Encoding)); assertEquals(ledger.getNumberOfEntries(), 2); assertEquals(ledger.getTotalSize(), "dummy-entry-1".getBytes(Encoding).length * 2); assertEquals(cursor.hasMoreEntries(), true); assertEquals(cursor.getNumberOfEntries(), 2); ledger.close(); } @Test(enabled = false) public void fenceManagedLedger() throws Exception { ManagedLedgerFactory factory1 = new ManagedLedgerFactoryImpl(bkc, bkc.getZkHandle()); ManagedLedger ledger1 = factory1.open("my_test_ledger"); ManagedCursor cursor1 = ledger1.openCursor("c1"); ledger1.addEntry("entry-1".getBytes(Encoding)); ManagedLedgerFactory factory2 = new ManagedLedgerFactoryImpl(bkc, bkc.getZkHandle()); ManagedLedger ledger2 = factory2.open("my_test_ledger"); ManagedCursor cursor2 = ledger2.openCursor("c1"); // At this point ledger1 must have been fenced try { ledger1.addEntry("entry-1".getBytes(Encoding)); fail("Expecting exception"); } catch (ManagedLedgerFencedException e) { } try { ledger1.addEntry("entry-2".getBytes(Encoding)); fail("Expecting exception"); } catch (ManagedLedgerFencedException e) { } try { cursor1.readEntries(10); fail("Expecting exception"); } catch (ManagedLedgerFencedException e) { } try { ledger1.openCursor("new cursor"); fail("Expecting exception"); } catch (ManagedLedgerFencedException e) { } ledger2.addEntry("entry-2".getBytes(Encoding)); assertEquals(cursor2.getNumberOfEntries(), 2); factory1.shutdown(); factory2.shutdown(); } @Test public void forceCloseLedgers() throws Exception { ManagedLedger ledger1 = factory.open("my_test_ledger", new ManagedLedgerConfig().setMaxEntriesPerLedger(1)); ledger1.openCursor("c1"); ManagedCursor c2 = ledger1.openCursor("c2"); ledger1.addEntry("entry-1".getBytes(Encoding)); ledger1.addEntry("entry-2".getBytes(Encoding)); ledger1.addEntry("entry-3".getBytes(Encoding)); c2.readEntries(1).forEach(e -> e.release()); c2.readEntries(1).forEach(e -> e.release()); c2.readEntries(1).forEach(e -> e.release()); ledger1.close(); try { ledger1.addEntry("entry-3".getBytes(Encoding)); fail("should not have reached this point"); } catch (ManagedLedgerException e) { // ok } try { ledger1.openCursor("new-cursor"); fail("should not have reached this point"); } catch (ManagedLedgerException e) { // ok } } @Test public void closeLedgerWithError() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); ledger.addEntry("entry-1".getBytes(Encoding)); stopZooKeeper(); stopBookKeeper(); try { ledger.close(); // fail("should have thrown exception"); } catch (ManagedLedgerException e) { // Ok } } @Test(timeOut = 20000) public void deleteWithErrors1() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); PositionImpl position = (PositionImpl) ledger.addEntry("dummy-entry-1".getBytes(Encoding)); assertEquals(ledger.getNumberOfEntries(), 1); // Force delete a ledger and test that deleting the ML still happens // without errors bkc.deleteLedger(position.getLedgerId()); ledger.delete(); } @Test(timeOut = 20000) public void deleteWithErrors2() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); ledger.addEntry("dummy-entry-1".getBytes(Encoding)); stopZooKeeper(); try { ledger.delete(); fail("should have failed"); } catch (ManagedLedgerException e) { // ok } catch (RejectedExecutionException e) { // ok } } @Test(timeOut = 20000) public void readWithErrors1() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger", new ManagedLedgerConfig().setMaxEntriesPerLedger(1)); ManagedCursor cursor = ledger.openCursor("c1"); ledger.addEntry("dummy-entry-1".getBytes(Encoding)); ledger.addEntry("dummy-entry-2".getBytes(Encoding)); stopZooKeeper(); stopBookKeeper(); try { cursor.readEntries(10); fail("should have failed"); } catch (ManagedLedgerException e) { // ok } try { ledger.addEntry("dummy-entry-3".getBytes(Encoding)); fail("should have failed"); } catch (ManagedLedgerException e) { // ok } } @Test(timeOut = 20000, enabled = false) void concurrentAsyncOpen() throws Exception { final CountDownLatch counter = new CountDownLatch(2); class Result { ManagedLedger instance1 = null; ManagedLedger instance2 = null; } final Result result = new Result(); factory.asyncOpen("my-test-ledger", new OpenLedgerCallback() { @Override public void openLedgerComplete(ManagedLedger ledger, Object ctx) { result.instance1 = ledger; counter.countDown(); } @Override public void openLedgerFailed(ManagedLedgerException exception, Object ctx) { } }, null); factory.asyncOpen("my-test-ledger", new OpenLedgerCallback() { @Override public void openLedgerComplete(ManagedLedger ledger, Object ctx) { result.instance2 = ledger; counter.countDown(); } @Override public void openLedgerFailed(ManagedLedgerException exception, Object ctx) { } }, null); counter.await(); assertEquals(result.instance1, result.instance2); assertNotNull(result.instance1); } @Test // (timeOut = 20000) public void asyncOpenClosedLedger() throws Exception { ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("my-closed-ledger"); ManagedCursor c1 = ledger.openCursor("c1"); ledger.addEntry("dummy-entry-1".getBytes(Encoding)); c1.close(); assertEquals(ledger.getNumberOfEntries(), 1); ledger.setFenced(); final CountDownLatch counter = new CountDownLatch(1); class Result { ManagedLedger instance1 = null; } final Result result = new Result(); factory.asyncOpen("my-closed-ledger", new OpenLedgerCallback() { @Override public void openLedgerComplete(ManagedLedger ledger, Object ctx) { result.instance1 = ledger; counter.countDown(); } @Override public void openLedgerFailed(ManagedLedgerException exception, Object ctx) { } }, null); counter.await(); assertNotNull(result.instance1); ManagedCursor c2 = result.instance1.openCursor("c1"); List<Entry> entries = c2.readEntries(1); assertEquals(entries.size(), 1); entries.forEach(e -> e.release()); } @Test public void getCursors() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); ManagedCursor c1 = ledger.openCursor("c1"); ManagedCursor c2 = ledger.openCursor("c2"); assertEquals(Sets.newHashSet(ledger.getCursors()), Sets.newHashSet(c1, c2)); c1.close(); ledger.deleteCursor("c1"); assertEquals(Sets.newHashSet(ledger.getCursors()), Sets.newHashSet(c2)); c2.close(); ledger.deleteCursor("c2"); assertEquals(Sets.newHashSet(ledger.getCursors()), Sets.newHashSet()); } @Test public void ledgersList() throws Exception { MetaStore store = factory.getMetaStore(); assertEquals(Sets.newHashSet(store.getManagedLedgers()), Sets.newHashSet()); ManagedLedger ledger1 = factory.open("ledger1"); assertEquals(Sets.newHashSet(store.getManagedLedgers()), Sets.newHashSet("ledger1")); ManagedLedger ledger2 = factory.open("ledger2"); assertEquals(Sets.newHashSet(store.getManagedLedgers()), Sets.newHashSet("ledger1", "ledger2")); ledger1.delete(); assertEquals(Sets.newHashSet(store.getManagedLedgers()), Sets.newHashSet("ledger2")); ledger2.delete(); assertEquals(Sets.newHashSet(store.getManagedLedgers()), Sets.newHashSet()); } @Test public void testCleanup() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); ledger.openCursor("c1"); ledger.addEntry("data".getBytes(Encoding)); assertEquals(bkc.getLedgers().size(), 2); ledger.delete(); assertEquals(bkc.getLedgers().size(), 0); } @Test(timeOut = 20000) public void testAsyncCleanup() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); ledger.openCursor("c1"); ledger.addEntry("data".getBytes(Encoding)); assertEquals(bkc.getLedgers().size(), 2); final CountDownLatch latch = new CountDownLatch(1); ledger.asyncDelete(new DeleteLedgerCallback() { @Override public void deleteLedgerFailed(ManagedLedgerException exception, Object ctx) { fail("should have succeeded"); } @Override public void deleteLedgerComplete(Object ctx) { latch.countDown(); } }, null); latch.await(); assertEquals(bkc.getLedgers().size(), 0); } @Test(timeOut = 20000) public void testReopenAndCleanup() throws Exception { ManagedLedger ledger = factory.open("my_test_ledger"); ledger.openCursor("c1"); ledger.addEntry("data".getBytes(Encoding)); ledger.close(); Thread.sleep(100); assertEquals(bkc.getLedgers().size(), 1); factory.shutdown(); factory = new ManagedLedgerFactoryImpl(bkc, bkc.getZkHandle()); ledger = factory.open("my_test_ledger"); ledger.openCursor("c1"); Thread.sleep(100); assertEquals(bkc.getLedgers().size(), 2); ledger.close(); factory.open("my_test_ledger", new ManagedLedgerConfig()).delete(); Thread.sleep(100); assertEquals(bkc.getLedgers().size(), 0); factory.shutdown(); } @Test(timeOut = 20000) public void doubleOpen() throws Exception { ManagedLedger ledger1 = factory.open("my_test_ledger"); ManagedLedger ledger2 = factory.open("my_test_ledger"); assertTrue(ledger1 == ledger2); } @Test public void compositeNames() throws Exception { // Should not throw exception factory.open("my/test/ledger"); } @Test public void previousPosition() throws Exception { ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("my_test_ledger", new ManagedLedgerConfig().setMaxEntriesPerLedger(2)); ManagedCursor cursor = ledger.openCursor("my_cursor"); Position p0 = cursor.getMarkDeletedPosition(); // This is expected because p0 is already an "invalid" position (since no entry has been mark-deleted yet) assertEquals(ledger.getPreviousPosition((PositionImpl) p0), p0); // Force to close an empty ledger ledger.close(); ledger = (ManagedLedgerImpl) factory.open("my_test_ledger", new ManagedLedgerConfig().setMaxEntriesPerLedger(2)); // again ledger.close(); ledger = (ManagedLedgerImpl) factory.open("my_test_ledger", new ManagedLedgerConfig().setMaxEntriesPerLedger(2)); PositionImpl pBeforeWriting = ledger.getLastPosition(); PositionImpl p1 = (PositionImpl) ledger.addEntry("entry".getBytes()); ledger.close(); ledger = (ManagedLedgerImpl) factory.open("my_test_ledger", new ManagedLedgerConfig().setMaxEntriesPerLedger(2)); Position p2 = ledger.addEntry("entry".getBytes()); Position p3 = ledger.addEntry("entry".getBytes()); Position p4 = ledger.addEntry("entry".getBytes()); assertEquals(ledger.getPreviousPosition(p1), pBeforeWriting); assertEquals(ledger.getPreviousPosition((PositionImpl) p2), p1); assertEquals(ledger.getPreviousPosition((PositionImpl) p3), p2); assertEquals(ledger.getPreviousPosition((PositionImpl) p4), p3); } /** * Reproduce a race condition between opening cursors and concurrent mark delete operations */ @Test(timeOut = 20000) public void testOpenRaceCondition() throws Exception { ManagedLedgerConfig config = new ManagedLedgerConfig(); config.setEnsembleSize(2).setAckQuorumSize(2).setMetadataEnsembleSize(2); final ManagedLedger ledger = factory.open("my-ledger", config); final ManagedCursor c1 = ledger.openCursor("c1"); final int N = 1000; final Position position = ledger.addEntry("entry-0".getBytes()); Executor executor = Executors.newCachedThreadPool(); final CountDownLatch counter = new CountDownLatch(2); executor.execute(new Runnable() { @Override public void run() { try { for (int i = 0; i < N; i++) { c1.markDelete(position); } counter.countDown(); } catch (Exception e) { e.printStackTrace(); } } }); executor.execute(new Runnable() { @Override public void run() { try { for (int i = 0; i < N; i++) { ledger.openCursor("cursor-" + i); } counter.countDown(); } catch (Exception e) { e.printStackTrace(); } } }); // If there is the race condition, this method will not complete triggering the test timeout counter.await(); } @Test public void invalidateConsumedEntriesFromCache() throws Exception { ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("my_test_ledger"); EntryCacheManager cacheManager = factory.getEntryCacheManager(); EntryCache entryCache = ledger.entryCache; ManagedCursorImpl c1 = (ManagedCursorImpl) ledger.openCursor("c1"); ManagedCursorImpl c2 = (ManagedCursorImpl) ledger.openCursor("c2"); PositionImpl p1 = (PositionImpl) ledger.addEntry("entry-1".getBytes()); PositionImpl p2 = (PositionImpl) ledger.addEntry("entry-2".getBytes()); PositionImpl p3 = (PositionImpl) ledger.addEntry("entry-3".getBytes()); PositionImpl p4 = (PositionImpl) ledger.addEntry("entry-4".getBytes()); assertEquals(entryCache.getSize(), 7 * 4); assertEquals(cacheManager.getSize(), entryCache.getSize()); c2.setReadPosition(p3); ledger.discardEntriesFromCache(c2, p2); assertEquals(entryCache.getSize(), 7 * 4); assertEquals(cacheManager.getSize(), entryCache.getSize()); c1.setReadPosition(p2); ledger.discardEntriesFromCache(c1, p1); assertEquals(entryCache.getSize(), 7 * 3); assertEquals(cacheManager.getSize(), entryCache.getSize()); c1.setReadPosition(p3); ledger.discardEntriesFromCache(c1, p2); assertEquals(entryCache.getSize(), 7 * 2); assertEquals(cacheManager.getSize(), entryCache.getSize()); ledger.deactivateCursor(c1); assertEquals(entryCache.getSize(), 7 * 2); // as c2.readPosition=p3 => Cache contains p3,p4 assertEquals(cacheManager.getSize(), entryCache.getSize()); c2.setReadPosition(p4); ledger.discardEntriesFromCache(c2, p3); assertEquals(entryCache.getSize(), 7); assertEquals(cacheManager.getSize(), entryCache.getSize()); ledger.deactivateCursor(c2); assertEquals(entryCache.getSize(), 0); assertEquals(cacheManager.getSize(), entryCache.getSize()); } @Test public void discardEmptyLedgersOnClose() throws Exception { ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("my_test_ledger"); ManagedCursor c1 = ledger.openCursor("c1"); ledger.addEntry("entry".getBytes()); assertEquals(ledger.getLedgersInfoAsList().size(), 1); c1.close(); ledger.close(); // re-open ledger = (ManagedLedgerImpl) factory.open("my_test_ledger"); assertEquals(ledger.getLedgersInfoAsList().size(), 2); // 1 ledger with 1 entry and the current writing ledger c1.close(); ledger.close(); // re-open, now the previous empty ledger should have been discarded ledger = (ManagedLedgerImpl) factory.open("my_test_ledger"); assertEquals(ledger.getLedgersInfoAsList().size(), 2); // 1 ledger with 1 entry, and the current // writing ledger } @Test public void discardEmptyLedgersOnError() throws Exception { ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("my_test_ledger"); assertEquals(ledger.getLedgersInfoAsList().size(), 1); bkc.failNow(BKException.Code.NoBookieAvailableException); zkc.failNow(Code.CONNECTIONLOSS); try { ledger.addEntry("entry".getBytes()); fail("Should have received exception"); } catch (ManagedLedgerException e) { // Ok } assertEquals(ledger.getLedgersInfoAsList().size(), 0); // Next write should fail as well try { ledger.addEntry("entry".getBytes()); fail("Should have received exception"); } catch (ManagedLedgerException e) { // Ok } assertEquals(ledger.getLedgersInfoAsList().size(), 0); assertEquals(ledger.getNumberOfEntries(), 0); } @Test public void cursorReadsWithDiscardedEmptyLedgers() throws Exception { ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("my_test_ledger"); ManagedCursor c1 = ledger.openCursor("c1"); Position p1 = c1.getReadPosition(); c1.close(); ledger.close(); // re-open ledger = (ManagedLedgerImpl) factory.open("my_test_ledger"); c1 = ledger.openCursor("c1"); assertEquals(c1.getNumberOfEntries(), 0); assertEquals(c1.hasMoreEntries(), false); ledger.addEntry("entry".getBytes()); assertEquals(c1.getNumberOfEntries(), 1); assertEquals(c1.hasMoreEntries(), true); assertEquals(ledger.getLedgersInfoAsList().size(), 1); List<Entry> entries = c1.readEntries(1); assertEquals(entries.size(), 1); entries.forEach(e -> e.release()); assertEquals(c1.hasMoreEntries(), false); assertEquals(c1.readEntries(1).size(), 0); c1.seek(p1); assertEquals(c1.hasMoreEntries(), true); assertEquals(c1.getNumberOfEntries(), 1); entries = c1.readEntries(1); assertEquals(entries.size(), 1); entries.forEach(e -> e.release()); assertEquals(c1.readEntries(1).size(), 0); } @Test public void cursorReadsWithDiscardedEmptyLedgersStillListed() throws Exception { ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("my_test_ledger"); ManagedCursor c1 = ledger.openCursor("c1"); ledger.addEntry("entry-1".getBytes()); ledger.close(); ledger = (ManagedLedgerImpl) factory.open("my_test_ledger"); c1 = ledger.openCursor("c1"); ledger.addEntry("entry-2".getBytes()); final LedgerInfo l1info = ledger.getLedgersInfoAsList().get(0); final LedgerInfo l2info = ledger.getLedgersInfoAsList().get(1); ledger.close(); // Add the deleted ledger back in the meta-data to simulate an empty ledger that was deleted but not removed // from the list of ledgers final CountDownLatch counter = new CountDownLatch(1); final MetaStore store = factory.getMetaStore(); store.getManagedLedgerInfo("my_test_ledger", new MetaStoreCallback<ManagedLedgerInfo>() { @Override public void operationComplete(ManagedLedgerInfo result, Stat version) { // Update the list ManagedLedgerInfo.Builder info = ManagedLedgerInfo.newBuilder(result); info.clearLedgerInfo(); info.addLedgerInfo(LedgerInfo.newBuilder().setLedgerId(l1info.getLedgerId()).build()); info.addLedgerInfo(l2info); store.asyncUpdateLedgerIds("my_test_ledger", info.build(), version, new MetaStoreCallback<Void>() { @Override public void operationComplete(Void result, Stat version) { counter.countDown(); } @Override public void operationFailed(MetaStoreException e) { counter.countDown(); } }); } @Override public void operationFailed(MetaStoreException e) { counter.countDown(); } }); // Wait for the change to be effective counter.await(); // Delete the ledger and mantain it in the ledgers list bkc.deleteLedger(l1info.getLedgerId()); // re-open ledger = (ManagedLedgerImpl) factory.open("my_test_ledger"); c1 = ledger.openCursor("c1"); assertEquals(c1.getNumberOfEntries(), 1); assertEquals(c1.hasMoreEntries(), true); assertEquals(ledger.getLedgersInfoAsList().size(), 2); List<Entry> entries = c1.readEntries(10); assertEquals(entries.size(), 1); entries.forEach(e -> e.release()); assertEquals(c1.hasMoreEntries(), false); entries = c1.readEntries(1); assertEquals(entries.size(), 0); entries.forEach(e -> e.release()); } @Test public void addEntryWithOffset() throws Exception { ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("my_test_ledger"); ManagedCursor c1 = ledger.openCursor("c1"); ledger.addEntry("012345678".getBytes(), 2, 3); List<Entry> entries = c1.readEntries(1); assertEquals(entries.get(0).getLength(), 3); Entry entry = entries.get(0); assertEquals(new String(entry.getData()), "234"); entry.release(); } @Test public void totalSizeTest() throws Exception { ManagedLedgerConfig conf = new ManagedLedgerConfig(); conf.setMaxEntriesPerLedger(1); ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("my_test_ledger", conf); ManagedCursor c1 = ledger.openCursor("c1"); ledger.addEntry(new byte[10], 1, 8); assertEquals(ledger.getTotalSize(), 8); PositionImpl p2 = (PositionImpl) ledger.addEntry(new byte[12], 2, 5); assertEquals(ledger.getTotalSize(), 13); c1.markDelete(new PositionImpl(p2.getLedgerId(), -1)); // Wait for background trimming Thread.sleep(400); assertEquals(ledger.getTotalSize(), 5); } @Test public void testMinimumRolloverTime() throws Exception { ManagedLedgerConfig conf = new ManagedLedgerConfig(); conf.setMaxEntriesPerLedger(1); conf.setMinimumRolloverTime(1, TimeUnit.SECONDS); ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("my_test_ledger", conf); ledger.openCursor("c1"); ledger.addEntry("data".getBytes()); ledger.addEntry("data".getBytes()); assertEquals(ledger.getLedgersInfoAsList().size(), 1); Thread.sleep(1000); ledger.addEntry("data".getBytes()); ledger.addEntry("data".getBytes()); assertEquals(ledger.getLedgersInfoAsList().size(), 2); } @Test public void testMaximumRolloverTime() throws Exception { ManagedLedgerConfig conf = new ManagedLedgerConfig(); conf.setMaxEntriesPerLedger(5); conf.setMinimumRolloverTime(1, TimeUnit.SECONDS); conf.setMaximumRolloverTime(1, TimeUnit.SECONDS); ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("my_test_maxtime_ledger", conf); ledger.openCursor("c1"); ledger.addEntry("data".getBytes()); ledger.addEntry("data".getBytes()); assertEquals(ledger.getLedgersInfoAsList().size(), 1); Thread.sleep(2000); ledger.addEntry("data".getBytes()); ledger.addEntry("data".getBytes()); assertEquals(ledger.getLedgersInfoAsList().size(), 2); } @Test public void testRetention() throws Exception { ManagedLedgerFactory factory = new ManagedLedgerFactoryImpl(bkc, bkc.getZkHandle()); ManagedLedgerConfig config = new ManagedLedgerConfig(); config.setRetentionSizeInMB(10); config.setMaxEntriesPerLedger(1); config.setRetentionTime(1, TimeUnit.HOURS); ManagedLedgerImpl ml = (ManagedLedgerImpl) factory.open("retention_test_ledger", config); ManagedCursor c1 = ml.openCursor("c1"); ml.addEntry("iamaverylongmessagethatshouldberetained".getBytes()); c1.skipEntries(1, IndividualDeletedEntries.Exclude); ml.close(); // reopen ml ml = (ManagedLedgerImpl) factory.open("retention_test_ledger", config); c1 = ml.openCursor("c1"); ml.addEntry("shortmessage".getBytes()); c1.skipEntries(1, IndividualDeletedEntries.Exclude); ml.close(); assertTrue(ml.getLedgersInfoAsList().size() > 1); assertTrue(ml.getTotalSize() > "shortmessage".getBytes().length); } @Test(enabled = true) public void testNoRetention() throws Exception { ManagedLedgerFactory factory = new ManagedLedgerFactoryImpl(bkc, bkc.getZkHandle()); ManagedLedgerConfig config = new ManagedLedgerConfig(); config.setRetentionSizeInMB(0); config.setMaxEntriesPerLedger(1); // Default is no-retention ManagedLedgerImpl ml = (ManagedLedgerImpl) factory.open("noretention_test_ledger", config); ManagedCursor c1 = ml.openCursor("c1noretention"); ml.addEntry("iamaverylongmessagethatshouldnotberetained".getBytes()); c1.skipEntries(1, IndividualDeletedEntries.Exclude); ml.close(); // reopen ml ml = (ManagedLedgerImpl) factory.open("noretention_test_ledger", config); c1 = ml.openCursor("c1noretention"); ml.addEntry("shortmessage".getBytes()); c1.skipEntries(1, IndividualDeletedEntries.Exclude); // sleep for trim Thread.sleep(1000); ml.close(); assertTrue(ml.getLedgersInfoAsList().size() <= 1); assertTrue(ml.getTotalSize() <= "shortmessage".getBytes().length); } @Test public void testDeletionAfterRetention() throws Exception { ManagedLedgerFactory factory = new ManagedLedgerFactoryImpl(bkc, bkc.getZkHandle()); ManagedLedgerConfig config = new ManagedLedgerConfig(); config.setRetentionSizeInMB(0); config.setMaxEntriesPerLedger(1); config.setRetentionTime(1, TimeUnit.SECONDS); ManagedLedgerImpl ml = (ManagedLedgerImpl) factory.open("deletion_after_retention_test_ledger", config); ManagedCursor c1 = ml.openCursor("c1noretention"); ml.addEntry("iamaverylongmessagethatshouldnotberetained".getBytes()); c1.skipEntries(1, IndividualDeletedEntries.Exclude); ml.close(); // reopen ml ml = (ManagedLedgerImpl) factory.open("deletion_after_retention_test_ledger", config); c1 = ml.openCursor("c1noretention"); ml.addEntry("shortmessage".getBytes()); c1.skipEntries(1, IndividualDeletedEntries.Exclude); // let retention expire Thread.sleep(1000); ml.close(); // sleep for trim Thread.sleep(100); assertTrue(ml.getLedgersInfoAsList().size() <= 1); assertTrue(ml.getTotalSize() <= "shortmessage".getBytes().length); } @Test public void testTimestampOnWorkingLedger() throws Exception { ManagedLedgerFactory factory = new ManagedLedgerFactoryImpl(bkc, bkc.getZkHandle()); ManagedLedgerConfig conf = new ManagedLedgerConfig(); conf.setMaxEntriesPerLedger(1); conf.setRetentionSizeInMB(10); conf.setRetentionTime(1, TimeUnit.HOURS); ManagedLedgerImpl ml = (ManagedLedgerImpl) factory.open("my_test_ledger", conf); ml.openCursor("c1"); ml.addEntry("msg1".getBytes()); Iterator<LedgerInfo> iter = ml.getLedgersInfoAsList().iterator(); long ts = -1; while (iter.hasNext()) { LedgerInfo i = iter.next(); if (iter.hasNext()) { assertTrue(ts <= i.getTimestamp(), i.toString()); ts = i.getTimestamp(); } else { // the last timestamp can be // 0 if it is still opened // >0 if it is closed after the addEntry see OpAddEntry#addComplete() assertTrue(i.getTimestamp() == 0 || ts <= i.getTimestamp(), i.toString()); } } ml.addEntry("msg02".getBytes()); ml.close(); // Thread.sleep(1000); iter = ml.getLedgersInfoAsList().iterator(); ts = -1; while (iter.hasNext()) { LedgerInfo i = iter.next(); if (iter.hasNext()) { assertTrue(ts <= i.getTimestamp(), i.toString()); ts = i.getTimestamp(); } else { assertTrue(i.getTimestamp() > 0, "well closed LedgerInfo should set a timestamp > 0"); } } } @Test public void testBackwardCompatiblityForMeta() throws Exception { final ManagedLedgerInfo[] storedMLInfo = new ManagedLedgerInfo[3]; final Stat[] versions = new Stat[1]; ManagedLedgerFactory factory = new ManagedLedgerFactoryImpl(bkc, bkc.getZkHandle()); ManagedLedgerConfig conf = new ManagedLedgerConfig(); conf.setMaxEntriesPerLedger(1); conf.setRetentionSizeInMB(10); conf.setRetentionTime(1, TimeUnit.HOURS); ManagedLedger ml = factory.open("backward_test_ledger", conf); ml.openCursor("c1"); ml.addEntry("msg1".getBytes()); ml.addEntry("msg2".getBytes()); ml.close(); MetaStore store = new MetaStoreImplZookeeper(zkc, executor); CountDownLatch l1 = new CountDownLatch(1); // obtain the ledger info store.getManagedLedgerInfo("backward_test_ledger", new MetaStoreCallback<ManagedLedgerInfo>() { @Override public void operationComplete(ManagedLedgerInfo result, Stat version) { storedMLInfo[0] = result; versions[0] = version; l1.countDown(); } @Override public void operationFailed(MetaStoreException e) { fail("on get ManagedLedgerInfo backward_test_ledger"); } }); l1.await(); ManagedLedgerInfo.Builder builder1 = ManagedLedgerInfo.newBuilder(); // simulate test for old ledger with no timestampl for (LedgerInfo info : storedMLInfo[0].getLedgerInfoList()) { LedgerInfo noTimestamp = ManagedLedgerInfo.LedgerInfo.newBuilder().mergeFrom(info).clearTimestamp().build(); assertFalse(noTimestamp.hasTimestamp(), "expected old version info with no timestamp"); builder1.addLedgerInfo(noTimestamp); } storedMLInfo[1] = builder1.build(); // test timestamp on new ledger CountDownLatch l2 = new CountDownLatch(1); store.asyncUpdateLedgerIds("backward_test_ledger", storedMLInfo[1], versions[0], new MetaStoreCallback<Void>() { @Override public void operationComplete(Void result, Stat version) { l2.countDown(); } @Override public void operationFailed(MetaStoreException e) { fail("on asyncUpdateLedgerIds"); } }); // verify that after update ledgers have timestamp ManagedLedgerImpl newVersionLedger = (ManagedLedgerImpl) factory.open("backward_test_ledger", conf); List<LedgerInfo> mlInfo = newVersionLedger.getLedgersInfoAsList(); assertTrue(mlInfo.stream().allMatch(new Predicate<LedgerInfo>() { @Override public boolean test(LedgerInfo ledgerInfo) { return ledgerInfo.hasTimestamp(); } })); } @Test public void testEstimatedBacklogSize() throws Exception { ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("testEstimatedBacklogSize"); ManagedCursor c1 = ledger.openCursor("c1"); ledger.addEntry(new byte[1024]); Position position2 = ledger.addEntry(new byte[1024]); ledger.addEntry(new byte[1024]); ledger.addEntry(new byte[1024]); Position lastPosition = ledger.addEntry(new byte[1024]); long backlog = ledger.getEstimatedBacklogSize(); assertEquals(backlog, 1024 * 5); List<Entry> entries = c1.readEntries(2); entries.forEach(Entry::release); c1.markDelete(position2); backlog = ledger.getEstimatedBacklogSize(); assertEquals(backlog, 1024 * 3); entries = c1.readEntries(3); entries.forEach(Entry::release); c1.markDelete(lastPosition); backlog = ledger.getEstimatedBacklogSize(); assertEquals(backlog, 0); } @Test public void testGetNextValidPosition() throws Exception { ManagedLedgerConfig conf = new ManagedLedgerConfig(); conf.setMaxEntriesPerLedger(1); ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("testGetNextValidPosition", conf); ManagedCursor c1 = ledger.openCursor("c1"); PositionImpl p1 = (PositionImpl) ledger.addEntry("entry1".getBytes()); PositionImpl p2 = (PositionImpl) ledger.addEntry("entry2".getBytes()); PositionImpl p3 = (PositionImpl) ledger.addEntry("entry3".getBytes()); assertEquals(ledger.getNextValidPosition((PositionImpl) c1.getMarkDeletedPosition()), p1); assertEquals(ledger.getNextValidPosition(p1), p2); assertEquals(ledger.getNextValidPosition(p3), PositionImpl.get(p3.getLedgerId(), p3.getEntryId() + 1)); } /** * Validations: * * 1. openCursor : activates cursor 2. EntryCache keeps entries: till entry will be read by all active cursors a. * active cursor1 reads entry b. EntryCache keeps entry till cursor2 reads c. active cursor2 reads entry d. * EntryCache deletes all read entries by cursor1 and cursor2 3. EntryCache discard entries: deactivate slower * cursor a. active cursor1 read all entries b. EntryCache keeps entry till cursor2 reads c. deactivate cursor2 d. * EntryCache deletes all read entries by cursor1 * * @throws Exception */ @Test public void testActiveDeactiveCursorWithDiscardEntriesFromCache() throws Exception { ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("cache_eviction_ledger"); // Open Cursor also adds cursor into activeCursor-container ManagedCursor cursor1 = ledger.openCursor("c1"); ManagedCursor cursor2 = ledger.openCursor("c2"); Set<ManagedCursor> activeCursors = Sets.newHashSet(); activeCursors.add(cursor1); activeCursors.add(cursor2); Field cacheField = ManagedLedgerImpl.class.getDeclaredField("entryCache"); cacheField.setAccessible(true); EntryCacheImpl entryCache = (EntryCacheImpl) cacheField.get(ledger); Iterator<ManagedCursor> activeCursor = ledger.getActiveCursors().iterator(); // (1) validate cursors are part of activeCursorContainer activeCursors.remove(activeCursor.next()); activeCursors.remove(activeCursor.next()); assertTrue(activeCursors.isEmpty()); assertFalse(activeCursor.hasNext()); final int totalInsertedEntries = 50; for (int i = 0; i < totalInsertedEntries; i++) { String content = "entry"; // 5 bytes ledger.addEntry(content.getBytes()); } // (2) Validate: as ledger has active cursors: all entries have been cached assertEquals((5 * totalInsertedEntries), entryCache.getSize()); // read 20 entries final int readEntries = 20; List<Entry> entries1 = cursor1.readEntries(readEntries); // Acknowledge only on last entry cursor1.markDelete(entries1.get(entries1.size() - 1).getPosition()); for (Entry entry : entries1) { log.info("Read entry. Position={} Content='{}'", entry.getPosition(), new String(entry.getData())); entry.release(); } // read after a second: as RateLimiter limits triggering of removing cache Thread.sleep(1000); List<Entry> entries2 = cursor2.readEntries(readEntries); // Acknowledge only on last entry cursor2.markDelete((entries2.get(entries2.size() - 1)).getPosition()); for (Entry entry : entries2) { log.info("Read entry. Position={} Content='{}'", entry.getPosition(), new String(entry.getData())); entry.release(); } // (3) Validate: cache should remove all entries read by both active cursors log.info("expected, found : {}, {}", (5 * (totalInsertedEntries - readEntries)), entryCache.getSize()); assertEquals((5 * (totalInsertedEntries - readEntries)), entryCache.getSize()); final int remainingEntries = totalInsertedEntries - readEntries; entries1 = cursor1.readEntries(remainingEntries); // Acknowledge only on last entry cursor1.markDelete(entries1.get(entries1.size() - 1).getPosition()); for (Entry entry : entries1) { log.info("Read entry. Position={} Content='{}'", entry.getPosition(), new String(entry.getData())); entry.release(); } // (4) Validate: cursor2 is active cursor and has not read these entries yet: so, cache should not remove these // entries assertEquals((5 * (remainingEntries)), entryCache.getSize()); ledger.deactivateCursor(cursor2); // (5) Validate: cursor2 is not active cursor now: cache should have removed all entries read by active cursor1 assertEquals(0, entryCache.getSize()); log.info("Finished reading entries"); ledger.close(); } @Test public void testActiveDeactiveCursor() throws Exception { ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("cache_eviction_ledger"); Field cacheField = ManagedLedgerImpl.class.getDeclaredField("entryCache"); cacheField.setAccessible(true); EntryCacheImpl entryCache = (EntryCacheImpl) cacheField.get(ledger); final int totalInsertedEntries = 20; for (int i = 0; i < totalInsertedEntries; i++) { String content = "entry"; // 5 bytes ledger.addEntry(content.getBytes()); } // (1) Validate: cache not stores entries as no active cursor assertEquals(0, entryCache.getSize()); // Open Cursor also adds cursor into activeCursor-container ManagedCursor cursor1 = ledger.openCursor("c1"); ManagedCursor cursor2 = ledger.openCursor("c2"); ledger.deactivateCursor(cursor2); for (int i = 0; i < totalInsertedEntries; i++) { String content = "entry"; // 5 bytes ledger.addEntry(content.getBytes()); } // (2) Validate: cache stores entries as active cursor has not read message assertEquals((5 * totalInsertedEntries), entryCache.getSize()); // read 20 entries List<Entry> entries1 = cursor1.readEntries(totalInsertedEntries); for (Entry entry : entries1) { log.info("Read entry. Position={} Content='{}'", entry.getPosition(), new String(entry.getData())); entry.release(); } // (3) Validate: cache discards all entries as read by active cursor assertEquals(0, entryCache.getSize()); ledger.close(); } @Test public void testCursorRecoveryForEmptyLedgers() throws Exception { ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("testCursorRecoveryForEmptyLedgers"); ManagedCursor c1 = ledger.openCursor("c1"); assertEquals(ledger.getLedgersInfoAsList().size(), 1); assertEquals(c1.getMarkDeletedPosition(), ledger.lastConfirmedEntry); c1.close(); ledger.close(); ledger = (ManagedLedgerImpl) factory.open("testCursorRecoveryForEmptyLedgers"); c1 = ledger.openCursor("c1"); assertEquals(ledger.getLedgersInfoAsList().size(), 1); assertEquals(c1.getMarkDeletedPosition(), ledger.lastConfirmedEntry); } @Test public void testBacklogCursor() throws Exception { ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("cache_backlog_ledger"); final long maxMessageCacheRetentionTimeMillis = 100; Field field = ManagedLedgerImpl.class.getDeclaredField("maxMessageCacheRetentionTimeMillis"); field.setAccessible(true); Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); field.set(ledger, maxMessageCacheRetentionTimeMillis); Field backlogThresholdField = ManagedLedgerImpl.class.getDeclaredField("maxActiveCursorBacklogEntries"); backlogThresholdField.setAccessible(true); final long maxActiveCursorBacklogEntries = (long) backlogThresholdField.get(ledger); // Open Cursor also adds cursor into activeCursor-container ManagedCursor cursor1 = ledger.openCursor("c1"); ManagedCursor cursor2 = ledger.openCursor("c2"); final int totalBacklogSizeEntries = (int) maxActiveCursorBacklogEntries; CountDownLatch latch = new CountDownLatch(totalBacklogSizeEntries); for (int i = 0; i < totalBacklogSizeEntries + 1; i++) { String content = "entry"; // 5 bytes ByteBuf entry = getMessageWithMetadata(content.getBytes()); ledger.asyncAddEntry(entry, new AddEntryCallback() { @Override public void addComplete(Position position, Object ctx) { latch.countDown(); entry.release(); } @Override public void addFailed(ManagedLedgerException exception, Object ctx) { latch.countDown(); entry.release(); } }, null); } latch.await(); // Verify: cursors are active as :haven't started deactivateBacklogCursor scan assertTrue(cursor1.isActive()); assertTrue(cursor2.isActive()); // it allows message to be older enough to be considered in backlog Thread.sleep(maxMessageCacheRetentionTimeMillis * 2); // deactivate backlog cursors ledger.checkBackloggedCursors(); Thread.sleep(100); // both cursors have to be inactive assertFalse(cursor1.isActive()); assertFalse(cursor2.isActive()); // read entries so, cursor1 reaches maxBacklog threshold again to be active again List<Entry> entries1 = cursor1.readEntries(50); for (Entry entry : entries1) { log.info("Read entry. Position={} Content='{}'", entry.getPosition(), new String(entry.getData())); entry.release(); } // activate cursors which caught up maxbacklog threshold ledger.checkBackloggedCursors(); // verify: cursor1 has consumed messages so, under maxBacklog threshold => active assertTrue(cursor1.isActive()); // verify: cursor2 has not consumed messages so, above maxBacklog threshold => inactive assertFalse(cursor2.isActive()); ledger.close(); } @Test public void testConcurrentOpenCursor() throws Exception { ManagedLedgerImpl ledger = (ManagedLedgerImpl) factory.open("testConcurrentOpenCursor"); final AtomicReference<ManagedCursor> cursor1 = new AtomicReference<>(null); final AtomicReference<ManagedCursor> cursor2 = new AtomicReference<>(null); final CyclicBarrier barrier = new CyclicBarrier(2); final CountDownLatch latch = new CountDownLatch(2); cachedExecutor.execute(() -> { try { barrier.await(); } catch (Exception e) { } ledger.asyncOpenCursor("c1", new OpenCursorCallback() { @Override public void openCursorFailed(ManagedLedgerException exception, Object ctx) { latch.countDown(); } @Override public void openCursorComplete(ManagedCursor cursor, Object ctx) { cursor1.set(cursor); latch.countDown(); } }, null); }); cachedExecutor.execute(() -> { try { barrier.await(); } catch (Exception e) { } ledger.asyncOpenCursor("c1", new OpenCursorCallback() { @Override public void openCursorFailed(ManagedLedgerException exception, Object ctx) { latch.countDown(); } @Override public void openCursorComplete(ManagedCursor cursor, Object ctx) { cursor2.set(cursor); latch.countDown(); } }, null); }); latch.await(); assertNotNull(cursor1.get()); assertNotNull(cursor2.get()); assertEquals(cursor1.get(), cursor2.get()); ledger.close(); } public ByteBuf getMessageWithMetadata(byte[] data) throws IOException { MessageMetadata messageData = MessageMetadata.newBuilder().setPublishTime(System.currentTimeMillis()) .setProducerName("prod-name").setSequenceId(0).build(); ByteBuf payload = Unpooled.wrappedBuffer(data, 0, data.length); int msgMetadataSize = messageData.getSerializedSize(); int headersSize = 4 + msgMetadataSize; ByteBuf headers = PooledByteBufAllocator.DEFAULT.buffer(headersSize, headersSize); ByteBufCodedOutputStream outStream = ByteBufCodedOutputStream.get(headers); headers.writeInt(msgMetadataSize); messageData.writeTo(outStream); outStream.recycle(); return DoubleByteBuf.get(headers, payload); } }