[ONOS-8085] Route Refresh timers in Controller

- Add timers in BgpController
- Add triggers in BgpLocalRib
- Code to send Route Refresh messages

Change-Id: Id45f1bcb3325ccc21c0d0d2e673c2b0097aac552
diff --git a/protocols/bgp/api/src/main/java/org/onosproject/bgp/controller/BgpController.java b/protocols/bgp/api/src/main/java/org/onosproject/bgp/controller/BgpController.java
index 4620d18..8030a57 100644
--- a/protocols/bgp/api/src/main/java/org/onosproject/bgp/controller/BgpController.java
+++ b/protocols/bgp/api/src/main/java/org/onosproject/bgp/controller/BgpController.java
@@ -221,4 +221,9 @@
      */
     Set<BgpRouteListener> routeListener();
 
+    /**
+     * Helper function to notify the controller if the topology has changed.
+     * Controller will decide if route-refresh needs to be triggered
+     */
+    void notifyTopologyChange();
 }
diff --git a/protocols/bgp/api/src/main/java/org/onosproject/bgp/controller/BgpPeer.java b/protocols/bgp/api/src/main/java/org/onosproject/bgp/controller/BgpPeer.java
index ab26173..5e4e532 100644
--- a/protocols/bgp/api/src/main/java/org/onosproject/bgp/controller/BgpPeer.java
+++ b/protocols/bgp/api/src/main/java/org/onosproject/bgp/controller/BgpPeer.java
@@ -155,4 +155,9 @@
     void updateEvpnNlri(FlowSpecOperation operType, IpAddress nextHop,
                         List<BgpValueType> extcommunity,
                         List<BgpEvpnNlri> evpnNlris);
+
+    /**
+     * Send the Route Refresh message to the connected BGP peer.
+     */
+    void sendRouteRefreshMessage();
 }
diff --git a/protocols/bgp/bgpio/src/main/java/org/onosproject/bgpio/util/Constants.java b/protocols/bgp/bgpio/src/main/java/org/onosproject/bgpio/util/Constants.java
index 4b6c6eb..7d62818 100644
--- a/protocols/bgp/bgpio/src/main/java/org/onosproject/bgpio/util/Constants.java
+++ b/protocols/bgp/bgpio/src/main/java/org/onosproject/bgpio/util/Constants.java
@@ -22,6 +22,8 @@
     private Constants() {
     }
 
+    public static final byte EMPTY = 0x00; //Empty byte
+
     public static final short TYPE_AND_LEN = 4;
     public static final short TYPE_AND_LEN_AS_SHORT = 4;
     public static final short TYPE_AND_LEN_AS_BYTE = 3;
diff --git a/protocols/bgp/ctl/src/main/java/org/onosproject/bgp/controller/impl/BgpControllerImpl.java b/protocols/bgp/ctl/src/main/java/org/onosproject/bgp/controller/impl/BgpControllerImpl.java
index e23b838..9a2df47 100644
--- a/protocols/bgp/ctl/src/main/java/org/onosproject/bgp/controller/impl/BgpControllerImpl.java
+++ b/protocols/bgp/ctl/src/main/java/org/onosproject/bgp/controller/impl/BgpControllerImpl.java
@@ -46,9 +46,16 @@
 import java.util.TreeMap;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
