SDFAB-104 Support routing via next hop in single leaf pair topology

The original design principle we adopted while implementing dual-homing is only recovering local failure using pair link.
Routing via next hop is a global thing recovered by updating ECMP hashing.
However, there is no spine in the single leaf pair setup so we need additional logic to recover this using pair link.

Change-Id: I3d648b139038be69656dd86b4c40d12bf10f50b2
diff --git a/impl/src/main/java/org/onosproject/segmentrouting/RouteHandler.java b/impl/src/main/java/org/onosproject/segmentrouting/RouteHandler.java
index c0ccfb6..dc1a25a 100644
--- a/impl/src/main/java/org/onosproject/segmentrouting/RouteHandler.java
+++ b/impl/src/main/java/org/onosproject/segmentrouting/RouteHandler.java
@@ -23,13 +23,13 @@
 import org.onlab.packet.VlanId;
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.Host;
-import org.onosproject.net.HostLocation;
 import org.onosproject.net.PortNumber;
 import org.onosproject.net.host.HostEvent;
 import org.onosproject.routeservice.ResolvedRoute;
 import org.onosproject.routeservice.RouteEvent;
 import org.onosproject.net.DeviceId;
 import org.onosproject.routeservice.RouteInfo;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -88,9 +88,9 @@
             return;
         }
 
-        ResolvedRoute rr = routes.stream().findFirst().orElse(null);
-        if (rr == null) {
+        if (routes.isEmpty()) {
             log.warn("No resolved route found. Abort processRouteAddedInternal");
+            return;
         }
 
         Set<ConnectPoint> allLocations = Sets.newHashSet();
@@ -102,7 +102,6 @@
         log.debug("RouteAdded. populateSubnet {}, {}", allLocations, allPrefixes);
         srManager.defaultRoutingHandler.populateSubnet(allLocations, allPrefixes);
 
-
         routes.forEach(route -> {
             IpPrefix prefix = route.prefix();
             MacAddress nextHopMac = route.nextHopMac();
@@ -115,6 +114,8 @@
                 log.debug("RouteAdded populateRoute {}, {}, {}, {}", location, prefix, nextHopMac, nextHopVlan);
                 srManager.defaultRoutingHandler.populateRoute(location.deviceId(), prefix,
                         nextHopMac, nextHopVlan, location.port(), false);
+
+                processSingleLeafPairIfNeeded(locations, location, prefix, nextHopVlan);
             });
         });
     }
@@ -189,6 +190,8 @@
                 log.debug("RouteUpdated. populateRoute {}, {}, {}, {}", location, prefix, nextHopMac, nextHopVlan);
                 srManager.defaultRoutingHandler.populateRoute(location.deviceId(), prefix,
                         nextHopMac, nextHopVlan, location.port(), false);
+
+                processSingleLeafPairIfNeeded(locations, location, prefix, nextHopVlan);
             });
         });
     }
@@ -223,19 +226,7 @@
                 srManager.deviceConfiguration.removeSubnet(location, prefix);
                 // We don't need to call revokeRoute again since revokeSubnet will remove the prefix
                 // from all devices, including the ones that next hop attaches to.
-
-                // Also remove redirection flows on the pair device if exists.
-                Optional<DeviceId> pairDeviceId = srManager.getPairDeviceId(location.deviceId());
-                Optional<PortNumber> pairLocalPort = srManager.getPairLocalPort(location.deviceId());
-                if (pairDeviceId.isPresent() && pairLocalPort.isPresent()) {
-                    // NOTE: Since the pairLocalPort is trunk port, use assigned vlan of original port
-                    //       when the host is untagged
-                    VlanId vlanId = Optional.ofNullable(srManager.getInternalVlanId(location)).orElse(nextHopVlan);
-
-                    log.debug("RouteRemoved. revokeRoute {}, {}, {}, {}", location, prefix, nextHopMac, nextHopVlan);
-                    srManager.defaultRoutingHandler.revokeRoute(pairDeviceId.get(), prefix,
-                            nextHopMac, vlanId, pairLocalPort.get(), false);
-                }
+                // revokeSubnet will also remove flow on the pair device (if exist) pointing to current location.
             });
         });
     }
@@ -245,19 +236,21 @@
         MacAddress hostMac = event.subject().mac();
         VlanId hostVlanId = event.subject().vlan();
 
