org.apache.bookkeeper.client.MetadataUpdateLoopTest.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.bookkeeper.client.MetadataUpdateLoopTest.java

Source

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.client;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.util.List;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;

import java.util.stream.Collectors;
import java.util.stream.IntStream;

import lombok.AllArgsConstructor;
import lombok.Data;

import org.apache.bookkeeper.client.api.DigestType;
import org.apache.bookkeeper.client.api.LedgerMetadata;
import org.apache.bookkeeper.meta.LedgerManager;
import org.apache.bookkeeper.meta.MockLedgerManager;
import org.apache.bookkeeper.net.BookieSocketAddress;
import org.apache.bookkeeper.versioning.Version;
import org.apache.bookkeeper.versioning.Versioned;
import org.apache.commons.lang3.tuple.Triple;

import org.junit.Assert;
import org.junit.Test;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Test metadata update loop.
 */
public class MetadataUpdateLoopTest {
    static final Logger LOG = LoggerFactory.getLogger(MetadataUpdateLoopTest.class);

    /**
     * Test that we can update the metadata using the update loop.
     */
    @Test
    public void testBasicUpdate() throws Exception {
        try (LedgerManager lm = new MockLedgerManager()) {
            LedgerMetadata initMeta = LedgerMetadataBuilder.create().withEnsembleSize(5)
                    .withDigestType(DigestType.CRC32C).withPassword(new byte[0])
                    .newEnsembleEntry(0L, Lists.newArrayList(new BookieSocketAddress("0.0.0.0:3181"),
                            new BookieSocketAddress("0.0.0.1:3181"), new BookieSocketAddress("0.0.0.2:3181"),
                            new BookieSocketAddress("0.0.0.3:3181"), new BookieSocketAddress("0.0.0.4:3181")))
                    .build();
            long ledgerId = 1234L;
            Versioned<LedgerMetadata> writtenMetadata = lm.createLedgerMetadata(ledgerId, initMeta).get();

            AtomicReference<Versioned<LedgerMetadata>> reference = new AtomicReference<>(writtenMetadata);

            BookieSocketAddress newAddress = new BookieSocketAddress("0.0.0.5:3181");
            MetadataUpdateLoop loop = new MetadataUpdateLoop(lm, ledgerId, reference::get,
                    (currentMetadata) -> true, (currentMetadata) -> {
                        List<BookieSocketAddress> ensemble = Lists.newArrayList(currentMetadata.getEnsembleAt(0L));
                        ensemble.set(0, newAddress);
                        return LedgerMetadataBuilder.from(currentMetadata).replaceEnsembleEntry(0L, ensemble)
                                .build();
                    }, reference::compareAndSet);
            loop.run().get();

            Assert.assertNotEquals(reference.get(), writtenMetadata);
            Assert.assertEquals(reference.get().getValue().getEnsembleAt(0L).get(0), newAddress);
        }
    }