+import static org.onlab.util.Tools.groupedThreads;
+
 @Component(immediate = true, service = BgpController.class)
 public class BgpControllerImpl implements BgpController {
 
@@ -72,6 +79,19 @@
     private Map<String, List<String>> closedSessionExceptionMap = new TreeMap<>();
     protected Set<BgpRouteListener> bgpRouteListener = new CopyOnWriteArraySet<>();
 
+    //IDs for timers
+    private static final int PERIODIC_TIMER = 1001;
+    private static final int WARMUP_TIMER = 1002;
+    private static final int COOLDOWN_TIMER = 1003;
+
+    private static final int POOL_SIZE = 3; //Current pool size is 3
+    private ScheduledExecutorService executor;
+    private ScheduledFuture<?> cooldownFuture;
+    private ScheduledFuture<?> periodicFuture;
+    private ScheduledFuture<?> warmupFuture;
+
+    private AtomicBoolean hasTopologyChanged = new AtomicBoolean(false);
+
     @Override
     public void activeSessionExceptionAdd(String peerId, String exception) {
         if (peerId != null) {
@@ -131,6 +151,9 @@
     @Activate
     public void activate() {
         this.ctrl.start();
+        executor = Executors.newScheduledThreadPool(
+                        POOL_SIZE,
+                        groupedThreads("onos/apps/bgpcontroller", "bgp-rr-timer"));
         log.info("Started");
     }
 
@@ -141,6 +164,7 @@
         // Close all connected peers
         closeConnectedPeers();
         this.ctrl.stop();
+        executor.shutdown();
         log.info("Stopped");
     }
 
@@ -265,6 +289,17 @@
             } else {
                 this.log.debug("Added Peer {}", bgpId.toString());
                 connectedPeers.put(bgpId, bgpPeer);
+
+                //If all timers are stopped, start periodic timer
+                this.log.info("Start periodic timer");
+                if (bgpconfig.isRouteRefreshEnabled()
+                        && (periodicFuture == null || periodicFuture.isCancelled())
+                        && (cooldownFuture == null || cooldownFuture.isCancelled())
+                        && (warmupFuture == null || warmupFuture.isCancelled())) {
+                        periodicFuture = executor.schedule(periodicTimerTask,
+                            bgpconfig.getRouteRefreshPeriodicTimer(), TimeUnit.SECONDS);
+                }
+
                 return true;
             }
         }
@@ -383,4 +418,143 @@
     public Set<BgpPrefixListener> prefixListener() {
         return bgpPrefixListener;
     }