-        Set<HostLocation> prevLocations = event.prevSubject().locations();
-        Set<HostLocation> newLocations = event.subject().locations();
-        Set<ConnectPoint> connectPoints = newLocations.stream()
-                .map(l -> (ConnectPoint) l).collect(Collectors.toSet());
+        Set<ConnectPoint> prevLocations = event.prevSubject().locations()
+                .stream().map(h -> (ConnectPoint) h)
+                .collect(Collectors.toSet());
+        Set<ConnectPoint> newLocations = event.subject().locations()
+                .stream().map(h -> (ConnectPoint) h)
+                .collect(Collectors.toSet());
         List<Set<IpPrefix>> batchedSubnets =
                 srManager.deviceConfiguration.getBatchedSubnets(event.subject().id());
-        Set<DeviceId> newDeviceIds = newLocations.stream().map(HostLocation::deviceId)
+        Set<DeviceId> newDeviceIds = newLocations.stream().map(ConnectPoint::deviceId)
                 .collect(Collectors.toSet());
 
         // Set of deviceIDs of the previous locations where the host was connected
         // Used to determine if host moved to different connect points
         // on same device or moved to a different device altogether
-        Set<DeviceId> oldDeviceIds = prevLocations.stream().map(HostLocation::deviceId)
+        Set<DeviceId> oldDeviceIds = prevLocations.stream().map(ConnectPoint::deviceId)
                 .collect(Collectors.toSet());
 
         // L3 Ucast bucket needs to be updated only once per host
@@ -288,7 +281,7 @@
 
         batchedSubnets.forEach(subnets -> {
             log.debug("HostMoved. populateSubnet {}, {}", newLocations, subnets);
-            srManager.defaultRoutingHandler.populateSubnet(connectPoints, subnets);
+            srManager.defaultRoutingHandler.populateSubnet(newLocations, subnets);
 
             subnets.forEach(prefix -> {
                 // For each old location
@@ -304,7 +297,10 @@
                     srManager.deviceConfiguration.removeSubnet(prevLocation, prefix);
 
                     // Do not remove flow from a device if the route is still reachable via its pair device.
-                    // populateSubnet will update the flow to point to its pair device via spine.
+                    // If spine exists,
+                    //     populateSubnet above will update the flow to point to its pair device via spine.
+                    // If spine does not exist,
+                    //     processSingleLeafPair below will update the flow to point to its pair device via pair port.
                     DeviceId pairDeviceId = srManager.getPairDeviceId(prevLocation.deviceId()).orElse(null);
                     if (newLocations.stream().anyMatch(n -> n.deviceId().equals(pairDeviceId))) {
                         return;
@@ -327,6 +323,10 @@
                               hostMac, hostVlanId, newLocation.port(), false);
                     }
                 });
+
+                newLocations.forEach(location -> {
+                    processSingleLeafPairIfNeeded(newLocations, location, prefix, hostVlanId);
+                });
             });
         });
 
@@ -370,4 +370,45 @@
         return Objects.nonNull(srManager.deviceConfiguration) &&
                 Objects.nonNull(srManager.defaultRoutingHandler);
     }
+
+    protected boolean processSingleLeafPairIfNeeded(Set<ConnectPoint> locations, ConnectPoint location, IpPrefix prefix,
+                                                    VlanId nextHopVlan) {
+        // Special handling for single leaf pair
+        if (!srManager.getInfraDeviceIds().isEmpty()) {
+            log.debug("Spine found. Skip single leaf pair handling");
+            return false;
+        }
+        Optional<DeviceId> pairDeviceId = srManager.getPairDeviceId(location.deviceId());
+        if (pairDeviceId.isEmpty()) {
+            log.debug("Pair device of {} not found", location.deviceId());
+            return false;
+        }
+        if (locations.stream().anyMatch(l -> l.deviceId().equals(pairDeviceId.get()))) {
+            log.debug("Pair device has a next hop available. Leave it as is.");
+            return false;
+        }
+        Optional<PortNumber> pairRemotePort = srManager.getPairLocalPort(pairDeviceId.get());
+        if (pairRemotePort.isEmpty()) {
+            log.debug("Pair remote port of {} not found", pairDeviceId.get());
+            return false;
+        }
+        // Use routerMac of the pair device as the next hop
+        MacAddress effectiveMac;
+        try {
+            effectiveMac = srManager.getDeviceMacAddress(location.deviceId());
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn("Abort populateRoute on pair device {}. routerMac not found", pairDeviceId);
+            return false;
+        }
+        // Since the pairLocalPort is trunk port, use assigned vlan of original port
+        // when the host is untagged
+        VlanId effectiveVlan = Optional.ofNullable(srManager.getInternalVlanId(location)).orElse(nextHopVlan);
+
+        log.debug("Single leaf pair. populateRoute {}/{}, {}, {}, {}", pairDeviceId, pairRemotePort, prefix,
+                effectiveMac, effectiveVlan);
+        srManager.defaultRoutingHandler.populateRoute(pairDeviceId.get(), prefix,
+                effectiveMac, effectiveVlan, pairRemotePort.get(), false);
+
+        return true;
+    }
 }
