org.midonet.cluster.cache.StateCacheTest.java Source code

Java tutorial

Introduction

Here is the source code for org.midonet.cluster.cache.StateCacheTest.java

Source

/*
 * Copyright 2017 Midokura SARL
 *
 * 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.midonet.cluster.cache;

import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import scala.Function0;
import scala.concurrent.duration.Duration;
import scala.runtime.AbstractFunction0;
import scala.runtime.BoxedUnit;

import com.codahale.metrics.MetricRegistry;
import com.typesafe.config.ConfigFactory;

import org.apache.commons.lang.ArrayUtils;
import org.apache.zookeeper.CreateMode;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import rx.Observable;
import rx.Subscription;

import org.midonet.cluster.data.storage.KeyType;
import org.midonet.cluster.data.storage.ZookeeperObjectMapper;
import org.midonet.cluster.data.storage.metrics.StorageMetrics;
import org.midonet.cluster.models.Commons;
import org.midonet.cluster.models.State;
import org.midonet.cluster.models.Topology;
import org.midonet.cluster.services.MidonetBackend$;
import org.midonet.cluster.storage.MidonetBackendConfig;
import org.midonet.cluster.util.UUIDUtil$;
import org.midonet.cluster.ZooKeeperTest;
import org.midonet.util.reactivex.TestAwaitableObserver;

public class StateCacheTest extends ZooKeeperTest {

    private static final Function0<BoxedUnit> NO_SETUP = new AbstractFunction0<BoxedUnit>() {
        @Override
        public BoxedUnit apply() {
            return BoxedUnit.UNIT;
        }
    };

    private static final Duration TIMEOUT = Duration.apply(1, TimeUnit.SECONDS);
    private static final String NAMESPACE = new UUID(0L, 0L).toString();

    private ZookeeperObjectMapper storage;
    private MidonetBackendConfig config;
    private MetricRegistry metricRegistry;
    private ZoomPaths paths;

    @BeforeClass
    public static void beforeAll() throws Exception {
        ZooKeeperTest.beforeAll(StateCacheTest.class);
    }

    @AfterClass
    public static void afterAll() throws Exception {
        ZooKeeperTest.afterAll(StateCacheTest.class);
    }

    @Before
    @Override
    public void before() throws Exception {
        super.before();

        config = new MidonetBackendConfig(ConfigFactory.parseString("zookeeper.root_key : " + ROOT), false, false,
                false);
        paths = new ZoomPaths(config);
        metricRegistry = new MetricRegistry();
        StorageMetrics metrics = new StorageMetrics(metricRegistry);

        storage = new ZookeeperObjectMapper(config, NAMESPACE, curator, curator, null, null, metrics);

        MidonetBackend$.MODULE$.setupBindings(storage, storage, NO_SETUP);
    }

    @After
    @Override
    public void after() {
        super.after();
    }

    private String getZoomStatePath(UUID owner, Class<?> clazz, UUID id, String key) {
        return String.format("%s/zoom/0/state/%s/%s/%s/%s", ROOT, owner.toString(), clazz.getSimpleName(),
                id.toString(), key);
    }

    private String getZoomStateMultiValuePath(UUID owner, Class<?> clazz, UUID id, String key, String value) {
        return getZoomStatePath(owner, clazz, id, key) + String.format("/%s", value);
    }

    private void addSingle(UUID owner, Class<?> clazz, UUID id, String key, String value) throws Exception {
        String path = getZoomStatePath(owner, clazz, id, key);
        if (curator.checkExists().forPath(path) != null) {
            curator.setData().forPath(path, value.getBytes());
        } else {
            curator.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path,
                    value.getBytes());
        }
    }

    private void removeSingle(UUID owner, Class<?> clazz, UUID id, String key) throws Exception {
        String path = getZoomStatePath(owner, clazz, id, key);
        curator.delete().forPath(path);
    }

    private void addMulti(UUID owner, Class<?> clazz, UUID id, String key, String value) throws Exception {
        String path = getZoomStateMultiValuePath(owner, clazz, id, key, value);

        if (curator.checkExists().forPath(path) == null) {
            curator.create().creatingParentContainersIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path);
        }

    }

    private void addMulti(UUID owner, Class<?> clazz, UUID id, String key, String[] values) throws Exception {
        for (String value : values) {
            addMulti(owner, clazz, id, key, value);
        }
    }

    private void removeMulti(UUID owner, Class<?> clazz, UUID id, String key, String value) throws Exception {
        String path = getZoomStateMultiValuePath(owner, clazz, id, key, value);
        curator.delete().forPath(path);
    }

    private void removeMulti(UUID owner, Class<?> clazz, UUID id, String key, String[] values) throws Exception {
        for (String value : values) {
            removeMulti(owner, clazz, id, key, value);
        }
    }

    private StateNotification.Update updateFor(StateNotification.Snapshot snapshot, String key) {
        for (StateNotification.Update update : snapshot)
            if (update.key().equals(key))
                return update;
        return null;
    }

    private StateNotification.Update updateFor(List<StateNotification> notifications, int begin, String key) {
        for (int index = begin; index < notifications.size(); index++) {
            if (notifications.get(index) instanceof StateNotification.Snapshot) {
                StateNotification.Snapshot snapshot = (StateNotification.Snapshot) notifications.get(index);
                StateNotification.Update update = updateFor(snapshot, key);
                if (update != null)
                    return update;
            } else {
                StateNotification.Update update = (StateNotification.Update) notifications.get(index);
                if (update.key().equals(key))
                    return update;
            }
        }
        return null;
    }

    private void assertUpdateEquals(StateNotification.Update update, UUID owner, Class<?> clazz, UUID id,
            String key, KeyType.KeyTypeVal type) {
        Assert.assertEquals(owner, update.owner());
        Assert.assertEquals(clazz, update.objectClass());
        Assert.assertEquals(id, update.id());
        Assert.assertEquals(key, update.key());
        Assert.assertEquals(type, update.type());
        if (type.isSingle())
            Assert.assertEquals(0, update.singleData().length);
        else
            Assert.assertEquals(0, update.multiData().length);
    }

    private void assertUpdateEquals(StateNotification.Update update, UUID owner, Class<?> clazz, UUID id,
            String key, KeyType.KeyTypeVal type, byte[] singleValue, String[] multiValue) {
        Assert.assertEquals(owner, update.owner());
        Assert.assertEquals(clazz, update.objectClass());
        Assert.assertEquals(id, update.id());
        Assert.assertEquals(key, update.key());
        Assert.assertEquals(type, update.type());
        if (type.isSingle())
            Assert.assertArrayEquals(singleValue, update.singleData());
        else {
            String[] sortedMultiValue = (String[]) ArrayUtils.clone(multiValue);
            Arrays.sort(sortedMultiValue);

            String[] sortedMultiData = (String[]) ArrayUtils.clone(update.multiData());
            Arrays.sort(sortedMultiData);

            Assert.assertArrayEquals(sortedMultiValue, sortedMultiValue);
        }
    }

    @Test
    public void testCacheLifecycle() {
        // Given a cache.
        StateCache cache = new StateCache(curator, paths, metricRegistry, Observable.empty());

        // When the cache is started.
        cache.startAsync().awaitRunning();

        // Then the cache is running.
        Assert.assertTrue(cache.isRunning());

        // When the cache is stopped.
        cache.stopAsync().awaitTerminated();

        // Then the cache is not running.
        Assert.assertFalse(cache.isRunning());

        // And the cache returns the classes.
        Assert.assertTrue(ObjectCache.classes().length > 0);
    }

    @Test
    public void testSubscribeBeforeStart() throws Exception {
        // Given a cache.
        StateCache cache = new StateCache(curator, paths, metricRegistry, Observable.empty());

        // And an observer.
        TestAwaitableObserver<StateNotification> observer = new TestAwaitableObserver<>();

        // When the observer subscribes.
        cache.observable().subscribe(observer);

        // Then the observer receives an error.
        observer.awaitCompletion(TIMEOUT);
        Assert.assertEquals(1, observer.getOnErrorEvents().size());
        Assert.assertEquals(IllegalStateException.class, observer.getOnErrorEvents().get(0).getClass());
    }

    @Test
    public void testCachePublishesHostState() throws Exception {
        // Given an object and state cache.
        ObjectCache objectCache = new ObjectCache(curator, paths, metricRegistry);
        StateCache cache = new StateCache(curator, paths, metricRegistry, objectCache.observable());

        // And an observer.
        TestAwaitableObserver<StateNotification> observer1 = new TestAwaitableObserver<>();

        // When the cache is started.
        objectCache.startAsync().awaitRunning();
        cache.startAsync().awaitRunning();

        // And the observer subscribes.
        Subscription sub1 = cache.observable().subscribe(observer1);

        // Then the observer should receive an empty snapshot notification.
        observer1.awaitOnNext(1, TIMEOUT);
        StateNotification.Snapshot snapshot = (StateNotification.Snapshot) observer1.getOnNextEvents().get(0);

        Assert.assertEquals(observer1.getOnNextEvents().size(), 1);
        Assert.assertTrue(snapshot.isEmpty());

        // When adding a host to the topology.
        UUID id = UUID.randomUUID();
        Topology.Host host = Topology.Host.newBuilder().setId(UUIDUtil$.MODULE$.toProto(id)).build();
        storage.create(host);

        // Then the observer receives two notifications.
        observer1.awaitOnNext(3, TIMEOUT);

        StateNotification.Update update = (StateNotification.Update) observer1.getOnNextEvents().get(1);
        Assert.assertEquals(update.singleData().length, 0);

        update = (StateNotification.Update) observer1.getOnNextEvents().get(2);
        Assert.assertEquals(update.singleData().length, 0);

        // When adding host alive state.
        addSingle(id, Topology.Host.class, id, "alive", "true");

        // Then the observer receives a notification.
        observer1.awaitOnNext(4, TIMEOUT);

        update = (StateNotification.Update) observer1.getOnNextEvents().get(3);
        assertUpdateEquals(update, id, Topology.Host.class, id, "alive", KeyType.SingleLastWriteWins(),
                "true".getBytes(), null);

        // When updating host alive state.
        addSingle(id, Topology.Host.class, id, "alive", "false");

        // Then the observer receives a notification.
        observer1.awaitOnNext(5, TIMEOUT);

        update = (StateNotification.Update) observer1.getOnNextEvents().get(4);
        assertUpdateEquals(update, id, Topology.Host.class, id, "alive", KeyType.SingleLastWriteWins(),
                "false".getBytes(), null);

        // When removing the state key.
        removeSingle(id, Topology.Host.class, id, "alive");

        // Then the observer receives a notification.
        observer1.awaitOnNext(6, TIMEOUT);

        update = (StateNotification.Update) observer1.getOnNextEvents().get(5);
        assertUpdateEquals(update, id, Topology.Host.class, id, "alive", KeyType.SingleLastWriteWins());

        // When re-adding the state key.
        addSingle(id, Topology.Host.class, id, "alive", "alive");

        // Then the observer receives a notification.
        observer1.awaitOnNext(7, TIMEOUT);

        update = (StateNotification.Update) observer1.getOnNextEvents().get(6);
        assertUpdateEquals(update, id, Topology.Host.class, id, "alive", KeyType.SingleLastWriteWins(),
                "alive".getBytes(), null);

        // Given a second observer.
        TestAwaitableObserver<StateNotification> observer2 = new TestAwaitableObserver<>();

        // When the observer subscribes.
        Subscription sub2 = cache.observable().subscribe(observer2);

        // Then the observer receives a snapshot with the current state.
        observer2.awaitOnNext(1, TIMEOUT);
        snapshot = (StateNotification.Snapshot) observer2.getOnNextEvents().get(0);

        Assert.assertEquals(observer2.getOnNextEvents().size(), 1);
        Assert.assertEquals(snapshot.size(), 2);

        assertUpdateEquals(updateFor(snapshot, "alive"), id, Topology.Host.class, id, "alive",
                KeyType.SingleLastWriteWins(), "alive".getBytes(), null);
        assertUpdateEquals(updateFor(snapshot, "host"), id, Topology.Host.class, id, "host",
                KeyType.SingleLastWriteWins());

        // When updating the host entry.
        State.HostState hostState = State.HostState.newBuilder()
                .setHostId(Commons.UUID.newBuilder().setLsb(1L).setMsb(1L)).build();
        addSingle(id, Topology.Host.class, id, "host", hostState.toString());

        // Then both observers should receive a notification.
        observer1.awaitOnNext(8, TIMEOUT);

        update = (StateNotification.Update) observer1.getOnNextEvents().get(7);
        assertUpdateEquals(update, id, Topology.Host.class, id, "host", KeyType.SingleLastWriteWins(),
                hostState.toByteArray(), null);

        observer2.awaitOnNext(2, TIMEOUT);

        update = (StateNotification.Update) observer2.getOnNextEvents().get(1);
        assertUpdateEquals(update, id, Topology.Host.class, id, "host", KeyType.SingleLastWriteWins(),
                hostState.toByteArray(), null);

        // When deleting the object.
        storage.delete(Topology.Host.class, id);

        // Then both observers should receive two notifications.
        observer1.awaitOnNext(10, TIMEOUT);

        update = (StateNotification.Update) observer1.getOnNextEvents().get(8);
        Assert.assertEquals(update.owner(), null);
        update = (StateNotification.Update) observer1.getOnNextEvents().get(9);
        Assert.assertEquals(update.owner(), null);

        observer2.awaitOnNext(4, TIMEOUT);

        update = (StateNotification.Update) observer2.getOnNextEvents().get(2);
        Assert.assertEquals(update.owner(), null);
        update = (StateNotification.Update) observer2.getOnNextEvents().get(3);
        Assert.assertEquals(update.owner(), null);

        // When creating a new object.
        storage.create(host);

        // Then both observers should receive notifications.
        observer1.awaitOnNext(12, TIMEOUT);

        update = (StateNotification.Update) observer1.getOnNextEvents().get(10);
        Assert.assertEquals(update.owner(), id);
        update = (StateNotification.Update) observer1.getOnNextEvents().get(11);
        Assert.assertEquals(update.owner(), id);

        observer2.awaitOnNext(6, TIMEOUT);

        update = (StateNotification.Update) observer2.getOnNextEvents().get(4);
        Assert.assertEquals(update.owner(), id);
        update = (StateNotification.Update) observer2.getOnNextEvents().get(5);
        Assert.assertEquals(update.owner(), id);

        // When the cache is stopped.
        cache.stopAsync().awaitTerminated();

        // Then both observers should receive notifications.
        observer1.awaitOnNext(14, TIMEOUT);

        update = (StateNotification.Update) observer1.getOnNextEvents().get(12);
        Assert.assertEquals(update.owner(), null);
        update = (StateNotification.Update) observer1.getOnNextEvents().get(13);
        Assert.assertEquals(update.owner(), null);

        observer2.awaitOnNext(8, TIMEOUT);

        update = (StateNotification.Update) observer2.getOnNextEvents().get(6);
        Assert.assertEquals(update.owner(), null);
        update = (StateNotification.Update) observer2.getOnNextEvents().get(7);
        Assert.assertEquals(update.owner(), null);

        objectCache.stopAsync().awaitTerminated();
    }

    @Test
    public void testCachePublishesPortActiveState() throws Exception {
        // Given an object and state cache.
        ObjectCache objectCache = new ObjectCache(curator, paths, metricRegistry);
        StateCache cache = new StateCache(curator, paths, metricRegistry, objectCache.observable());

        // And an observer.
        TestAwaitableObserver<StateNotification> observer1 = new TestAwaitableObserver<>();

        // When the cache is started.
        objectCache.startAsync().awaitRunning();
        cache.startAsync().awaitRunning();

        // And the observer subscribes.
        cache.observable().subscribe(observer1);

        // Then the observer should receive an empty snapshot notification.
        observer1.awaitOnNext(1, TIMEOUT);
        StateNotification.Snapshot snapshot = (StateNotification.Snapshot) observer1.getOnNextEvents().get(0);

        Assert.assertEquals(observer1.getOnNextEvents().size(), 1);
        Assert.assertTrue(snapshot.isEmpty());

        // When adding a host to the topology.
        UUID hostId = UUID.randomUUID();
        Topology.Host host = Topology.Host.newBuilder().setId(UUIDUtil$.MODULE$.toProto(hostId)).build();
        storage.create(host);

        // Then the observer receives three notifications, one snapshot and one
        // for each key registered for a host.
        observer1.awaitOnNext(3, TIMEOUT);

        // When adding a port to the topology.
        UUID portId = UUID.randomUUID();
        Topology.Port port = Topology.Port.newBuilder().setId(UUIDUtil$.MODULE$.toProto(portId)).build();
        storage.create(port);

        // Then the observer receives three more notifications, one for each
        // state key registered for a port.
        observer1.awaitOnNext(6, TIMEOUT);

        // And one update should be port inactive.
        StateNotification.Update update = updateFor(observer1.getOnNextEvents(), 3, "active");
        assertUpdateEquals(update, null, Topology.Port.class, portId, "active", KeyType.SingleLastWriteWins());

        // When making the port active.
        State.PortState state = State.PortState.newBuilder().setHostId(UUIDUtil$.MODULE$.toProto(hostId)).build();
        addSingle(hostId, Topology.Port.class, portId, "active", state.toString());

        // Then the observer should not receive a new notification.
        Assert.assertEquals(6, observer1.getOnNextEvents().size());

        // When the port is bound to the host.
        port = port.toBuilder().setHostId(UUIDUtil$.MODULE$.toProto(hostId)).build();
        storage.update(port);

        // Then the observer receives three new notifications.
        observer1.awaitOnNext(9, TIMEOUT);

        // And the notification is port active.
        update = updateFor(observer1.getOnNextEvents(), 6, "active");
        assertUpdateEquals(update, hostId, Topology.Port.class, portId, "active", KeyType.SingleLastWriteWins(),
                state.toByteArray(), null);

        // When removing the port state.
        removeSingle(hostId, Topology.Port.class, portId, "active");

        // Then the observer receives a new notifications.
        observer1.awaitOnNext(10, TIMEOUT);

        // And the update should be port inactive.
        update = updateFor(observer1.getOnNextEvents(), 9, "active");
        assertUpdateEquals(update, hostId, Topology.Port.class, portId, "active", KeyType.SingleLastWriteWins());

        // When making the port active.
        addSingle(hostId, Topology.Port.class, portId, "active", state.toString());

        // Then the observer receives a new notifications.
        observer1.awaitOnNext(11, TIMEOUT);

        // And the update should be port inactive.
        update = updateFor(observer1.getOnNextEvents(), 10, "active");
        assertUpdateEquals(update, hostId, Topology.Port.class, portId, "active", KeyType.SingleLastWriteWins(),
                state.toByteArray(), null);

        // When removing the port from the host.
        port = port.toBuilder().clearHostId().build();
        storage.update(port);

        // Then the observer receives three new notifications.
        observer1.awaitOnNext(14, TIMEOUT);

        // And one notification is port inactive.
        update = updateFor(observer1.getOnNextEvents(), 11, "active");
        assertUpdateEquals(update, null, Topology.Port.class, portId, "active", KeyType.SingleLastWriteWins());

        cache.stopAsync().awaitTerminated();
        objectCache.stopAsync().awaitTerminated();
    }

    @Test
    public void testCachePublishesPortRoutesState() throws Exception {
        // Given an object and state cache.
        ObjectCache objectCache = new ObjectCache(curator, paths, metricRegistry);
        StateCache cache = new StateCache(curator, paths, metricRegistry, objectCache.observable());

        // And an observer.
        TestAwaitableObserver<StateNotification> observer1 = new TestAwaitableObserver<>();

        // When the cache is started.
        objectCache.startAsync().awaitRunning();
        cache.startAsync().awaitRunning();

        // And the observer subscribes.
        cache.observable().subscribe(observer1);

        // Then the observer should receive an empty snapshot notification.
        observer1.awaitOnNext(1, TIMEOUT);
        StateNotification.Snapshot snapshot = (StateNotification.Snapshot) observer1.getOnNextEvents().get(0);

        Assert.assertEquals(observer1.getOnNextEvents().size(), 1);
        Assert.assertTrue(snapshot.isEmpty());

        // When adding a host to the topology.
        UUID hostId = UUID.randomUUID();
        Topology.Host host = Topology.Host.newBuilder().setId(UUIDUtil$.MODULE$.toProto(hostId)).build();
        storage.create(host);

        // Then the observer receives three notifications, one snapshot and one
        // for each key registered for a host.
        observer1.awaitOnNext(3, TIMEOUT);

        // When adding a port to the topology
        UUID portId = UUID.randomUUID();
        Topology.Port port = Topology.Port.newBuilder().setId(UUIDUtil$.MODULE$.toProto(portId)).build();
        storage.create(port);

        // Then the observer receives three more notifications, one for each
        // state key registered for a port.
        observer1.awaitOnNext(6, TIMEOUT);

        // And one update should be port routes.
        StateNotification.Update update = updateFor(observer1.getOnNextEvents(), 3, "routes");
        assertUpdateEquals(update, null, Topology.Port.class, portId, "routes", KeyType.Multiple());

        // When adding port routes state (just random strings).
        String[] routes = new String[] { UUID.randomUUID().toString(), UUID.randomUUID().toString(),
                UUID.randomUUID().toString(), UUID.randomUUID().toString(), };
        addMulti(hostId, Topology.Port.class, portId, "routes", routes);

        // Then the observer should not receive a new notification.
        Assert.assertEquals(6, observer1.getOnNextEvents().size());

        // When the port is bound to the host.
        port = port.toBuilder().setHostId(UUIDUtil$.MODULE$.toProto(hostId)).build();
        storage.update(port);

        // Then the observer receives three new notifications.
        observer1.awaitOnNext(9, TIMEOUT);

        // And the notification is port routes.
        update = updateFor(observer1.getOnNextEvents(), 6, "routes");
        assertUpdateEquals(update, hostId, Topology.Port.class, portId, "routes", KeyType.Multiple(), null, routes);

        // When removing a port route.
        removeMulti(hostId, Topology.Port.class, portId, "routes", routes[0]);

        // Then the observer receives a new notification.
        observer1.awaitOnNext(10, TIMEOUT);

        // And the notification is port routes.
        update = updateFor(observer1.getOnNextEvents(), 9, "routes");
        assertUpdateEquals(update, hostId, Topology.Port.class, portId, "routes", KeyType.Multiple(), null,
                new String[] { routes[1], routes[2], routes[3] });

        // When re-adding the port route.
        addMulti(hostId, Topology.Port.class, portId, "routes", routes[0]);

        // Then the observer receives a new notification.
        observer1.awaitOnNext(11, TIMEOUT);

        // And the notification is port routes.
        update = updateFor(observer1.getOnNextEvents(), 10, "routes");
        assertUpdateEquals(update, hostId, Topology.Port.class, portId, "routes", KeyType.Multiple(), null, routes);

        // Given a second observer.
        TestAwaitableObserver<StateNotification> observer2 = new TestAwaitableObserver<>();

        // When the observer subscribes.
        cache.observable().subscribe(observer2);

        // Then the observer receives a snapshot with the current snapshot.
        observer2.awaitOnNext(1, TIMEOUT);
        snapshot = (StateNotification.Snapshot) observer2.getOnNextEvents().get(0);
        update = updateFor(snapshot, "routes");

        // And the snapshot includes five notifications.
        Assert.assertEquals(snapshot.size(), 5);

        // And one notification is a port routes notification.
        assertUpdateEquals(update, hostId, Topology.Port.class, portId, "routes", KeyType.Multiple(), null, routes);

        // When deleting the object.
        storage.delete(Topology.Port.class, portId);

        // Then both observers should receive three more notifications.
        observer1.awaitOnNext(14, TIMEOUT);
        observer2.awaitOnNext(4, TIMEOUT);

        // And these should include a routes notification.
        update = updateFor(observer1.getOnNextEvents(), 11, "routes");
        assertUpdateEquals(update, null, Topology.Port.class, portId, "routes", KeyType.Multiple());

        update = updateFor(observer2.getOnNextEvents(), 1, "routes");
        assertUpdateEquals(update, null, Topology.Port.class, portId, "routes", KeyType.Multiple());

        cache.stopAsync().awaitTerminated();
        objectCache.stopAsync().awaitTerminated();
    }

    @Test
    public void testCachePublishesPortBgpState() throws Exception {
        // Given an object and state cache.
        ObjectCache objectCache = new ObjectCache(curator, paths, metricRegistry);
        StateCache cache = new StateCache(curator, paths, metricRegistry, objectCache.observable());

        // And an observer.
        TestAwaitableObserver<StateNotification> observer1 = new TestAwaitableObserver<>();

        // When the cache is started.
        objectCache.startAsync().awaitRunning();
        cache.startAsync().awaitRunning();

        // And the observer subscribes.
        cache.observable().subscribe(observer1);

        // Then the observer should receive an empty snapshot notification.
        observer1.awaitOnNext(1, TIMEOUT);
        StateNotification.Snapshot snapshot = (StateNotification.Snapshot) observer1.getOnNextEvents().get(0);

        Assert.assertEquals(observer1.getOnNextEvents().size(), 1);
        Assert.assertTrue(snapshot.isEmpty());

        // When adding a host to the topology.
        UUID hostId = UUID.randomUUID();
        Topology.Host host = Topology.Host.newBuilder().setId(UUIDUtil$.MODULE$.toProto(hostId)).build();
        storage.create(host);

        // Then the observer receives three notifications, one snapshot and one
        // for each key registered for a host.
        observer1.awaitOnNext(3, TIMEOUT);

        // When adding a port to the topology
        UUID portId = UUID.randomUUID();
        Topology.Port port = Topology.Port.newBuilder().setId(UUIDUtil$.MODULE$.toProto(portId)).build();
        storage.create(port);

        // Then the observer receives three more notifications, one for each
        // state key registered for a port.
        observer1.awaitOnNext(6, TIMEOUT);

        // And one update should be port BGP.
        StateNotification.Update update = updateFor(observer1.getOnNextEvents(), 3, "bgp");
        assertUpdateEquals(update, null, Topology.Port.class, portId, "bgp", KeyType.SingleLastWriteWins());

        // When adding port BGP state.
        addSingle(hostId, Topology.Port.class, portId, "bgp", "data0");

        // Then the observer should not receive a new notification.
        Assert.assertEquals(6, observer1.getOnNextEvents().size());

        // When the port is bound to the host.
        port = port.toBuilder().setHostId(UUIDUtil$.MODULE$.toProto(hostId)).build();
        storage.update(port);

        // Then the observer receives three new notifications.
        observer1.awaitOnNext(9, TIMEOUT);

        // And the notification is port BGP.
        update = updateFor(observer1.getOnNextEvents(), 6, "bgp");
        assertUpdateEquals(update, hostId, Topology.Port.class, portId, "bgp", KeyType.SingleLastWriteWins(),
                "data0".getBytes(), null);

        // When updating the BGP state.
        addSingle(hostId, Topology.Port.class, portId, "bgp", "data1");

        // Then the observer receives a new notification.
        observer1.awaitOnNext(10, TIMEOUT);

        // And the notification is port BGP.
        update = updateFor(observer1.getOnNextEvents(), 9, "bgp");
        assertUpdateEquals(update, hostId, Topology.Port.class, portId, "bgp", KeyType.SingleLastWriteWins(),
                "data1".getBytes(), null);

        // When removing the state key.
        removeSingle(hostId, Topology.Port.class, portId, "bgp");

        // Then the observer receives a new notification.
        observer1.awaitOnNext(11, TIMEOUT);

        // And the notification is port BGP.
        update = updateFor(observer1.getOnNextEvents(), 10, "bgp");
        assertUpdateEquals(update, hostId, Topology.Port.class, portId, "bgp", KeyType.SingleLastWriteWins());

        // When re-adding the state key.
        addSingle(hostId, Topology.Port.class, portId, "bgp", "data2");

        // Then the observer receives a new notification.
        observer1.awaitOnNext(12, TIMEOUT);

        // And the notification is port BGP.
        update = updateFor(observer1.getOnNextEvents(), 11, "bgp");
        assertUpdateEquals(update, hostId, Topology.Port.class, portId, "bgp", KeyType.SingleLastWriteWins(),
                "data2".getBytes(), null);

        // Given a second observer.
        TestAwaitableObserver<StateNotification> observer2 = new TestAwaitableObserver<>();

        // When the observer subscribes.
        cache.observable().subscribe(observer2);

        // Then the observer receives a snapshot with the current snapshot.
        observer2.awaitOnNext(1, TIMEOUT);
        snapshot = (StateNotification.Snapshot) observer2.getOnNextEvents().get(0);
        update = updateFor(snapshot, "bgp");

        // And the snapshot includes five notifications.
        Assert.assertEquals(snapshot.size(), 5);

        // And one notification is a port routes notification.
        assertUpdateEquals(update, hostId, Topology.Port.class, portId, "bgp", KeyType.SingleLastWriteWins(),
                "data2".getBytes(), null);

        // When updating the bgp entry.
        addSingle(hostId, Topology.Port.class, portId, "bgp", "data3");

        // Then both observers should receive a notification.
        observer1.awaitOnNext(13, TIMEOUT);
        observer2.awaitOnNext(2, TIMEOUT);

        update = updateFor(observer1.getOnNextEvents(), 12, "bgp");
        assertUpdateEquals(update, hostId, Topology.Port.class, portId, "bgp", KeyType.SingleLastWriteWins(),
                "data3".getBytes(), null);

        update = updateFor(observer2.getOnNextEvents(), 1, "bgp");
        assertUpdateEquals(update, hostId, Topology.Port.class, portId, "bgp", KeyType.SingleLastWriteWins(),
                "data3".getBytes(), null);

        // When deleting the object.
        storage.delete(Topology.Port.class, portId);

        // Then both observers should receive three more notifications.
        observer1.awaitOnNext(16, TIMEOUT);
        observer2.awaitOnNext(5, TIMEOUT);

        // And these should include a routes notification.
        update = updateFor(observer1.getOnNextEvents(), 13, "bgp");
        assertUpdateEquals(update, null, Topology.Port.class, portId, "bgp", KeyType.SingleLastWriteWins());

        update = updateFor(observer2.getOnNextEvents(), 2, "bgp");
        assertUpdateEquals(update, null, Topology.Port.class, portId, "bgp", KeyType.SingleLastWriteWins());

        cache.stopAsync().awaitTerminated();
        objectCache.stopAsync().awaitTerminated();
    }
}