ONOS-1326: Added support for observing when node liveness status was last updated. Useful for detecting/debugging stability issues.

Change-Id: I8ffebcf3a09a51c6e3e7526986a0f05530ed757f
diff --git a/core/store/dist/src/main/java/org/onosproject/store/cluster/impl/HazelcastClusterStore.java b/core/store/dist/src/main/java/org/onosproject/store/cluster/impl/HazelcastClusterStore.java
index d32f7a8..55d877d 100644
--- a/core/store/dist/src/main/java/org/onosproject/store/cluster/impl/HazelcastClusterStore.java
+++ b/core/store/dist/src/main/java/org/onosproject/store/cluster/impl/HazelcastClusterStore.java
@@ -18,6 +18,7 @@
 import com.google.common.base.Optional;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
 import com.hazelcast.core.IMap;
 import com.hazelcast.core.Member;
 import com.hazelcast.core.MemberAttributeEvent;
@@ -28,6 +29,7 @@
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
 import org.apache.felix.scr.annotations.Service;
+import org.joda.time.DateTime;
 import org.onosproject.cluster.ClusterEvent;
 import org.onosproject.cluster.ClusterStore;
 import org.onosproject.cluster.ClusterStoreDelegate;
@@ -63,6 +65,7 @@
     private String listenerId;
     private final MembershipListener listener = new InternalMembershipListener();
     private final Map<NodeId, State> states = new ConcurrentHashMap<>();
+    private final Map<NodeId, DateTime> lastUpdatedTimes = Maps.newConcurrentMap();
 
     private String nodesListenerId;
 
@@ -123,6 +126,11 @@
     }
 
     @Override
+    public DateTime getLastUpdated(NodeId nodeId) {
+        return lastUpdatedTimes.get(nodeId);
+    }
+
+    @Override
     public ControllerNode addNode(NodeId nodeId, IpAddress ip, int tcpPort) {
         return addNode(new DefaultControllerNode(nodeId, ip, tcpPort));
     }
@@ -139,7 +147,7 @@
     private synchronized ControllerNode addNode(DefaultControllerNode node) {
         rawNodes.put(serialize(node.id()), serialize(node));
         nodes.put(node.id(), Optional.of(node));
-        states.put(node.id(), State.ACTIVE);
+        updateState(node.id(), State.ACTIVE);
         return node;
     }
 
@@ -153,6 +161,11 @@
         return IpAddress.valueOf(member.getSocketAddress().getAddress());
     }
 
+    private void updateState(NodeId nodeId, State newState) {
+        updateState(nodeId, newState);
+        lastUpdatedTimes.put(nodeId, DateTime.now());
+    }
+
     // Interceptor for membership events.
     private class InternalMembershipListener implements MembershipListener {
         @Override
@@ -166,7 +179,7 @@
         public void memberRemoved(MembershipEvent membershipEvent) {
             log.info("Member {} removed", membershipEvent.getMember());
             NodeId nodeId = new NodeId(memberAddress(membershipEvent.getMember()).toString());
-            states.put(nodeId, State.INACTIVE);
+            updateState(nodeId, State.INACTIVE);
             notifyDelegate(new ClusterEvent(INSTANCE_DEACTIVATED, getNode(nodeId)));
         }
 
@@ -178,4 +191,4 @@
                      memberAttributeEvent.getValue());
         }
     }
-}
+}
\ No newline at end of file