diff --git a/impl/src/test/java/org/onosproject/segmentrouting/HostHandlerTest.java b/impl/src/test/java/org/onosproject/segmentrouting/HostHandlerTest.java
index 45d2b4d..e9cde67 100644
--- a/impl/src/test/java/org/onosproject/segmentrouting/HostHandlerTest.java
+++ b/impl/src/test/java/org/onosproject/segmentrouting/HostHandlerTest.java
@@ -231,7 +231,7 @@
         mockNetworkConfigRegistry.applyConfig(dev4Config);
 
         // Initialize Segment Routing Manager
-        SegmentRoutingManager srManager = new MockSegmentRoutingManager(NEXT_TABLE);
+        SegmentRoutingManager srManager = new MockSegmentRoutingManager(NEXT_TABLE, Maps.newHashMap());
         srManager.storageService = createMock(StorageService.class);
         expect(srManager.storageService.consistentMapBuilder()).andReturn(new TestConsistentMap.Builder<>()).anyTimes();
         expect(srManager.storageService.consistentMultimapBuilder()).andReturn(
diff --git a/impl/src/test/java/org/onosproject/segmentrouting/MockSegmentRoutingManager.java b/impl/src/test/java/org/onosproject/segmentrouting/MockSegmentRoutingManager.java
index ad098b4..fdd3951 100644
--- a/impl/src/test/java/org/onosproject/segmentrouting/MockSegmentRoutingManager.java
+++ b/impl/src/test/java/org/onosproject/segmentrouting/MockSegmentRoutingManager.java
@@ -16,12 +16,14 @@
 
 package org.onosproject.segmentrouting;
 
+import org.onlab.packet.MacAddress;
 import org.onosproject.core.DefaultApplicationId;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.PortNumber;
 import org.onosproject.net.flow.TrafficSelector;
 import org.onosproject.net.flow.TrafficTreatment;
 
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -30,11 +32,16 @@
  */
 public class MockSegmentRoutingManager extends SegmentRoutingManager {
     private Map<Integer, TrafficTreatment> nextTable;
+    private Map<DeviceId, MacAddress> routerMacs;
+    private List<DeviceId> infraDeviceIds;
     private AtomicInteger atomicNextId = new AtomicInteger();
 
-    MockSegmentRoutingManager(Map<Integer, TrafficTreatment> nextTable) {
+    MockSegmentRoutingManager(Map<Integer, TrafficTreatment> nextTable,
+                              Map<DeviceId, MacAddress> routerMacs) {
         appId = new DefaultApplicationId(1, SegmentRoutingManager.APP_NAME);
         this.nextTable = nextTable;
+        this.routerMacs = routerMacs;
+        this.infraDeviceIds = List.of(DeviceId.deviceId("device:1"));
     }
 
     @Override
@@ -46,4 +53,18 @@
         nextTable.put(nextId, treatment);
         return nextId;
     }
+
+    @Override
+    public List<DeviceId> getInfraDeviceIds() {
+        return List.copyOf(infraDeviceIds);
+    }
+
+    public void setInfraDeviceIds(List<DeviceId> infraDeviceIds) {
+        this.infraDeviceIds = infraDeviceIds;
+    }
+
+    @Override
+    public MacAddress getDeviceMacAddress(DeviceId deviceId) {
+        return routerMacs.get(deviceId);
+    }
 }
diff --git a/impl/src/test/java/org/onosproject/segmentrouting/RouteHandlerTest.java b/impl/src/test/java/org/onosproject/segmentrouting/RouteHandlerTest.java
index 690f87e..e09aaa7 100644
--- a/impl/src/test/java/org/onosproject/segmentrouting/RouteHandlerTest.java
+++ b/impl/src/test/java/org/onosproject/segmentrouting/RouteHandlerTest.java
@@ -51,6 +51,7 @@
 import org.onosproject.store.service.TestConsistentMap;
 import org.onosproject.store.service.TestConsistentMultimap;
 
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -66,7 +67,7 @@
  * Unit test for {@link RouteHandler}.
  */
 public class RouteHandlerTest {
-    private SegmentRoutingManager srManager;
+    private MockSegmentRoutingManager srManager;
     private RouteHandler routeHandler;
     private HostService hostService;
 
@@ -149,6 +150,14 @@
             null, null, null, null, null);
     private static final Set<Interface> INTERFACES = Sets.newHashSet(IF_CP1, IF_CP2);
 
+    // Route MAC
+    private static final MacAddress ROUTER_MAC_1 = MacAddress.valueOf("00:AA:00:00:00:01");
+    private static final MacAddress ROUTER_MAC_2 = MacAddress.valueOf("00:AA:00:00:00:01");
+    private static final Map<DeviceId, MacAddress> ROUTER_MACS = Map.of(
+            CP1.deviceId(), ROUTER_MAC_1,
+            CP2.deviceId(), ROUTER_MAC_2
+    );
+
     @Before
     public void setUp() {
         ObjectMapper mapper = new ObjectMapper();
@@ -169,7 +178,7 @@
         mockNetworkConfigRegistry.applyConfig(dev2Config);
 
         // Initialize Segment Routing Manager
-        srManager = new MockSegmentRoutingManager(NEXT_TABLE);
+        srManager = new MockSegmentRoutingManager(NEXT_TABLE, ROUTER_MACS);
         srManager.storageService = createMock(StorageService.class);
         expect(srManager.storageService.consistentMapBuilder()).andReturn(new TestConsistentMap.Builder<>()).anyTimes();
         expect(srManager.storageService.consistentMultimapBuilder()).andReturn(
@@ -249,6 +258,31 @@
         assertEquals(1, SUBNET_TABLE.size());
     }
 
+    // Only one of two dual-homed next hops present.
+    // Expect one routing table to be programmed with direct flow.
+    // The other is pointing to pair port
+    @Test
+    public void initDhcpRouteSingleDualHomeNextHopSingleLeafPair() {
+        srManager.setInfraDeviceIds(List.of());
+
+        ROUTE_STORE.put(P1, Sets.newHashSet(DHCP_RR1));
+
+        routeHandler.init(CP1.deviceId());
+
+        assertEquals(2, ROUTING_TABLE.size());
+        MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+        assertEquals(M1, rtv1.macAddress);
+        assertEquals(V1, rtv1.vlanId);
+        assertEquals(CP1.port(), rtv1.portNumber);
+
+        MockRoutingTableValue rtv2 = ROUTING_TABLE.get(new MockRoutingTableKey(CP2.deviceId(), P1));
+        assertEquals(ROUTER_MAC_1, rtv2.macAddress);
+        assertEquals(V1, rtv2.vlanId);
+        assertEquals(P9, rtv2.portNumber);
+
+        assertEquals(1, SUBNET_TABLE.size());
+    }
+
     // Both dual-homed next hops present.
     // Expect both routing table to be programmed with direct flow
     @Test
@@ -272,6 +306,29 @@
     }
 
     @Test
+    public void initDhcpRouteBothDualHomeNextHopSingleLeafPair() {
+        srManager.setInfraDeviceIds(List.of());
+
+        ROUTE_STORE.put(P1, Sets.newHashSet(DHCP_RR3));
+
+        routeHandler.init(CP1.deviceId());
+
+        assertEquals(2, ROUTING_TABLE.size());
+        MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+        assertEquals(M3, rtv1.macAddress);
+        assertEquals(V3, rtv1.vlanId);
+        assertEquals(CP1.port(), rtv1.portNumber);
+
+        MockRoutingTableValue rtv2 = ROUTING_TABLE.get(new MockRoutingTableKey(CP2.deviceId(), P1));
+        assertEquals(M3, rtv2.macAddress);
+        assertEquals(V3, rtv2.vlanId);
+        assertEquals(CP2.port(), rtv2.portNumber);
+
+        assertEquals(2, SUBNET_TABLE.size());
+    }
+
+
+    @Test
     public void processRouteAdded() {
         reset(srManager.deviceConfiguration);
         srManager.deviceConfiguration.addSubnet(CP1, P1);
@@ -399,6 +456,37 @@
     }
 
     @Test
+    public void testOneDualHomedAddedSingleLeafPair() {
+        srManager.setInfraDeviceIds(List.of());
+
+        reset(srManager.deviceConfiguration);
+        srManager.deviceConfiguration.addSubnet(CP1, P1);
+        expectLastCall().once();
+        srManager.deviceConfiguration.addSubnet(CP2, P1);
+        expectLastCall().once();
+        replay(srManager.deviceConfiguration);
+
+        RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_ADDED, RR3, Sets.newHashSet(RR3));
+        routeHandler.processRouteAdded(re);
+
+        assertEquals(2, ROUTING_TABLE.size());
+        MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+        MockRoutingTableValue rtv2 = ROUTING_TABLE.get(new MockRoutingTableKey(CP2.deviceId(), P1));
+        assertEquals(M3, rtv1.macAddress);
+        assertEquals(M3, rtv2.macAddress);
+        assertEquals(V3, rtv1.vlanId);
+        assertEquals(V3, rtv2.vlanId);
+        assertEquals(CP1.port(), rtv1.portNumber);
+        assertEquals(CP2.port(), rtv2.portNumber);
+
+        assertEquals(2, SUBNET_TABLE.size());
+        assertTrue(SUBNET_TABLE.get(CP1).contains(P1));
+        assertTrue(SUBNET_TABLE.get(CP2).contains(P1));
+
+        verify(srManager.deviceConfiguration);
+    }
+
+    @Test
     public void testOneSingleHomedToTwoSingleHomed() {
         processRouteAdded();
 
@@ -480,6 +568,48 @@
         assertEquals(V3, rtv1.vlanId);
         assertEquals(CP1.port(), rtv1.portNumber);
 
+        MockRoutingTableValue rtv2 = ROUTING_TABLE.get(new MockRoutingTableKey(CP2.deviceId(), P1));
+        assertEquals(M3, rtv2.macAddress);
+        assertEquals(V3, rtv2.vlanId);
+        assertEquals(CP2.port(), rtv2.portNumber);
+
+        // ECMP route table hasn't changed
+        assertEquals(1, SUBNET_TABLE.size());
+        assertTrue(SUBNET_TABLE.get(CP1).contains(P1));
+
+        verify(srManager.deviceConfiguration);
+    }
+
+    @Test
+    public void testDualHomedSingleLocationFailSingleLeafPair() {
+        srManager.setInfraDeviceIds(List.of());
+
+        testOneDualHomedAddedSingleLeafPair();
+
+        ROUTE_STORE.put(P1, Sets.newHashSet(RR3));
+
+        reset(srManager.deviceConfiguration);
+        expect(srManager.deviceConfiguration.getBatchedSubnets(H3D.id()))
+                .andReturn(Lists.<Set<IpPrefix>>newArrayList(Sets.newHashSet(P1)));
+        srManager.deviceConfiguration.removeSubnet(CP2, P1);
+        expectLastCall().once();
+        replay(srManager.deviceConfiguration);
+
+        HostEvent he = new HostEvent(HostEvent.Type.HOST_MOVED, H3S, H3D);
+        routeHandler.processHostMovedEvent(he);
+
+        // We do not remove the route on CP2. Instead, we let the subnet population overrides it
+        assertEquals(2, ROUTING_TABLE.size());
+        MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+        assertEquals(M3, rtv1.macAddress);
+        assertEquals(V3, rtv1.vlanId);
+        assertEquals(CP1.port(), rtv1.portNumber);
+
+        MockRoutingTableValue rtv2 = ROUTING_TABLE.get(new MockRoutingTableKey(CP2.deviceId(), P1));
+        assertEquals(ROUTER_MAC_1, rtv2.macAddress);
+        assertEquals(V3, rtv2.vlanId);
+        assertEquals(P9, rtv2.portNumber);
+
         // ECMP route table hasn't changed
         assertEquals(1, SUBNET_TABLE.size());
         assertTrue(SUBNET_TABLE.get(CP1).contains(P1));
@@ -510,6 +640,30 @@
     }
 
     @Test