    /**
     * Test that when 2 update loops conflict when making diffent updates to the metadata,
     * both will eventually succeed, and both updates will be reflected in the final metadata.
     */
    @Test
    public void testConflictOnWrite() throws Exception {
        try (BlockableMockLedgerManager lm = spy(new BlockableMockLedgerManager())) {
            lm.blockWrites();

            long ledgerId = 1234L;
            BookieSocketAddress b0 = new BookieSocketAddress("0.0.0.0:3181");
            BookieSocketAddress b1 = new BookieSocketAddress("0.0.0.1:3181");
            BookieSocketAddress b2 = new BookieSocketAddress("0.0.0.2:3181");
            BookieSocketAddress b3 = new BookieSocketAddress("0.0.0.3:3181");

            LedgerMetadata initMeta = LedgerMetadataBuilder.create().withEnsembleSize(2)
                    .withDigestType(DigestType.CRC32C).withPassword(new byte[0]).withWriteQuorumSize(2)
                    .newEnsembleEntry(0L, Lists.newArrayList(b0, b1)).build();
            Versioned<LedgerMetadata> writtenMetadata = lm.createLedgerMetadata(ledgerId, initMeta).get();

            AtomicReference<Versioned<LedgerMetadata>> reference1 = new AtomicReference<>(writtenMetadata);
            CompletableFuture<Versioned<LedgerMetadata>> loop1 = new MetadataUpdateLoop(lm, ledgerId,
                    reference1::get, (currentMetadata) -> currentMetadata.getEnsembleAt(0L).contains(b0),
                    (currentMetadata) -> {
                        List<BookieSocketAddress> ensemble = Lists.newArrayList(currentMetadata.getEnsembleAt(0L));
                        ensemble.set(0, b2);
                        return LedgerMetadataBuilder.from(currentMetadata).replaceEnsembleEntry(0L, ensemble)
                                .build();
                    }, reference1::compareAndSet).run();

            AtomicReference<Versioned<LedgerMetadata>> reference2 = new AtomicReference<>(writtenMetadata);
            CompletableFuture<Versioned<LedgerMetadata>> loop2 = new MetadataUpdateLoop(lm, ledgerId,
                    reference2::get, (currentMetadata) -> currentMetadata.getEnsembleAt(0L).contains(b1),
                    (currentMetadata) -> {
                        List<BookieSocketAddress> ensemble = Lists.newArrayList(currentMetadata.getEnsembleAt(0L));
                        ensemble.set(1, b3);
                        return LedgerMetadataBuilder.from(currentMetadata).replaceEnsembleEntry(0L, ensemble)
                                .build();
                    }, reference2::compareAndSet).run();

            lm.releaseWrites();

            Versioned<LedgerMetadata> l1meta = loop1.get();
            Versioned<LedgerMetadata> l2meta = loop2.get();

            Assert.assertEquals(l1meta, reference1.get());
            Assert.assertEquals(l2meta, reference2.get());

            Assert.assertEquals(l1meta.getVersion().compare(l2meta.getVersion()), Version.Occurred.BEFORE);

            Assert.assertEquals(l1meta.getValue().getEnsembleAt(0L).get(0), b2);
            Assert.assertEquals(l1meta.getValue().getEnsembleAt(0L).get(1), b1);

            Assert.assertEquals(l2meta.getValue().getEnsembleAt(0L).get(0), b2);
            Assert.assertEquals(l2meta.getValue().getEnsembleAt(0L).get(1), b3);

            verify(lm, times(3)).writeLedgerMetadata(anyLong(), any(), any());
        }
    }

    /**
     * Test that when 2 updates loops try to make the same modification, and they
     * conflict on the write to the store, the one that receives the conflict won't
     * try to write again, as the value is now correct.
     */
    @Test
    public void testConflictOnWriteBothWritingSame() throws Exception {
        try (BlockableMockLedgerManager lm = spy(new BlockableMockLedgerManager())) {
            lm.blockWrites();

            long ledgerId = 1234L;
            BookieSocketAddress b0 = new BookieSocketAddress("0.0.0.0:3181");
            BookieSocketAddress b1 = new BookieSocketAddress("0.0.0.1:3181");
            BookieSocketAddress b2 = new BookieSocketAddress("0.0.0.2:3181");

            LedgerMetadata initMeta = LedgerMetadataBuilder.create().withEnsembleSize(2)
                    .withDigestType(DigestType.CRC32C).withPassword(new byte[0]).withWriteQuorumSize(2)
                    .newEnsembleEntry(0L, Lists.newArrayList(b0, b1)).build();
            Versioned<LedgerMetadata> writtenMetadata = lm.createLedgerMetadata(ledgerId, initMeta).get();
            AtomicReference<Versioned<LedgerMetadata>> reference = new AtomicReference<>(writtenMetadata);

            CompletableFuture<Versioned<LedgerMetadata>> loop1 = new MetadataUpdateLoop(lm, ledgerId,
                    reference::get, (currentMetadata) -> currentMetadata.getEnsembleAt(0L).contains(b0),
                    (currentMetadata) -> {
                        List<BookieSocketAddress> ensemble = Lists.newArrayList(currentMetadata.getEnsembleAt(0L));
                        ensemble.set(0, b2);
                        return LedgerMetadataBuilder.from(currentMetadata).replaceEnsembleEntry(0L, ensemble)
                                .build();
                    }, reference::compareAndSet).run();
            CompletableFuture<Versioned<LedgerMetadata>> loop2 = new MetadataUpdateLoop(lm, ledgerId,
                    reference::get, (currentMetadata) -> currentMetadata.getEnsembleAt(0L).contains(b0),
                    (currentMetadata) -> {
                        List<BookieSocketAddress> ensemble = Lists.newArrayList(currentMetadata.getEnsembleAt(0L));
                        ensemble.set(0, b2);
                        return LedgerMetadataBuilder.from(currentMetadata).replaceEnsembleEntry(0L, ensemble)
                                .build();
                    }, reference::compareAndSet).run();

            lm.releaseWrites();

            Assert.assertEquals(loop1.get(), loop2.get());
            Assert.assertEquals(loop1.get(), reference.get());

            Assert.assertEquals(reference.get().getValue().getEnsembleAt(0L).get(0), b2);
            Assert.assertEquals(reference.get().getValue().getEnsembleAt(0L).get(1), b1);

            verify(lm, times(2)).writeLedgerMetadata(anyLong(), any(), any());
        }
    }