+
+    @Override
+    public void notifyTopologyChange() {
+        log.info("Topology change received");
+
+        hasTopologyChanged.set(true);
+
+        //If cooldown timer is running, do nothing further because routeRefresh will be sent when it expires
+        if (cooldownFuture != null && !cooldownFuture.isCancelled()) {
+            log.debug("Do nothing : Cooldown timer running");
+            return;
+        }
+
+        //If warmup timer is running, refresh it. If not, start it
+        if (warmupFuture != null && !warmupFuture.isCancelled()) {
+            warmupFuture.cancel(true);
+            warmupFuture = null;
+
+            warmupFuture = executor.schedule(warmupTimerTask,
+                    bgpconfig.getRouteRefreshWarmupTimer(), TimeUnit.SECONDS);
+
+            log.debug("Warmup timer running. Re-started warmup timer");
+            return;
+        } else {
+            warmupFuture = executor.schedule(warmupTimerTask,
+                    bgpconfig.getRouteRefreshWarmupTimer(), TimeUnit.SECONDS);
+            log.debug("Warmup timer started");
+            return;
+        }
+    }
+
+    protected void resetTimers() {
+        if (periodicFuture != null && !periodicFuture.isCancelled()) {
+            periodicFuture.cancel(true);
+            periodicFuture = null;
+        }
+
+        if (warmupFuture != null && !warmupFuture.isCancelled()) {
+            warmupFuture.cancel(true);
+            warmupFuture = null;
+        }
+
+        if (cooldownFuture != null && !cooldownFuture.isCancelled()) {
+            cooldownFuture.cancel(true);
+            cooldownFuture = null;
+        }
+    }
+
+    protected synchronized void timerCallback(int timerId) {
+        switch (timerId) {
+            case PERIODIC_TIMER:
+                //Cancel periodic timer and run cooldown timer
+                periodicFuture.cancel(true);
+                periodicFuture = null;
+
+                sendRouteRefreshToPeers();
+
+                //Cancel warmup timer if it is running
+                if (warmupFuture != null && !warmupFuture.isCancelled()) {
+                    warmupFuture.cancel(true);
+                    warmupFuture = null;
+                }
+
+                cooldownFuture = executor.schedule(cooldownTimerTask,
+                        bgpconfig.getRouteRefreshCooldownTimer(), TimeUnit.SECONDS);
+                log.debug("Cooldown timer started");
+                break;
+            case WARMUP_TIMER:
+                //Send route refresh and start cooldown timer
+                warmupFuture.cancel(true);
+                warmupFuture = null;
+
+                sendRouteRefreshToPeers();
+
+                cooldownFuture = executor.schedule(cooldownTimerTask,
+                        bgpconfig.getRouteRefreshCooldownTimer(), TimeUnit.SECONDS);
+                //Cancel periodic timer, if it is running
+                if (periodicFuture != null && !periodicFuture.isCancelled()) {
+                    periodicFuture.cancel(true);
+                    periodicFuture = null;
+                }
+                log.debug("Cooldown timer started");
+                break;
+            case COOLDOWN_TIMER:
+                //If hasTopologyChanged is true, we need to restart cooldown timer.
+                //Otherwise, start periodic timer
+                boolean hasTopologyChangedValue = hasTopologyChanged.get();
+
+                cooldownFuture.cancel(true);
+                cooldownFuture = null;
+
+                if (hasTopologyChangedValue) {
+                    sendRouteRefreshToPeers();
+                    cooldownFuture = executor.schedule(cooldownTimerTask,
+                            bgpconfig.getRouteRefreshCooldownTimer(), TimeUnit.SECONDS);
+                    log.debug("Cooldown timer started");
+                } else {
+                    periodicFuture = executor.schedule(periodicTimerTask,
+                            bgpconfig.getRouteRefreshPeriodicTimer(), TimeUnit.SECONDS);
+                    log.debug("Periodic timer started");
+                }
+                break;
+            default:
+                log.error("Invalid timerId in callback");
+        }
+
+    }
+
+    private synchronized void sendRouteRefreshToPeers() {
+        //Iterate over peers and send route refresh
+        connectedPeers.forEach((k, v) -> v.sendRouteRefreshMessage());
+
+        //Refresh hasTopologyChanged variable
+        hasTopologyChanged.set(false);
+    }
+
+    private Runnable periodicTimerTask = new Runnable() {
+        @Override
+        public void run() {
+            log.debug("Periodic Timer Expired");
+            timerCallback(PERIODIC_TIMER);
+        }
+    };
+
+    private Runnable cooldownTimerTask = new Runnable() {
+        @Override
+        public void run() {
+            log.info("Cooldown Timer Expired");
+            timerCallback(COOLDOWN_TIMER);
+        }
+    };
+
+    private Runnable warmupTimerTask = new Runnable() {
+        @Override
+        public void run() {
+            log.debug("Warmup Timer Expired");
+            timerCallback(WARMUP_TIMER);
+        }
+    };
 }
diff --git a/protocols/bgp/ctl/src/main/java/org/onosproject/bgp/controller/impl/BgpLocalRibImpl.java b/protocols/bgp/ctl/src/main/java/org/onosproject/bgp/controller/impl/BgpLocalRibImpl.java
index edbe75c..abea91c 100644
--- a/protocols/bgp/ctl/src/main/java/org/onosproject/bgp/controller/impl/BgpLocalRibImpl.java
+++ b/protocols/bgp/ctl/src/main/java/org/onosproject/bgp/controller/impl/BgpLocalRibImpl.java
@@ -153,7 +153,8 @@
                 for (BgpNodeListener l : bgpController.listener()) {
                     l.addNode((BgpNodeLSNlriVer4) nlri, details);
                 }
-                log.debug("Local RIB ad node: {}", detailsLocRib.toString());
+                bgpController.notifyTopologyChange();
+                log.debug("Local RIB add node: {}", detailsLocRib.toString());
             }
         } else if (nlri instanceof BgpLinkLsNlriVer4) {
             BgpLinkLSIdentifier linkLsIdentifier = ((BgpLinkLsNlriVer4) nlri).getLinkIdentifier();
@@ -173,6 +174,7 @@
                 for (BgpLinkListener l : bgpController.linkListener()) {
                     l.addLink((BgpLinkLsNlriVer4) nlri, details);
                 }
+                bgpController.notifyTopologyChange();
                 log.debug("Local RIB add link: {}", detailsLocRib.toString());
             }
         } else if (nlri instanceof BgpPrefixIPv4LSNlriVer4) {
@@ -314,6 +316,7 @@
                 l.deleteNode((BgpNodeLSNlriVer4) nlri);
             }
             nodeTree.remove(nodeLsIdentifier);