+    public void testDualHomedBothLocationFaiSingleLeafPair() {
+        srManager.setInfraDeviceIds(List.of());
+
+        testDualHomedSingleLocationFailSingleLeafPair();
+
+        hostService = new MockHostService(HOSTS_ONE_FAIL);
+
+        reset(srManager.deviceConfiguration);
+        srManager.deviceConfiguration.removeSubnet(CP1, P1);
+        expectLastCall().once();
+        srManager.deviceConfiguration.removeSubnet(CP2, P1);
+        expectLastCall().once();
+        replay(srManager.deviceConfiguration);
+
+        RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_REMOVED, RR3, Sets.newHashSet(RR3));
+        routeHandler.processRouteRemoved(re);
+
+        assertEquals(0, ROUTING_TABLE.size());
+        assertEquals(0, SUBNET_TABLE.size());
+
+        verify(srManager.deviceConfiguration);
+    }
+
+    @Test
     public void testSingleHomedToDualHomed() {
         testDualHomedSingleLocationFail();
 
@@ -541,6 +695,39 @@
     }
 
     @Test
+    public void testSingleHomedToDualHomedSingleLeafPair() {
+        srManager.setInfraDeviceIds(List.of());
+
+        testDualHomedSingleLocationFailSingleLeafPair();
+
+        reset(srManager.deviceConfiguration);
+        expect(srManager.deviceConfiguration.getBatchedSubnets(H3S.id()))
+                .andReturn(Lists.<Set<IpPrefix>>newArrayList(Sets.newHashSet(P1)));
+        srManager.deviceConfiguration.addSubnet(CP2, P1);
+        expectLastCall().once();
+        replay(srManager.deviceConfiguration);
+
+        HostEvent he = new HostEvent(HostEvent.Type.HOST_MOVED, H3D, H3S);
+        routeHandler.processHostMovedEvent(he);
+
+        assertEquals(2, ROUTING_TABLE.size());
+        MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+        MockRoutingTableValue rtv2 = ROUTING_TABLE.get(new MockRoutingTableKey(CP2.deviceId(), P1));
+        assertEquals(M3, rtv1.macAddress);
+        assertEquals(M3, rtv2.macAddress);
+        assertEquals(V3, rtv1.vlanId);
+        assertEquals(V3, rtv2.vlanId);
+        assertEquals(CP1.port(), rtv1.portNumber);
+        assertEquals(CP2.port(), rtv2.portNumber);
+
+        assertEquals(2, SUBNET_TABLE.size());
+        assertTrue(SUBNET_TABLE.get(CP1).contains(P1));
+        assertTrue(SUBNET_TABLE.get(CP2).contains(P1));
+
+        verify(srManager.deviceConfiguration);
+    }
+
+    @Test
     public void testTwoSingleHomedRemoved() {
         testTwoSingleHomedAdded();
 
@@ -583,6 +770,28 @@
     }
 
     @Test