    /**
     * Test that when 2 update loops both manage to write, but conflict on
     * updating the local value.
     */
    @Test
    public void testConflictOnLocalUpdate() throws Exception {
        try (DeferCallbacksMockLedgerManager lm = spy(new DeferCallbacksMockLedgerManager(1))) {
            long ledgerId = 1234L;
            BookieSocketAddress b0 = new BookieSocketAddress("0.0.0.0:3181");
            BookieSocketAddress b1 = new BookieSocketAddress("0.0.0.1:3181");
            BookieSocketAddress b2 = new BookieSocketAddress("0.0.0.2:3181");
            BookieSocketAddress b3 = new BookieSocketAddress("0.0.0.3:3181");

            LedgerMetadata initMeta = LedgerMetadataBuilder.create().withEnsembleSize(2)
                    .withDigestType(DigestType.CRC32C).withPassword(new byte[0]).withWriteQuorumSize(2)
                    .newEnsembleEntry(0L, Lists.newArrayList(b0, b1)).build();
            Versioned<LedgerMetadata> writtenMetadata = lm.createLedgerMetadata(ledgerId, initMeta).get();
            AtomicReference<Versioned<LedgerMetadata>> reference = new AtomicReference<>(writtenMetadata);

            CompletableFuture<Versioned<LedgerMetadata>> loop1 = new MetadataUpdateLoop(lm, ledgerId,
                    reference::get, (currentMetadata) -> currentMetadata.getEnsembleAt(0L).contains(b0),
                    (currentMetadata) -> {
                        List<BookieSocketAddress> ensemble = Lists.newArrayList(currentMetadata.getEnsembleAt(0L));
                        ensemble.set(0, b2);
                        return LedgerMetadataBuilder.from(currentMetadata).replaceEnsembleEntry(0L, ensemble)
                                .build();
                    }, reference::compareAndSet).run();

            lm.waitForWriteCount(1);
            CompletableFuture<Versioned<LedgerMetadata>> loop2 = new MetadataUpdateLoop(lm, ledgerId,
                    reference::get, (currentMetadata) -> currentMetadata.getEnsembleAt(0L).contains(b1),
                    (currentMetadata) -> {
                        List<BookieSocketAddress> ensemble = Lists.newArrayList(currentMetadata.getEnsembleAt(0L));
                        ensemble.set(1, b3);
                        return LedgerMetadataBuilder.from(currentMetadata).replaceEnsembleEntry(0L, ensemble)
                                .build();
                    }, reference::compareAndSet).run();
            Assert.assertEquals(loop2.get(), reference.get());

            lm.runDeferred();

            Assert.assertEquals(loop1.get(), reference.get());

            Assert.assertEquals(reference.get().getValue().getEnsembleAt(0L).get(0), b2);
            Assert.assertEquals(reference.get().getValue().getEnsembleAt(0L).get(1), b3);

            verify(lm, times(3)).writeLedgerMetadata(anyLong(), any(), any());
        }
    }