+            bgpController.notifyTopologyChange();
         }
     }
 
@@ -379,7 +382,7 @@
                 l.deleteLink((BgpLinkLsNlriVer4) nlri);
             }
             linkTree.remove(linkLsIdentifier);
-
+            bgpController.notifyTopologyChange();
         }
     }
 
diff --git a/protocols/bgp/ctl/src/main/java/org/onosproject/bgp/controller/impl/BgpPeerImpl.java b/protocols/bgp/ctl/src/main/java/org/onosproject/bgp/controller/impl/BgpPeerImpl.java
index 4dc8fee..d53bbc2 100644
--- a/protocols/bgp/ctl/src/main/java/org/onosproject/bgp/controller/impl/BgpPeerImpl.java
+++ b/protocols/bgp/ctl/src/main/java/org/onosproject/bgp/controller/impl/BgpPeerImpl.java
@@ -46,6 +46,7 @@
 import org.onosproject.bgpio.types.MpReachNlri;
 import org.onosproject.bgpio.types.MpUnReachNlri;
 import org.onosproject.bgpio.types.MultiProtocolExtnCapabilityTlv;
+import org.onosproject.bgpio.types.RouteRefreshCapabilityTlv;
 import org.onosproject.bgpio.types.Origin;
 import org.onosproject.bgpio.types.RpdCapabilityTlv;
 import org.onosproject.bgpio.types.attr.WideCommunity;
@@ -160,6 +161,19 @@
         return false;
     }
 
+    private final boolean isRouteRefreshSupported() {
+        List<BgpValueType> capabilities = sessionInfo.remoteBgpCapability();
+
+        for (BgpValueType currentCapability : capabilities) {
+            if (currentCapability instanceof RouteRefreshCapabilityTlv) {
+                //Presence of Reoute Refresh capability TLV means route refresh is supported
+                log.debug("Route Refresh is supported by peer");
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * Send flow specification update message to peer.
      *
@@ -320,6 +334,24 @@
     }
 
     @Override
+    public void sendRouteRefreshMessage() {
+        if (!isRouteRefreshSupported()) {
+            log.debug("Route Refresh not supported by peer, so cannot send message");
+            return;
+        }
+
+        BgpMessage msg = Controller.getBgpMessageFactory4()
+                .routeRefreshMsgBuilder()
+                .addAfiSafiValue(Constants.AFI_IPV6_UNICAST, Constants.EMPTY, Constants.SAFI_UNICAST)
+                .addAfiSafiValue(Constants.AFI_VALUE, Constants.EMPTY, Constants.SAFI_VALUE)
+                .build();
+
+        channel.write(Collections.singletonList(msg));
+
+        log.info("Route Refresh sent to {}", channelId);
+    }
+
+    @Override
     public void buildAdjRibIn(List<BgpValueType> pathAttr) throws BgpParseException {
         ListIterator<BgpValueType> iterator = pathAttr.listIterator();
         while (iterator.hasNext()) {
diff --git a/providers/bgp/topology/src/test/java/org/onosproject/provider/bgp/topology/impl/BgpControllerAdapter.java b/providers/bgp/topology/src/test/java/org/onosproject/provider/bgp/topology/impl/BgpControllerAdapter.java
index c1b53ed..8ba85bf 100644
--- a/providers/bgp/topology/src/test/java/org/onosproject/provider/bgp/topology/impl/BgpControllerAdapter.java
+++ b/providers/bgp/topology/src/test/java/org/onosproject/provider/bgp/topology/impl/BgpControllerAdapter.java
@@ -181,4 +181,9 @@
     public void removePrefixListener(BgpPrefixListener listener) {
         // TODO Auto-generated method stub
     }
+
+    @Override
+    public void notifyTopologyChange() {
+        // TODO Auto-generated method stub
+    }
 }