+    public void testOneDualHomeRemovedSingleLeafPair() {
+        srManager.setInfraDeviceIds(List.of());
+
+        testOneDualHomedAddedSingleLeafPair();
+
+        reset(srManager.deviceConfiguration);
+        srManager.deviceConfiguration.removeSubnet(CP1, P1);
+        expectLastCall().once();
+        srManager.deviceConfiguration.removeSubnet(CP2, P1);
+        expectLastCall().once();
+        replay(srManager.deviceConfiguration);
+
+        RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_REMOVED, RR3, Sets.newHashSet(RR3));
+        routeHandler.processRouteRemoved(re);
+
+        assertEquals(0, ROUTING_TABLE.size());
+        assertEquals(0, SUBNET_TABLE.size());
+
+        verify(srManager.deviceConfiguration);
+    }
+
+    @Test
     public void testMoreThanTwoNextHop() {
         // next hop = CP1, CP2
         reset(srManager.deviceConfiguration);
diff --git a/impl/src/test/java/org/onosproject/segmentrouting/RoutingRulePopulatorTest.java b/impl/src/test/java/org/onosproject/segmentrouting/RoutingRulePopulatorTest.java
index 7250ef3..9ac553d 100644
--- a/impl/src/test/java/org/onosproject/segmentrouting/RoutingRulePopulatorTest.java
+++ b/impl/src/test/java/org/onosproject/segmentrouting/RoutingRulePopulatorTest.java
@@ -75,7 +75,7 @@
         Set<Interface> interfaces = Sets.newHashSet(u10, t10, t10n20);
         interfaceService = new MockInterfaceService(interfaces);
         deviceService = EasyMock.createMock(DeviceService.class);
-        srManager = new MockSegmentRoutingManager(Maps.newHashMap());
+        srManager = new MockSegmentRoutingManager(Maps.newHashMap(), Maps.newHashMap());
         srManager.deviceConfiguration =  EasyMock.createMock(DeviceConfiguration.class);
         srManager.interfaceService = interfaceService;
         srManager.deviceService = deviceService;