    private static BookieSocketAddress address(String s) {
        try {
            return new BookieSocketAddress(s);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Hammer test. Kick off a lot of metadata updates concurrently with a ledger manager
     * that runs callbacks on random threads, and validate all updates complete eventually,
     * and that the final metadata reflects all the updates.
     */
    @Test
    public void testHammer() throws Exception {
        try (NonDeterministicMockLedgerManager lm = new NonDeterministicMockLedgerManager()) {
            long ledgerId = 1234L;

            int ensembleSize = 100;
            List<BookieSocketAddress> initialEnsemble = IntStream.range(0, ensembleSize)
                    .mapToObj((i) -> address(String.format("0.0.0.%d:3181", i))).collect(Collectors.toList());

            LedgerMetadata initMeta = LedgerMetadataBuilder.create().withEnsembleSize(ensembleSize)
                    .withDigestType(DigestType.CRC32C).withPassword(new byte[0])
                    .newEnsembleEntry(0L, initialEnsemble).build();
            Versioned<LedgerMetadata> writtenMetadata = lm.createLedgerMetadata(ledgerId, initMeta).get();

            AtomicReference<Versioned<LedgerMetadata>> reference = new AtomicReference<>(writtenMetadata);

            List<BookieSocketAddress> replacementBookies = IntStream.range(0, ensembleSize)
                    .mapToObj((i) -> address(String.format("0.0.%d.1:3181", i))).collect(Collectors.toList());

            List<CompletableFuture<Versioned<LedgerMetadata>>> loops = IntStream.range(0, ensembleSize)
                    .mapToObj((i) -> new MetadataUpdateLoop(lm, ledgerId, reference::get,
                            (currentMetadata) -> currentMetadata.getEnsembleAt(0L).contains(initialEnsemble.get(i)),
                            (currentMetadata) -> {
                                List<BookieSocketAddress> ensemble = Lists
                                        .newArrayList(currentMetadata.getEnsembleAt(0L));
                                ensemble.set(i, replacementBookies.get(i));
                                return LedgerMetadataBuilder.from(currentMetadata)
                                        .replaceEnsembleEntry(0L, ensemble).build();
                            }, reference::compareAndSet).run())
                    .collect(Collectors.toList());

            loops.forEach((l) -> l.join());

            Assert.assertEquals(reference.get().getValue().getEnsembleAt(0L), replacementBookies);
        }
    }

    /**
     * Test that if we have two conflicting updates, only one of the loops will complete.
     * The other will throw an exception.
     */
    @Test
    public void testNewestValueCannotBeUsedAfterReadBack() throws Exception {
        try (BlockableMockLedgerManager lm = spy(new BlockableMockLedgerManager())) {
            lm.blockWrites();

            long ledgerId = 1234L;
            BookieSocketAddress b0 = new BookieSocketAddress("0.0.0.0:3181");
            BookieSocketAddress b1 = new BookieSocketAddress("0.0.0.1:3181");

            LedgerMetadata initMeta = LedgerMetadataBuilder.create().withEnsembleSize(1)
                    .withDigestType(DigestType.CRC32C).withPassword(new byte[0]).withWriteQuorumSize(1)
                    .withAckQuorumSize(1).newEnsembleEntry(0L, Lists.newArrayList(b0)).build();
            Versioned<LedgerMetadata> writtenMetadata = lm.createLedgerMetadata(ledgerId, initMeta).get();

            AtomicReference<Versioned<LedgerMetadata>> reference = new AtomicReference<>(writtenMetadata);
            CompletableFuture<Versioned<LedgerMetadata>> loop1 = new MetadataUpdateLoop(lm, ledgerId,
                    reference::get, (currentMetadata) -> !currentMetadata.isClosed(), (currentMetadata) -> {
                        return LedgerMetadataBuilder.from(currentMetadata).withClosedState().withLastEntryId(10L)
                                .withLength(100L).build();
                    }, reference::compareAndSet).run();
            CompletableFuture<Versioned<LedgerMetadata>> loop2 = new MetadataUpdateLoop(lm, ledgerId,
                    reference::get, (currentMetadata) -> {
                        if (currentMetadata.isClosed()) {
                            throw new BKException.BKLedgerClosedException();
                        } else {
                            return currentMetadata.getEnsembleAt(0L).contains(b0);
                        }
                    }, (currentMetadata) -> {
                        List<BookieSocketAddress> ensemble = Lists.newArrayList(currentMetadata.getEnsembleAt(0L));
                        ensemble.set(0, b1);
                        return LedgerMetadataBuilder.from(currentMetadata).replaceEnsembleEntry(0L, ensemble)
                                .build();
                    }, reference::compareAndSet).run();
            lm.releaseWrites();

            Versioned<LedgerMetadata> l1meta = loop1.get();
            try {
                loop2.get();
                Assert.fail("Update loop should have failed");
            } catch (ExecutionException ee) {
                Assert.assertEquals(ee.getCause().getClass(), BKException.BKLedgerClosedException.class);
            }
            Assert.assertEquals(l1meta, reference.get());
            Assert.assertEquals(l1meta.getValue().getEnsembleAt(0L).get(0), b0);
            Assert.assertTrue(l1meta.getValue().isClosed());

            verify(lm, times(2)).writeLedgerMetadata(anyLong(), any(), any());
        }
    }

    static class NonDeterministicMockLedgerManager extends MockLedgerManager {
        final ExecutorService cbExecutor = Executors
                .newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat("non-deter-%d").build());

        @Override
        public void executeCallback(Runnable r) {
            cbExecutor.execute(r);
        }

        @Override
        public void close() {
            cbExecutor.shutdownNow();
            super.close();
        }
    }

    static class DeferCallbacksMockLedgerManager extends MockLedgerManager {
        int writeCount = 0;
        final int numToDefer;
        List<Triple<CompletableFuture<Versioned<LedgerMetadata>>, Versioned<LedgerMetadata>, Throwable>> deferred = Lists
                .newArrayList();

        DeferCallbacksMockLedgerManager(int numToDefer) {
            this.numToDefer = numToDefer;
        }

        synchronized void runDeferred() {
            deferred.forEach((d) -> {
                Throwable t = d.getRight();
                if (t != null) {
                    d.getLeft().completeExceptionally(t);
                } else {
                    d.getLeft().complete(d.getMiddle());
                }
            });
        }

        synchronized void waitForWriteCount(int count) throws Exception {
            while (writeCount < count) {
                wait();
            }
        }

        @Override
        public synchronized CompletableFuture<Versioned<LedgerMetadata>> writeLedgerMetadata(long ledgerId,
                LedgerMetadata metadata, Version currentVersion) {
            CompletableFuture<Versioned<LedgerMetadata>> promise = new CompletableFuture<>();
            super.writeLedgerMetadata(ledgerId, metadata, currentVersion).whenComplete((written, exception) -> {
                synchronized (DeferCallbacksMockLedgerManager.this) {
                    if (writeCount++ < numToDefer) {
                        LOG.info("Added to deferals");
                        deferred.add(Triple.of(promise, written, exception));
                    } else {
                        LOG.info("Completing {}", numToDefer);
                        if (exception != null) {
                            promise.completeExceptionally(exception);
                        } else {
                            promise.complete(written);
                        }
                    }
                    DeferCallbacksMockLedgerManager.this.notifyAll();
                }
            });
            return promise;
        };
    }

    @Data
    @AllArgsConstructor
    static class DeferredUpdate {
        final CompletableFuture<Versioned<LedgerMetadata>> promise;
        final long ledgerId;
        final LedgerMetadata metadata;
        final Version currentVersion;
    }

    static class BlockableMockLedgerManager extends MockLedgerManager {
        boolean blocking = false;
        List<DeferredUpdate> reqs = Lists.newArrayList();

        synchronized void blockWrites() {
            blocking = true;
        }

        synchronized void releaseWrites() {
            blocking = false;
            reqs.forEach((r) -> {
                super.writeLedgerMetadata(r.getLedgerId(), r.getMetadata(), r.getCurrentVersion())
                        .whenComplete((written, exception) -> {
                            if (exception != null) {
                                r.getPromise().completeExceptionally(exception);
                            } else {
                                r.getPromise().complete(written);
                            }
                        });
            });
        }

        @Override
        public synchronized CompletableFuture<Versioned<LedgerMetadata>> writeLedgerMetadata(long ledgerId,
                LedgerMetadata metadata, Version currentVersion) {
            if (blocking) {
                CompletableFuture<Versioned<LedgerMetadata>> promise = new CompletableFuture<>();
                reqs.add(new DeferredUpdate(promise, ledgerId, metadata, currentVersion));
                return promise;
            } else {
                return super.writeLedgerMetadata(ledgerId, metadata, currentVersion);
            }
        };
    }
}