BUG-5280: add FrontendMetadata 24/43124/15
authorRobert Varga <rovarga@cisco.com>
Wed, 3 Aug 2016 00:52:20 +0000 (02:52 +0200)
committerTony Tkacik <ttkacik@cisco.com>
Fri, 12 Aug 2016 08:39:14 +0000 (08:39 +0000)
This patch adds the frontend tracking abilities for followers.
It also defines the corresponding ShardDataTreeSnapshotMetadata
for use with persistence.

Change-Id: I7e2c6755c3389dcb5284f17a9c6076fb9e7ac95e
Signed-off-by: Robert Varga <rovarga@cisco.com>
Signed-off-by: Vaclav Demcak <vdemcak@cisco.com>
opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/FrontendClientMetadataBuilder.java [new file with mode: 0644]
opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/FrontendHistoryMetadataBuilder.java [new file with mode: 0644]
opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/FrontendMetadata.java [new file with mode: 0644]
opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/Shard.java
opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/ShardDataTree.java
opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/ShardDataTreeMetadata.java
opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/persisted/FrontendClientMetadata.java [new file with mode: 0644]
opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/persisted/FrontendHistoryMetadata.java [new file with mode: 0644]
opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/persisted/FrontendShardDataTreeSnapshotMetadata.java [new file with mode: 0644]
opendaylight/md-sal/sal-distributed-datastore/src/test/java/org/opendaylight/controller/cluster/datastore/persisted/FrontendShardDataTreeSnapshotMetadataTest.java [new file with mode: 0644]

diff --git a/opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/FrontendClientMetadataBuilder.java b/opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/FrontendClientMetadataBuilder.java
new file mode 100644 (file)
index 0000000..0dab830
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.controller.cluster.datastore;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeSet;
+import com.google.common.primitives.UnsignedLong;
+import java.util.HashMap;
+import java.util.Map;
+import org.opendaylight.controller.cluster.access.concepts.ClientIdentifier;
+import org.opendaylight.controller.cluster.access.concepts.LocalHistoryIdentifier;
+import org.opendaylight.controller.cluster.access.concepts.TransactionIdentifier;
+import org.opendaylight.controller.cluster.datastore.persisted.FrontendClientMetadata;
+import org.opendaylight.controller.cluster.datastore.persisted.FrontendHistoryMetadata;
+import org.opendaylight.yangtools.concepts.Builder;
+import org.opendaylight.yangtools.concepts.Identifiable;
+
+final class FrontendClientMetadataBuilder implements Builder<FrontendClientMetadata>, Identifiable<ClientIdentifier> {
+    private final Map<LocalHistoryIdentifier, FrontendHistoryMetadataBuilder> currentHistories = new HashMap<>();
+    private final RangeSet<UnsignedLong> purgedHistories;
+    private final ClientIdentifier identifier;
+
+    FrontendClientMetadataBuilder(final ClientIdentifier identifier) {
+        this.identifier = Preconditions.checkNotNull(identifier);
+        purgedHistories = TreeRangeSet.create();
+    }
+
+    FrontendClientMetadataBuilder(final FrontendClientMetadata meta) {
+        this.identifier = Preconditions.checkNotNull(meta.getIdentifier());
+        purgedHistories = TreeRangeSet.create(meta.getPurgedHistories());
+
+        for (FrontendHistoryMetadata h : meta.getCurrentHistories()) {
+            final FrontendHistoryMetadataBuilder b = new FrontendHistoryMetadataBuilder(identifier, h);
+            currentHistories.put(b.getIdentifier(), b);
+        }
+    }
+
+    @Override
+    public FrontendClientMetadata build() {
+        return new FrontendClientMetadata(identifier, purgedHistories,
+            Collections2.transform(currentHistories.values(), FrontendHistoryMetadataBuilder::build));
+    }
+
+    @Override
+    public ClientIdentifier getIdentifier() {
+        return identifier;
+    }
+
+    void onHistoryClosed(final LocalHistoryIdentifier historyId) {
+        ensureHistory(historyId).onHistoryClosed();
+    }
+
+    void onHistoryPurged(final LocalHistoryIdentifier historyId) {
+        currentHistories.remove(historyId);
+        // XXX: do we need to account for cookies?
+        purgedHistories.add(Range.singleton(UnsignedLong.fromLongBits(historyId.getHistoryId())));
+    }
+
+    void onTransactionCommitted(final TransactionIdentifier txId) {
+        ensureHistory(txId.getHistoryId()).onTransactionCommitted(txId);
+    }
+
+    private FrontendHistoryMetadataBuilder ensureHistory(final LocalHistoryIdentifier historyId) {
+        final FrontendHistoryMetadataBuilder existing = currentHistories.get(historyId);
+        if (existing != null) {
+            return existing;
+        }
+
+        final FrontendHistoryMetadataBuilder ret = new FrontendHistoryMetadataBuilder(historyId);
+        currentHistories.put(historyId, ret);
+        return ret;
+    }
+}
diff --git a/opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/FrontendHistoryMetadataBuilder.java b/opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/FrontendHistoryMetadataBuilder.java
new file mode 100644 (file)
index 0000000..d8d7bdd
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.controller.cluster.datastore;
+
+import com.google.common.base.Preconditions;
+import org.opendaylight.controller.cluster.access.concepts.ClientIdentifier;
+import org.opendaylight.controller.cluster.access.concepts.LocalHistoryIdentifier;
+import org.opendaylight.controller.cluster.access.concepts.TransactionIdentifier;
+import org.opendaylight.controller.cluster.datastore.persisted.FrontendHistoryMetadata;
+import org.opendaylight.yangtools.concepts.Builder;
+import org.opendaylight.yangtools.concepts.Identifiable;
+
+final class FrontendHistoryMetadataBuilder implements Builder<FrontendHistoryMetadata>, Identifiable<LocalHistoryIdentifier> {
+    private final LocalHistoryIdentifier identifier;
+
+    private long nextTransaction;
+    private boolean closed;
+
+    FrontendHistoryMetadataBuilder(final LocalHistoryIdentifier identifier) {
+        this.identifier = Preconditions.checkNotNull(identifier);
+    }
+
+    FrontendHistoryMetadataBuilder(final ClientIdentifier clientId, final FrontendHistoryMetadata meta) {
+        identifier = new LocalHistoryIdentifier(clientId, meta.getHistoryId(), meta.getCookie());
+        nextTransaction = meta.getNextTransaction();
+        closed = meta.isClosed();
+    }
+
+    @Override
+    public LocalHistoryIdentifier getIdentifier() {
+        return identifier;
+    }
+
+    @Override
+    public FrontendHistoryMetadata build() {
+        return new FrontendHistoryMetadata(identifier.getHistoryId(), identifier.getCookie(), nextTransaction, closed);
+    }
+
+    void onHistoryClosed() {
+        closed = true;
+    }
+
+    void onTransactionCommitted(final TransactionIdentifier txId) {
+        nextTransaction = txId.getTransactionId() + 1;
+    }
+}
diff --git a/opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/FrontendMetadata.java b/opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/FrontendMetadata.java
new file mode 100644 (file)
index 0000000..164e7a9
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.controller.cluster.datastore;
+
+import com.google.common.collect.Collections2;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.concurrent.NotThreadSafe;
+import org.opendaylight.controller.cluster.access.concepts.ClientIdentifier;
+import org.opendaylight.controller.cluster.access.concepts.FrontendIdentifier;
+import org.opendaylight.controller.cluster.access.concepts.LocalHistoryIdentifier;
+import org.opendaylight.controller.cluster.access.concepts.TransactionIdentifier;
+import org.opendaylight.controller.cluster.datastore.persisted.FrontendClientMetadata;
+import org.opendaylight.controller.cluster.datastore.persisted.FrontendShardDataTreeSnapshotMetadata;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Frontend state as observed by a shard follower. This class is responsible for maintaining metadata state
+ * so that this can be used to seed {@link LeaderFrontendState} with proper state so that the frontend/backend
+ * conversation can continue where it left off.
+ *
+ * @author Robert Varga
+ */
+@NotThreadSafe
+final class FrontendMetadata extends ShardDataTreeMetadata<FrontendShardDataTreeSnapshotMetadata> {
+    private static final Logger LOG = LoggerFactory.getLogger(FrontendMetadata.class);
+
+    private final Map<FrontendIdentifier, FrontendClientMetadataBuilder> clients = new HashMap<>();
+
+    @Override
+    Class<FrontendShardDataTreeSnapshotMetadata> getSupportedType() {
+        return FrontendShardDataTreeSnapshotMetadata.class;
+    }
+
+    @Override
+    void reset() {
+        clients.clear();
+    }
+
+    @Override
+    void doApplySnapshot(final FrontendShardDataTreeSnapshotMetadata snapshot) {
+        clients.clear();
+
+        for (FrontendClientMetadata m : snapshot.getClients()) {
+            clients.put(m.getIdentifier().getFrontendId(), new FrontendClientMetadataBuilder(m));
+        }
+    }
+
+    @Override
+    FrontendShardDataTreeSnapshotMetadata toStapshot() {
+        return new FrontendShardDataTreeSnapshotMetadata(Collections2.transform(clients.values(),
+            FrontendClientMetadataBuilder::build));
+    }
+
+    private FrontendClientMetadataBuilder ensureClient(final ClientIdentifier id) {
+        final FrontendClientMetadataBuilder existing = clients.get(id.getFrontendId());
+        if (existing != null && id.equals(existing.getIdentifier())) {
+            return existing;
+        }
+
+        final FrontendClientMetadataBuilder client = new FrontendClientMetadataBuilder(id);
+        final FrontendClientMetadataBuilder previous = clients.put(id.getFrontendId(), client);
+        if (previous != null) {
+            LOG.debug("Replaced client {} with {}", previous, client);
+        } else {
+            LOG.debug("Added client {}", client);
+        }
+        return client;
+    }
+
+    @Override
+    void onHistoryClosed(final LocalHistoryIdentifier historyId) {
+        ensureClient(historyId.getClientId()).onHistoryClosed(historyId);
+    }
+
+    @Override
+    void onHistoryPurged(final LocalHistoryIdentifier historyId) {
+        ensureClient(historyId.getClientId()).onHistoryPurged(historyId);
+    }
+
+    @Override
+    void onTransactionCommitted(final TransactionIdentifier txId) {
+        ensureClient(txId.getHistoryId().getClientId()).onTransactionCommitted(txId);
+    }
+}
index fbd7c89..1b12462 100644 (file)
@@ -129,6 +129,8 @@ public class Shard extends RaftActor {
 
     private final ShardTransactionMessageRetrySupport messageRetrySupport;
 
+    private final FrontendMetadata frontendMetadata = new FrontendMetadata();
+
     protected Shard(final AbstractBuilder<?, ?> builder) {
         super(builder.getId().toString(), builder.getPeerAddresses(),
                 Optional.of(builder.getDatastoreContext().getShardRaftConfig()), DataStoreVersions.CURRENT_VERSION);
index f1d3787..d43602a 100644 (file)
@@ -346,7 +346,7 @@ public class ShardDataTree extends ShardDataTreeTransactionParent {
 
     private void allMetadataCommittedTransaction(final TransactionIdentifier txId) {
         for (ShardDataTreeMetadata<?> m : metadata) {
-            m.transactionCommitted(txId);
+            m.onTransactionCommitted(txId);
         }
     }
 
index 8a62a2b..1fce144 100644 (file)
@@ -10,6 +10,7 @@ package org.opendaylight.controller.cluster.datastore;
 import com.google.common.base.Verify;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import org.opendaylight.controller.cluster.access.concepts.LocalHistoryIdentifier;
 import org.opendaylight.controller.cluster.access.concepts.TransactionIdentifier;
 import org.opendaylight.controller.cluster.datastore.persisted.ShardDataTreeSnapshotMetadata;
 
@@ -29,5 +30,7 @@ abstract class ShardDataTreeMetadata<T extends ShardDataTreeSnapshotMetadata<T>>
     abstract @Nullable T toStapshot();
 
     // Lifecycle events
-    abstract void transactionCommitted(TransactionIdentifier txId);
+    abstract void onTransactionCommitted(TransactionIdentifier txId);
+    abstract void onHistoryClosed(LocalHistoryIdentifier historyId);
+    abstract void onHistoryPurged(LocalHistoryIdentifier historyId);
 }
diff --git a/opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/persisted/FrontendClientMetadata.java b/opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/persisted/FrontendClientMetadata.java
new file mode 100644 (file)
index 0000000..91c81ed
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.controller.cluster.datastore.persisted;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableRangeSet;
+import com.google.common.collect.ImmutableRangeSet.Builder;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.primitives.UnsignedLong;
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Set;
+import org.opendaylight.controller.cluster.access.concepts.ClientIdentifier;
+import org.opendaylight.yangtools.concepts.Identifiable;
+import org.opendaylight.yangtools.concepts.WritableObject;
+import org.opendaylight.yangtools.concepts.WritableObjects;
+
+public final class FrontendClientMetadata implements Identifiable<ClientIdentifier>, WritableObject {
+    private final Collection<FrontendHistoryMetadata> currentHistories;
+    private final RangeSet<UnsignedLong> purgedHistories;
+    private final ClientIdentifier identifier;
+
+    public FrontendClientMetadata(final ClientIdentifier identifier, final RangeSet<UnsignedLong> purgedHistories,
+            final Collection<FrontendHistoryMetadata> currentHistories) {
+        this.identifier = Preconditions.checkNotNull(identifier);
+        this.purgedHistories = ImmutableRangeSet.copyOf(purgedHistories);
+        this.currentHistories = ImmutableList.copyOf(currentHistories);
+    }
+
+    public Collection<FrontendHistoryMetadata> getCurrentHistories() {
+        return currentHistories;
+    }
+
+    public RangeSet<UnsignedLong> getPurgedHistories() {
+        return purgedHistories;
+    }
+
+    @Override
+    public ClientIdentifier getIdentifier() {
+        return identifier;
+    }
+
+    @Override
+    public void writeTo(final DataOutput out) throws IOException {
+        identifier.writeTo(out);
+
+        final Set<Range<UnsignedLong>> ranges = purgedHistories.asRanges();
+        out.writeInt(ranges.size());
+        for (final Range<UnsignedLong> r : ranges) {
+            WritableObjects.writeLongs(out, r.lowerEndpoint().longValue(), r.upperEndpoint().longValue());
+        }
+
+        out.writeInt(currentHistories.size());
+        for (final FrontendHistoryMetadata h : currentHistories) {
+            h.writeTo(out);
+        }
+    }
+
+    public static FrontendClientMetadata readFrom(final DataInput in) throws IOException, ClassNotFoundException {
+        final ClientIdentifier id = ClientIdentifier.readFrom(in);
+
+        final int purgedSize = in.readInt();
+        final Builder<UnsignedLong> b = ImmutableRangeSet.builder();
+        for (int i = 0; i < purgedSize; ++i) {
+            final byte header = WritableObjects.readLongHeader(in);
+            final UnsignedLong lower = UnsignedLong.fromLongBits(WritableObjects.readFirstLong(in, header));
+            final UnsignedLong upper = UnsignedLong.fromLongBits(WritableObjects.readSecondLong(in, header));
+
+            b.add(Range.closed(lower, upper));
+        }
+
+        final int currentSize = in.readInt();
+        final Collection<FrontendHistoryMetadata> currentHistories = new ArrayList<>(currentSize);
+        for (int i = 0; i < currentSize; ++i) {
+            currentHistories.add(FrontendHistoryMetadata.readFrom(in));
+        }
+
+        return new FrontendClientMetadata(id, b.build(), currentHistories);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(FrontendClientMetadata.class).add("Identifier", identifier)
+                .add("CurrentHistory", currentHistories).add("Range", purgedHistories).toString();
+    }
+}
diff --git a/opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/persisted/FrontendHistoryMetadata.java b/opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/persisted/FrontendHistoryMetadata.java
new file mode 100644 (file)
index 0000000..2a52ecc
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.controller.cluster.datastore.persisted;
+
+import com.google.common.base.MoreObjects;
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import org.opendaylight.yangtools.concepts.WritableObject;
+import org.opendaylight.yangtools.concepts.WritableObjects;
+
+public final class FrontendHistoryMetadata implements WritableObject {
+    private final long historyId;
+    private final long cookie;
+    private final long nextTransaction;
+    private final boolean closed;
+
+    public FrontendHistoryMetadata(final long historyId, final long cookie, final long nextTransaction,
+            final boolean closed) {
+        this.historyId = historyId;
+        this.cookie = cookie;
+        this.nextTransaction = nextTransaction;
+        this.closed = closed;
+    }
+
+    public long getHistoryId() {
+        return historyId;
+    }
+
+    public long getCookie() {
+        return cookie;
+    }
+
+    public long getNextTransaction() {
+        return nextTransaction;
+    }
+
+    public boolean isClosed() {
+        return closed;
+    }
+
+    @Override
+    public void writeTo(final DataOutput out) throws IOException {
+        WritableObjects.writeLongs(out, historyId, cookie);
+        WritableObjects.writeLong(out, nextTransaction);
+        out.writeBoolean(closed);
+    }
+
+    public static FrontendHistoryMetadata readFrom(final DataInput in) throws IOException {
+        final byte header = WritableObjects.readLongHeader(in);
+        final long historyId = WritableObjects.readFirstLong(in, header);
+        final long cookie = WritableObjects.readSecondLong(in, header);
+        final long nextTransaction = WritableObjects.readLong(in);
+        final boolean closed = in.readBoolean();
+
+        return new FrontendHistoryMetadata(historyId, cookie, nextTransaction, closed);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(FrontendHistoryMetadata.class).add("historiId", historyId)
+                .add("cookie", cookie).add("nextTransaction", nextTransaction).add("closed", closed).toString();
+    }
+}
diff --git a/opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/persisted/FrontendShardDataTreeSnapshotMetadata.java b/opendaylight/md-sal/sal-distributed-datastore/src/main/java/org/opendaylight/controller/cluster/datastore/persisted/FrontendShardDataTreeSnapshotMetadata.java
new file mode 100644 (file)
index 0000000..55d9757
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.controller.cluster.datastore.persisted;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public final class FrontendShardDataTreeSnapshotMetadata extends
+        ShardDataTreeSnapshotMetadata<FrontendShardDataTreeSnapshotMetadata> {
+
+    private static final class Proxy implements Externalizable {
+        private static final long serialVersionUID = 1L;
+
+        private List<FrontendClientMetadata> clients;
+
+        public Proxy() {
+            // For Externalizable
+        }
+
+        Proxy(final FrontendShardDataTreeSnapshotMetadata metadata) {
+            this.clients = metadata.getClients();
+        }
+
+        @Override
+        public void writeExternal(final ObjectOutput out) throws IOException {
+            out.writeInt(clients.size());
+            for (final FrontendClientMetadata c : clients) {
+                c.writeTo(out);
+            }
+        }
+
+        @Override
+        public void readExternal(final ObjectInput in) throws IOException, ClassNotFoundException {
+            final int size = in.readInt();
+            final List<FrontendClientMetadata> readedClients = new ArrayList<>(size);
+            for (int i = 0; i < size ; ++i) {
+                readedClients.add(FrontendClientMetadata.readFrom(in));
+            }
+            this.clients = ImmutableList.copyOf(readedClients);
+        }
+
+        private Object readResolve() {
+            return new FrontendShardDataTreeSnapshotMetadata(clients);
+        }
+    }
+
+    private static final long serialVersionUID = 1L;
+
+    private final List<FrontendClientMetadata> clients;
+
+    public FrontendShardDataTreeSnapshotMetadata(final Collection<FrontendClientMetadata> clients) {
+        this.clients = ImmutableList.copyOf(clients);
+    }
+
+    public List<FrontendClientMetadata> getClients() {
+        return clients;
+    }
+
+    @Override
+    protected Externalizable externalizableProxy() {
+        return new Proxy(this);
+    }
+
+    @Override
+    public Class<FrontendShardDataTreeSnapshotMetadata> getType() {
+        return FrontendShardDataTreeSnapshotMetadata.class;
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(FrontendShardDataTreeSnapshotMetadata.class).add("clients", clients)
+                .toString();
+    }
+}
diff --git a/opendaylight/md-sal/sal-distributed-datastore/src/test/java/org/opendaylight/controller/cluster/datastore/persisted/FrontendShardDataTreeSnapshotMetadataTest.java b/opendaylight/md-sal/sal-distributed-datastore/src/test/java/org/opendaylight/controller/cluster/datastore/persisted/FrontendShardDataTreeSnapshotMetadataTest.java
new file mode 100644 (file)
index 0000000..f99cf6f
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.controller.cluster.datastore.persisted;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeSet;
+import com.google.common.primitives.UnsignedLong;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+import org.opendaylight.controller.cluster.access.concepts.ClientIdentifier;
+import org.opendaylight.controller.cluster.access.concepts.FrontendIdentifier;
+import org.opendaylight.controller.cluster.access.concepts.FrontendType;
+import org.opendaylight.controller.cluster.access.concepts.MemberName;
+
+public class FrontendShardDataTreeSnapshotMetadataTest {
+
+    @Test(expected = NullPointerException.class)
+    public final void testCreateMetadataSnapshotNullInput() {
+        new FrontendShardDataTreeSnapshotMetadata(null);
+    }
+
+    @Test
+    public final void testCreateMetadataSnapshotEmptyInput() throws Exception {
+        final FrontendShardDataTreeSnapshotMetadata emptyOrigSnapshot = createEmptyMetadataSnapshot();
+        final FrontendShardDataTreeSnapshotMetadata emptyCopySnapshot = copy(emptyOrigSnapshot);
+        testMetadataSnapshotEqual(emptyOrigSnapshot, emptyCopySnapshot);
+    }
+
+    @Test
+    public final void testSerializeMetadataSnapshotWithOneClient() throws Exception {
+        final FrontendShardDataTreeSnapshotMetadata origSnapshot = createMetadataSnapshot(1);
+        final FrontendShardDataTreeSnapshotMetadata copySnapshot = copy(origSnapshot);
+        testMetadataSnapshotEqual(origSnapshot, copySnapshot);
+    }
+
+    @Test
+    public final void testSerializeMetadataSnapshotWithMoreClients() throws Exception {
+        final FrontendShardDataTreeSnapshotMetadata origSnapshot = createMetadataSnapshot(5);
+        final FrontendShardDataTreeSnapshotMetadata copySnapshot = copy(origSnapshot);
+        testMetadataSnapshotEqual(origSnapshot, copySnapshot);
+    }
+
+    private static void testMetadataSnapshotEqual(final FrontendShardDataTreeSnapshotMetadata origSnapshot,
+            final FrontendShardDataTreeSnapshotMetadata copySnapshot) {
+
+        final List<FrontendClientMetadata> origClientList = origSnapshot.getClients();
+        final List<FrontendClientMetadata> copyClientList = copySnapshot.getClients();
+
+        assertTrue(origClientList.size() == copyClientList.size());
+
+        final Map<ClientIdentifier, FrontendClientMetadata> origIdent = new HashMap<>();
+        final Map<ClientIdentifier, FrontendClientMetadata> copyIdent = new HashMap<>();
+        origClientList.forEach(client -> origIdent.put(client.getIdentifier(), client));
+        origClientList.forEach(client -> copyIdent.put(client.getIdentifier(), client));
+
+        assertTrue(origIdent.keySet().containsAll(copyIdent.keySet()));
+        assertTrue(copyIdent.keySet().containsAll(origIdent.keySet()));
+
+        origIdent.values().forEach(client -> {
+            final FrontendClientMetadata copyClient = copyIdent.get(client.getIdentifier());
+            testObject(client.getIdentifier(), copyClient.getIdentifier());
+            assertTrue(client.getPurgedHistories().equals(copyClient.getPurgedHistories()));
+            assertTrue(client.getCurrentHistories().equals(copyClient.getCurrentHistories()));
+        });
+    }
+
+    private static FrontendShardDataTreeSnapshotMetadata createEmptyMetadataSnapshot() {
+        return new FrontendShardDataTreeSnapshotMetadata(Collections.<FrontendClientMetadata> emptyList());
+    }
+
+    private static FrontendShardDataTreeSnapshotMetadata createMetadataSnapshot(final int size) {
+        final List<FrontendClientMetadata> clients = new ArrayList<>();
+        for (long i = 0; i < size; i++) {
+            clients.add(createFrontedClientMetadata(i));
+        }
+        return new FrontendShardDataTreeSnapshotMetadata(clients);
+    }
+
+    private static FrontendClientMetadata createFrontedClientMetadata(final long i) {
+        final String index = String.valueOf(i);
+        final String indexName = "test_" + index;
+        final FrontendIdentifier frontendIdentifier = FrontendIdentifier.create(MemberName.forName(indexName),
+                FrontendType.forName(index));
+        final ClientIdentifier clientIdentifier = ClientIdentifier.create(frontendIdentifier, i);
+
+        final RangeSet<UnsignedLong> purgedHistories = TreeRangeSet.create();
+        purgedHistories.add(Range.closed(UnsignedLong.ZERO, UnsignedLong.ONE));
+
+        final Collection<FrontendHistoryMetadata> currentHistories = Collections
+                .singleton(new FrontendHistoryMetadata(i, i, i, true));
+
+        return new FrontendClientMetadata(clientIdentifier, purgedHistories, currentHistories);
+    }
+
+    private static final <T> void testObject(final T object, final T equalObject) {
+        assertEquals(object.hashCode(), equalObject.hashCode());
+        assertTrue(object.equals(object));
+        assertTrue(object.equals(equalObject));
+        assertFalse(object.equals(null));
+        assertFalse(object.equals("dummy"));
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> T copy(final T o) throws IOException, ClassNotFoundException {
+        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
+            oos.writeObject(o);
+        }
+
+        try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()))) {
+            return (T) ois.readObject();
+        }
+    }
+}