[CORD-555][CORD-966] Support add/remove multiple buckets for group

Change-Id: I496838e4ba6387c7c43264d3077bc7a760b3b372
diff --git a/drivers/default/src/main/java/org/onosproject/driver/pipeline/Ofdpa2GroupHandler.java b/drivers/default/src/main/java/org/onosproject/driver/pipeline/Ofdpa2GroupHandler.java
index 9e3161a..27fc791 100644
--- a/drivers/default/src/main/java/org/onosproject/driver/pipeline/Ofdpa2GroupHandler.java
+++ b/drivers/default/src/main/java/org/onosproject/driver/pipeline/Ofdpa2GroupHandler.java
@@ -21,6 +21,7 @@
 import com.google.common.cache.RemovalNotification;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
 import org.onlab.osgi.ServiceDirectory;
 import org.onlab.packet.IpPrefix;
 import org.onlab.packet.MacAddress;
@@ -28,6 +29,7 @@
 import org.onlab.packet.VlanId;
 import org.onosproject.core.ApplicationId;
 import org.onosproject.core.DefaultGroupId;
+import org.onosproject.core.GroupId;
 import org.onosproject.driver.extensions.OfdpaSetVlanVid;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.PortNumber;
@@ -65,7 +67,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Deque;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
@@ -84,6 +85,8 @@
 import static org.onosproject.net.flow.criteria.Criterion.Type.TUNNEL_ID;
 import static org.onosproject.net.flow.criteria.Criterion.Type.VLAN_VID;
 import static org.onosproject.net.flowobjective.NextObjective.Type.HASHED;
+import static org.onosproject.net.group.GroupDescription.Type.ALL;
+import static org.onosproject.net.group.GroupDescription.Type.SELECT;
 import static org.slf4j.LoggerFactory.getLogger;
 
 /**
@@ -119,6 +122,8 @@
     protected static final int PORT_LOWER_BITS_MASK = 0x3f;
     protected static final long PORT_HIGHER_BITS_MASK = ~PORT_LOWER_BITS_MASK;
 
+    protected static final String HEX_PREFIX = "0x";
+
     private final Logger log = getLogger(getClass());
     private ServiceDirectory serviceDirectory;
     protected GroupService groupService;
@@ -129,6 +134,7 @@
     private Cache<GroupKey, List<OfdpaNextGroup>> pendingAddNextObjectives;
     private Cache<NextObjective, List<GroupKey>> pendingRemoveNextObjectives;
     private ConcurrentHashMap<GroupKey, Set<GroupChainElem>> pendingGroups;
+    private ConcurrentHashMap<GroupKey, Set<NextObjective>> pendingUpdateNextObjectives;
     private ScheduledExecutorService groupChecker =
             Executors.newScheduledThreadPool(2, groupedThreads("onos/pipeliner", "ofdpa2-%d", log));
 
@@ -187,6 +193,7 @@
                     }
                 }).build();
         pendingGroups = new ConcurrentHashMap<>();
+        pendingUpdateNextObjectives = new ConcurrentHashMap<>();
         groupChecker.scheduleAtFixedRate(new GroupChecker(), 0, 500, TimeUnit.MILLISECONDS);
 
         groupService.addListener(new InnerGroupListener());
@@ -416,8 +423,8 @@
         VlanId vlanid = null;
         long portNum = 0;
         boolean setVlan = false, popVlan = false;
-        MacAddress srcMac = MacAddress.ZERO;
-        MacAddress dstMac = MacAddress.ZERO;
+        MacAddress srcMac;
+        MacAddress dstMac;
         for (Instruction ins : treatment.allInstructions()) {
             if (ins.type() == Instruction.Type.L2MODIFICATION) {
                 L2ModificationInstruction l2ins = (L2ModificationInstruction) ins;
@@ -683,20 +690,14 @@
                 new DefaultGroupKey(Ofdpa2Pipeline.appKryo.serialize(l2floodgk));
 
         // collection of group buckets pointing to all the l2 interface groups
-        List<GroupBucket> l2floodBuckets = Lists.newArrayList();
-        groupInfos.forEach(groupInfo -> {
-            GroupDescription l2intGrpDesc = groupInfo.nextGroupDesc;
-            TrafficTreatment.Builder ttb = DefaultTrafficTreatment.builder();
-            ttb.group(new DefaultGroupId(l2intGrpDesc.givenGroupId()));
-            GroupBucket abucket = DefaultGroupBucket.createAllGroupBucket(ttb.build());
-            l2floodBuckets.add(abucket);
-        });
+        List<GroupBucket> l2floodBuckets =
+                generateNextGroupBuckets(groupInfos, ALL);
         // create the l2flood group-description to wait for all the
         // l2interface groups to be processed
         GroupDescription l2floodGroupDescription =
                 new DefaultGroupDescription(
                         deviceId,
-                        GroupDescription.Type.ALL,
+                        ALL,
                         new GroupBuckets(l2floodBuckets),
                         l2floodgroupkey,
                         l2floodgroupId,
@@ -750,7 +751,7 @@
                 new DefaultGroupKey(Ofdpa2Pipeline.appKryo.serialize(l3MulticastIndex));
 
         GroupDescription l3MulticastGroupDesc = new DefaultGroupDescription(deviceId,
-                GroupDescription.Type.ALL,
+                ALL,
                 new GroupBuckets(l3McastBuckets),
                 l3MulticastGroupKey,
                 l3MulticastGroupId,
@@ -829,7 +830,7 @@
         GroupDescription l3ecmpGroupDesc =
                 new DefaultGroupDescription(
                         deviceId,
-                        GroupDescription.Type.SELECT,
+                        SELECT,
                         new GroupBuckets(l3ecmpGroupBuckets),
                         l3ecmpGroupKey,
                         l3ecmpGroupId,
@@ -1021,59 +1022,71 @@
         if (nextObjective.type() != NextObjective.Type.HASHED &&
                 nextObjective.type() != NextObjective.Type.BROADCAST) {
             log.warn("AddBuckets not applied to nextType:{} in dev:{} for next:{}",
-                    nextObjective.type(), deviceId, nextObjective.id());
+                     nextObjective.type(), deviceId, nextObjective.id());
             Ofdpa2Pipeline.fail(nextObjective, ObjectiveError.UNSUPPORTED);
             return;
         }
-        if (nextObjective.next().size() > 1) {
-            // FIXME - support editing multiple buckets CORD-555
-            log.warn("Only one bucket can be added at a time");
-            Ofdpa2Pipeline.fail(nextObjective, ObjectiveError.UNSUPPORTED);
-            return;
-        }
+
         // first check to see if bucket being added is not a duplicate of an
         // existing bucket. If it is for an existing outport, then its a duplicate.
-        Set<PortNumber> existingOutPorts = new HashSet<>();
+        Set<TrafficTreatment> duplicateBuckets = Sets.newHashSet();
         List<Deque<GroupKey>> allActiveKeys = Ofdpa2Pipeline.appKryo.deserialize(next.data());
-        for (Deque<GroupKey> gkeys : allActiveKeys) {
-            // get the last group for the outport
-            Group glast = groupService.getGroup(deviceId, gkeys.peekLast());
-            if (glast != null && !glast.buckets().buckets().isEmpty()) {
-                PortNumber op = readOutPortFromTreatment(
-                                    glast.buckets().buckets().get(0).treatment());
-                if (op != null) {
-                    existingOutPorts.add(op);
-                }
+        Set<PortNumber> existingPorts = getExistingOutputPorts(allActiveKeys);
+
+        nextObjective.next().forEach(trafficTreatment -> {
+            PortNumber portNumber = readOutPortFromTreatment(trafficTreatment);
+
+            if (portNumber == null) {
+                return;
             }
+
+            if (existingPorts.contains(portNumber)) {
+                duplicateBuckets.add(trafficTreatment);
+            }
+        });
+
+        if (!duplicateBuckets.isEmpty()) {
+            log.warn("Some buckets {} already exists in next id {}, abort.",
+                     duplicateBuckets, nextObjective.id());
         }
-        // only a single bucket being added
-        TrafficTreatment tt = nextObjective.next().iterator().next();
-        PortNumber newport = readOutPortFromTreatment(tt);
-        if (existingOutPorts.contains(newport)) {
-            log.info("Attempt to add bucket for existing outport:{} in dev:{} for next:{}",
-                     newport, deviceId, nextObjective.id());
-            return;
-        }
+
         if (nextObjective.type() == NextObjective.Type.HASHED) {
-            addBucketToHashGroup(nextObjective, allActiveKeys, newport);
+            addBucketToHashGroup(nextObjective, allActiveKeys);
         } else if (nextObjective.type() == NextObjective.Type.BROADCAST) {
-            addBucketToBroadcastGroup(nextObjective, allActiveKeys, newport);
+            addBucketToBroadcastGroup(nextObjective, allActiveKeys);
         }
     }
 
+    private Set<PortNumber> getExistingOutputPorts(List<Deque<GroupKey>> allActiveKeys) {
+        Set<PortNumber> existingPorts = Sets.newHashSet();
+
+        allActiveKeys.forEach(keyChain -> {
+            GroupKey ifaceGroupKey = keyChain.peekLast();
+            Group ifaceGroup = groupService.getGroup(deviceId, ifaceGroupKey);
+            if (ifaceGroup != null && !ifaceGroup.buckets().buckets().isEmpty()) {
+                 ifaceGroup.buckets().buckets().forEach(bucket -> {
+                     PortNumber portNumber = readOutPortFromTreatment(bucket.treatment());
+
+                     if (portNumber != null) {
+                         existingPorts.add(portNumber);
+                     }
+                 });
+            }
+        });
+
+        return existingPorts;
+    }
+
     private void addBucketToHashGroup(NextObjective nextObjective,
-                                      List<Deque<GroupKey>> allActiveKeys,
-                                      PortNumber newport) {
+                                      List<Deque<GroupKey>> allActiveKeys) {
         // storage for all group keys in the chain of groups created
         List<Deque<GroupKey>> allGroupKeys = new ArrayList<>();
         List<GroupInfo> unsentGroups = new ArrayList<>();
+        List<GroupBucket> newBuckets = Lists.newArrayList();
         createHashBucketChains(nextObjective, allGroupKeys, unsentGroups);
 
-        // now we can create the bucket to add to the outermost L3 ECMP group
-        GroupInfo gi = unsentGroups.get(0); // only one bucket, so only one group-chain
-        TrafficTreatment.Builder ttb = DefaultTrafficTreatment.builder();
-        ttb.group(new DefaultGroupId(gi.nextGroupDesc.givenGroupId()));
-        GroupBucket sbucket = DefaultGroupBucket.createSelectGroupBucket(ttb.build());
+        // now we can create the buckets to add to the outermost L3 ECMP group
+        newBuckets = generateNextGroupBuckets(unsentGroups, SELECT);
 
         // retrieve the original L3 ECMP group
         Group l3ecmpGroup = retrieveTopLevelGroup(allActiveKeys, nextObjective.id());
@@ -1090,12 +1103,14 @@
         GroupDescription l3ecmpGroupDesc =
                 new DefaultGroupDescription(
                         deviceId,
-                        GroupDescription.Type.SELECT,
-                        new GroupBuckets(Collections.singletonList(sbucket)),
+                        SELECT,
+                        new GroupBuckets(newBuckets),
                         l3ecmpGroupKey,
                         l3ecmpGroupId,
                         nextObjective.appId());
-        GroupChainElem l3ecmpGce = new GroupChainElem(l3ecmpGroupDesc, 1, true);
+        GroupChainElem l3ecmpGce = new GroupChainElem(l3ecmpGroupDesc,
+                                                      unsentGroups.size(),
+                                                      true);
 
         // update original NextGroup with new bucket-chain
         // If active keys shows only the top-level group without a chain of groups,
@@ -1108,19 +1123,23 @@
         allActiveKeys.add(newBucketChain);
         updatePendingNextObjective(l3ecmpGroupKey,
                                    new OfdpaNextGroup(allActiveKeys, nextObjective));
+
         log.debug("Adding to L3ECMP: device:{} gid:{} gkey:{} nextId:{}",
                 deviceId, Integer.toHexString(l3ecmpGroupId),
                 l3ecmpGroupKey, nextObjective.id());
-        // send the innermost group
-        log.debug("Sending innermost group {} in group chain on device {} ",
-                Integer.toHexString(gi.innerMostGroupDesc.givenGroupId()), deviceId);
-        updatePendingGroups(gi.nextGroupDesc.appCookie(), l3ecmpGce);
-        groupService.addGroup(gi.innerMostGroupDesc);
+
+        unsentGroups.forEach(groupInfo -> {
+            // send the innermost group
+            log.debug("Sending innermost group {} in group chain on device {} ",
+                      Integer.toHexString(groupInfo.innerMostGroupDesc.givenGroupId()), deviceId);
+            updatePendingGroups(groupInfo.nextGroupDesc.appCookie(), l3ecmpGce);
+            groupService.addGroup(groupInfo.innerMostGroupDesc);
+        });
+
     }
 
     private void addBucketToBroadcastGroup(NextObjective nextObj,
-                                        List<Deque<GroupKey>> allActiveKeys,
-                                        PortNumber newport) {
+                                        List<Deque<GroupKey>> allActiveKeys) {
         VlanId assignedVlan = Ofdpa2Pipeline.readVlanFromSelector(nextObj.meta());
         if (assignedVlan == null) {
             log.warn("VLAN ID required by broadcast next obj is missing. "
@@ -1136,7 +1155,7 @@
         if (ipDst != null) {
             if (ipDst.isMulticast()) {
                 addBucketToL3MulticastGroup(nextObj, allActiveKeys,
-                                            groupInfos, assignedVlan, newport);
+                                            groupInfos, assignedVlan);
             } else {
                 log.warn("Broadcast NextObj with non-multicast IP address {}", nextObj);
                 Ofdpa2Pipeline.fail(nextObj, ObjectiveError.BADPARAMS);
@@ -1144,85 +1163,150 @@
             }
         } else {
             addBucketToL2FloodGroup(nextObj, allActiveKeys,
-                                    groupInfos, assignedVlan, newport);
+                                    groupInfos, assignedVlan);
         }
     }
 
     private void addBucketToL2FloodGroup(NextObjective nextObj,
                                          List<Deque<GroupKey>> allActiveKeys,
                                          List<GroupInfo> groupInfos,
-                                         VlanId assignedVlan,
-                                         PortNumber newport) {
-        // create the bucket to add to the outermost L2 Flood group
-        GroupInfo groupInfo = groupInfos.get(0); // only one bucket to add
-        GroupDescription l2intGrpDesc = groupInfo.nextGroupDesc;
-        TrafficTreatment.Builder ttb = DefaultTrafficTreatment.builder();
-        ttb.group(new DefaultGroupId(l2intGrpDesc.givenGroupId()));
-        GroupBucket abucket = DefaultGroupBucket.createAllGroupBucket(ttb.build());
-        // get the group being edited
-        Group l2floodGroup = retrieveTopLevelGroup(allActiveKeys, nextObj.id());
-        if (l2floodGroup == null) {
+                                         VlanId assignedVlan) {
+        Group l2FloodGroup = retrieveTopLevelGroup(allActiveKeys, nextObj.id());
+
+        if (l2FloodGroup == null) {
+            log.warn("Can't find L2 flood group while adding bucket to it. NextObj = {}",
+                     nextObj);
             Ofdpa2Pipeline.fail(nextObj, ObjectiveError.GROUPMISSING);
             return;
         }
-        GroupKey l2floodGroupKey = l2floodGroup.appCookie();
-        int l2floodGroupId = l2floodGroup.id().id();
 
-        //ensure assignedVlan applies to the chosen group
-        VlanId expectedVlan = VlanId.vlanId((short) ((l2floodGroupId & 0x0fff0000) >> 16));
-        if (!expectedVlan.equals(assignedVlan)) {
-            log.warn("VLAN ID {} does not match Flood group {} to which bucket is "
-                    + "being added, for next:{} in dev:{}. Abort.", assignedVlan,
-                    Integer.toHexString(l2floodGroupId), nextObj.id(), deviceId);
-            Ofdpa2Pipeline.fail(nextObj, ObjectiveError.BADPARAMS);
-        }
-        GroupDescription l2floodGroupDescription =
+        GroupKey l2floodGroupKey = l2FloodGroup.appCookie();
+        int l2floodGroupId = l2FloodGroup.id().id();
+        List<GroupBucket> newBuckets = generateNextGroupBuckets(groupInfos, ALL);
+
+        GroupDescription l2FloodGroupDescription =
                 new DefaultGroupDescription(
                         deviceId,
-                        GroupDescription.Type.ALL,
-                        new GroupBuckets(Collections.singletonList(abucket)),
+                        ALL,
+                        new GroupBuckets(newBuckets),
                         l2floodGroupKey,
                         l2floodGroupId,
                         nextObj.appId());
-        GroupChainElem l2floodGce = new GroupChainElem(l2floodGroupDescription, 1, true);
 
-        // update original NextGroup with new bucket-chain
-        // If active keys shows only the top-level group without a chain of groups,
-        // then it represents an empty group. Update by replacing empty chain.
-        Deque<GroupKey> newBucketChain = new ArrayDeque<>();
-        newBucketChain.addFirst(groupInfo.nextGroupDesc.appCookie());
-        newBucketChain.addFirst(l2floodGroupKey);
-        if (allActiveKeys.size() == 1 && allActiveKeys.get(0).size() == 1) {
-            allActiveKeys.clear();
-        }
-        allActiveKeys.add(newBucketChain);
+        GroupChainElem l2FloodGroupChainElement =
+                new GroupChainElem(l2FloodGroupDescription,
+                                   groupInfos.size(),
+                                   true);
+
         updatePendingNextObjective(l2floodGroupKey,
                                    new OfdpaNextGroup(allActiveKeys, nextObj));
-        log.debug("Adding to L2FLOOD: device:{} gid:{} gkey:{} nextId:{}",
-                  deviceId, Integer.toHexString(l2floodGroupId),
-                  l2floodGroupKey, nextObj.id());
-        // send the innermost group
-        log.debug("Sending innermost group {} in group chain on device {} ",
-                Integer.toHexString(groupInfo.innerMostGroupDesc.givenGroupId()),
-                deviceId);
-        updatePendingGroups(groupInfo.nextGroupDesc.appCookie(), l2floodGce);
-        groupService.addGroup(groupInfo.innerMostGroupDesc);
+
+        //ensure assignedVlan applies to the chosen group
+        VlanId floodGroupVlan = extractVlanIdFromGroupId(l2floodGroupId);
+
+        if (!floodGroupVlan.equals(assignedVlan)) {
+            log.warn("VLAN ID {} does not match Flood group {} to which bucket is "
+                             + "being added, for next:{} in dev:{}. Abort.", assignedVlan,
+                     Integer.toHexString(l2floodGroupId), nextObj.id(), deviceId);
+            Ofdpa2Pipeline.fail(nextObj, ObjectiveError.BADPARAMS);
+            return;
+        }
+
+        groupInfos.forEach(groupInfo -> {
+            // update original NextGroup with new bucket-chain
+            // If active keys shows only the top-level group without a chain of groups,
+            // then it represents an empty group. Update by replacing empty chain.
+            Deque<GroupKey> newBucketChain = new ArrayDeque<>();
+            newBucketChain.addFirst(groupInfo.nextGroupDesc.appCookie());
+            newBucketChain.addFirst(l2floodGroupKey);
+            if (allActiveKeys.size() == 1 && allActiveKeys.get(0).size() == 1) {
+                allActiveKeys.clear();
+            }
+            allActiveKeys.add(newBucketChain);
+
+            log.debug("Adding to L2FLOOD: device:{} gid:{} gkey:{} nextId:{}",
+                      deviceId, Integer.toHexString(l2floodGroupId),
+                      l2floodGroupKey, nextObj.id());
+            // send the innermost group
+            log.debug("Sending innermost group {} in group chain on device {} ",
+                      Integer.toHexString(groupInfo.innerMostGroupDesc.givenGroupId()),
+                      deviceId);
+
+            updatePendingGroups(groupInfo.nextGroupDesc.appCookie(), l2FloodGroupChainElement);
+
+            DeviceId innerMostGroupDevice = groupInfo.innerMostGroupDesc.deviceId();
+            GroupKey innerMostGroupKey = groupInfo.innerMostGroupDesc.appCookie();
+            Group existsL2IGroup = groupService.getGroup(innerMostGroupDevice, innerMostGroupKey);
+
+            if (existsL2IGroup != null) {
+                // group already exist
+                processPendingAddGroupsOrNextObjs(innerMostGroupKey, true);
+            } else {
+                groupService.addGroup(groupInfo.innerMostGroupDesc);
+            }
+
+        });
+    }
+
+    private VlanId extractVlanIdFromGroupId(int groupId) {
+        // Extract the 9th to 20th bit from group id as vlan id.
+        short vlanId = (short) ((groupId & 0x0fff0000) >> 16);
+        return VlanId.vlanId(vlanId);
+    }
+
+    private List<GroupBucket> generateNextGroupBuckets(List<GroupInfo> groupInfos, GroupDescription.Type bucketType) {
+        List<GroupBucket> newBuckets = Lists.newArrayList();
+
+        groupInfos.forEach(groupInfo -> {
+            GroupDescription groupDesc = groupInfo.nextGroupDesc;
+            TrafficTreatment.Builder treatmentBuilder = DefaultTrafficTreatment.builder();
+            treatmentBuilder.group(new DefaultGroupId(groupDesc.givenGroupId()));
+            GroupBucket newBucket = null;
+            switch (bucketType) {
+                case ALL:
+                    newBucket =
+                            DefaultGroupBucket.createAllGroupBucket(treatmentBuilder.build());
+                    break;
+                case INDIRECT:
+                    newBucket =
+                            DefaultGroupBucket.createIndirectGroupBucket(treatmentBuilder.build());
+                    break;
+                case SELECT:
+                    newBucket =
+                            DefaultGroupBucket.createSelectGroupBucket(treatmentBuilder.build());
+                    break;
+                case FAILOVER:
+                    // TODO support failover bucket type
+                default:
+                    log.warn("Unknown bucket type: {}", bucketType);
+                    break;
+            }
+
+            if (newBucket != null) {
+                newBuckets.add(newBucket);
+            }
+
+        });
+
+        return ImmutableList.copyOf(newBuckets);
     }
 
     private void addBucketToL3MulticastGroup(NextObjective nextObj,
                                              List<Deque<GroupKey>> allActiveKeys,
                                              List<GroupInfo> groupInfos,
-                                             VlanId assignedVlan,
-                                             PortNumber newport) {
-        // create the bucket to add to the outermost L3 Multicast group
-        GroupInfo groupInfo = groupInfos.get(0); // only one bucket to add
-        // Points to L3 interface group if there is one.
-        // Otherwise points to L2 interface group directly.
-        GroupDescription nextGroupDesc = (groupInfo.nextGroupDesc != null) ?
-                groupInfo.nextGroupDesc : groupInfo.innerMostGroupDesc;
-        TrafficTreatment.Builder ttb = DefaultTrafficTreatment.builder();
-        ttb.group(new DefaultGroupId(nextGroupDesc.givenGroupId()));
-        GroupBucket abucket = DefaultGroupBucket.createAllGroupBucket(ttb.build());
+                                             VlanId assignedVlan) {
+        // create the buckets to add to the outermost L3 Multicast group
+        List<GroupBucket> newBuckets = Lists.newArrayList();
+        groupInfos.forEach(groupInfo -> {
+            // Points to L3 interface group if there is one.
+            // Otherwise points to L2 interface group directly.
+            GroupDescription nextGroupDesc = (groupInfo.nextGroupDesc != null) ?
+                    groupInfo.nextGroupDesc : groupInfo.innerMostGroupDesc;
+            TrafficTreatment.Builder treatmentBuidler = DefaultTrafficTreatment.builder();
+            treatmentBuidler.group(new DefaultGroupId(nextGroupDesc.givenGroupId()));
+            GroupBucket newBucket = DefaultGroupBucket.createAllGroupBucket(treatmentBuidler.build());
+            newBuckets.add(newBucket);
+        });
 
         // get the group being edited
         Group l3mcastGroup = retrieveTopLevelGroup(allActiveKeys, nextObj.id());
@@ -1234,7 +1318,7 @@
         int l3mcastGroupId = l3mcastGroup.id().id();
 
         //ensure assignedVlan applies to the chosen group
-        VlanId expectedVlan = VlanId.vlanId((short) ((l3mcastGroupId & 0x0fff0000) >> 16));
+        VlanId expectedVlan = extractVlanIdFromGroupId(l3mcastGroupId);
         if (!expectedVlan.equals(assignedVlan)) {
             log.warn("VLAN ID {} does not match L3 Mcast group {} to which bucket is "
                     + "being added, for next:{} in dev:{}. Abort.", assignedVlan,
@@ -1244,46 +1328,53 @@
         GroupDescription l3mcastGroupDescription =
                 new DefaultGroupDescription(
                         deviceId,
-                        GroupDescription.Type.ALL,
-                        new GroupBuckets(Collections.singletonList(abucket)),
+                        ALL,
+                        new GroupBuckets(newBuckets),
                         l3mcastGroupKey,
                         l3mcastGroupId,
                         nextObj.appId());
         GroupChainElem l3mcastGce = new GroupChainElem(l3mcastGroupDescription,
-                                                       1, true);
+                                                       groupInfos.size(), true);
 
-        // update original NextGroup with new bucket-chain
-        Deque<GroupKey> newBucketChain = new ArrayDeque<>();
-        newBucketChain.addFirst(groupInfo.innerMostGroupDesc.appCookie());
-        // Add L3 interface group to the chain if there is one.
-        if (!groupInfo.nextGroupDesc.equals(groupInfo.innerMostGroupDesc)) {
-            newBucketChain.addFirst(groupInfo.nextGroupDesc.appCookie());
-        }
-        newBucketChain.addFirst(l3mcastGroupKey);
-        // If active keys shows only the top-level group without a chain of groups,
-        // then it represents an empty group. Update by replacing empty chain.
-        if (allActiveKeys.size() == 1 && allActiveKeys.get(0).size() == 1) {
-            allActiveKeys.clear();
-        }
-        allActiveKeys.add(newBucketChain);
+        groupInfos.forEach(groupInfo -> {
+            // update original NextGroup with new bucket-chain
+            Deque<GroupKey> newBucketChain = new ArrayDeque<>();
+            newBucketChain.addFirst(groupInfo.innerMostGroupDesc.appCookie());
+            // Add L3 interface group to the chain if there is one.
+            if (!groupInfo.nextGroupDesc.equals(groupInfo.innerMostGroupDesc)) {
+                newBucketChain.addFirst(groupInfo.nextGroupDesc.appCookie());
+            }
+            newBucketChain.addFirst(l3mcastGroupKey);
+            // If active keys shows only the top-level group without a chain of groups,
+            // then it represents an empty group. Update by replacing empty chain.
+            if (allActiveKeys.size() == 1 && allActiveKeys.get(0).size() == 1) {
+                allActiveKeys.clear();
+            }
+            allActiveKeys.add(newBucketChain);
+
+            updatePendingGroups(groupInfo.nextGroupDesc.appCookie(), l3mcastGce);
+            // Point next group to inner-most group, if any
+            if (!groupInfo.nextGroupDesc.equals(groupInfo.innerMostGroupDesc)) {
+                GroupChainElem innerGce = new GroupChainElem(groupInfo.nextGroupDesc,
+                                                             1, false);
+                updatePendingGroups(groupInfo.innerMostGroupDesc.appCookie(), innerGce);
+            }
+            log.debug("Adding to L3MCAST: device:{} gid:{} gkey:{} nextId:{}",
+                      deviceId, Integer.toHexString(l3mcastGroupId),
+                      l3mcastGroupKey, nextObj.id());
+            // send the innermost group
+            log.debug("Sending innermost group {} in group chain on device {} ",
+                      Integer.toHexString(groupInfo.innerMostGroupDesc.givenGroupId()),
+                      deviceId);
+            groupService.addGroup(groupInfo.innerMostGroupDesc);
+
+        });
+
         updatePendingNextObjective(l3mcastGroupKey,
                                    new OfdpaNextGroup(allActiveKeys, nextObj));
 
-        updatePendingGroups(groupInfo.nextGroupDesc.appCookie(), l3mcastGce);
-        // Point next group to inner-most group, if any
-        if (!groupInfo.nextGroupDesc.equals(groupInfo.innerMostGroupDesc)) {
-            GroupChainElem innerGce = new GroupChainElem(groupInfo.nextGroupDesc,
-                    1, false);
-            updatePendingGroups(groupInfo.innerMostGroupDesc.appCookie(), innerGce);
-        }
-        log.debug("Adding to L3MCAST: device:{} gid:{} gkey:{} nextId:{}",
-                  deviceId, Integer.toHexString(l3mcastGroupId),
-                  l3mcastGroupKey, nextObj.id());
-        // send the innermost group
-        log.debug("Sending innermost group {} in group chain on device {} ",
-                Integer.toHexString(groupInfo.innerMostGroupDesc.givenGroupId()),
-                deviceId);
-        groupService.addGroup(groupInfo.innerMostGroupDesc);
+
+
     }
 
     /**
@@ -1302,23 +1393,31 @@
             Ofdpa2Pipeline.fail(nextObjective, ObjectiveError.UNSUPPORTED);
             return;
         }
+        Set<PortNumber> portsToRemove = Sets.newHashSet();
         Collection<TrafficTreatment> treatments = nextObjective.next();
-        TrafficTreatment treatment = treatments.iterator().next();
-        // find the bucket to remove by noting the outport, and figuring out the
-        // top-level group in the group-chain that indirectly references the port
-        PortNumber portToRemove = readOutPortFromTreatment(treatment);
-        if (portToRemove == null) {
+        for (TrafficTreatment treatment : treatments) {
+            // find the bucket to remove by noting the outport, and figuring out the
+            // top-level group in the group-chain that indirectly references the port
+            PortNumber portToRemove = readOutPortFromTreatment(treatment);
+            if (portToRemove == null) {
+                log.warn("treatment {} of next objective {} has no outport.. cannot remove bucket"
+                       + "from group in dev: {}", treatment, nextObjective.id(), deviceId);
+            } else {
+                portsToRemove.add(portToRemove);
+            }
+        }
+
+        if (portsToRemove.isEmpty()) {
             log.warn("next objective {} has no outport.. cannot remove bucket"
-                    + "from group in dev: {}", nextObjective.id(), deviceId);
+                             + "from group in dev: {}", nextObjective.id(), deviceId);
             Ofdpa2Pipeline.fail(nextObjective, ObjectiveError.BADPARAMS);
-            return;
         }
 
         List<Deque<GroupKey>> allActiveKeys = Ofdpa2Pipeline.appKryo.deserialize(next.data());
-        Deque<GroupKey> foundChain = null;
-        int index = 0;
+        List<Deque<GroupKey>> chainsToRemove = Lists.newArrayList();
         for (Deque<GroupKey> gkeys : allActiveKeys) {
             // last group in group chain should have a single bucket pointing to port
+
             GroupKey groupWithPort = gkeys.peekLast();
             Group group = groupService.getGroup(deviceId, groupWithPort);
             if (group == null) {
@@ -1328,49 +1427,77 @@
             }
             PortNumber pout = readOutPortFromTreatment(
                                   group.buckets().buckets().get(0).treatment());
-            if (pout.equals(portToRemove)) {
-                foundChain = gkeys;
-                break;
+            if (portsToRemove.contains(pout)) {
+                chainsToRemove.add(gkeys);
             }
-            index++;
         }
-        if (foundChain == null) {
+
+        if (chainsToRemove.isEmpty()) {
             log.warn("Could not find appropriate group-chain for removing bucket"
                     + " for next id {} in dev:{}", nextObjective.id(), deviceId);
             Ofdpa2Pipeline.fail(nextObjective, ObjectiveError.BADPARAMS);
             return;
         }
 
-        //first groupkey is the one we want to modify
-        GroupKey modGroupKey = foundChain.peekFirst();
+
+        List<GroupBucket> bucketsToRemove = Lists.newArrayList();
+        //first group key is the one we want to modify
+        GroupKey modGroupKey = chainsToRemove.get(0).peekFirst();
         Group modGroup = groupService.getGroup(deviceId, modGroupKey);
-        //second groupkey is the one we wish to remove the reference to
-        GroupKey pointedGroupKey = null;
-        int i = 0;
-        for (GroupKey gk : foundChain) {
-            if (i++ == 1) {
-                pointedGroupKey = gk;
-                break;
+
+        for (Deque<GroupKey> foundChain : chainsToRemove) {
+            //second group key is the one we wish to remove the reference to
+            if (foundChain.size() < 2) {
+                // additional check to make sure second group key exist in
+                // the chain.
+                log.warn("Can't find second group key from chain {}",
+                         foundChain);
+                continue;
             }
+            GroupKey pointedGroupKey = foundChain.stream().collect(Collectors.toList()).get(1);
+
+            Group pointedGroup = groupService.getGroup(deviceId, pointedGroupKey);
+
+            if (pointedGroup == null) {
+                continue;
+            }
+
+            GroupBucket bucket;
+            if (nextObjective.type() == NextObjective.Type.HASHED) {
+                bucket = DefaultGroupBucket.createSelectGroupBucket(
+                        DefaultTrafficTreatment.builder()
+                                .group(pointedGroup.id())
+                                .build());
+            } else {
+                bucket = DefaultGroupBucket.createAllGroupBucket(
+                        DefaultTrafficTreatment.builder()
+                                .group(pointedGroup.id())
+                                .build());
+            }
+
+            bucketsToRemove.add(bucket);
         }
-        Group pointedGroup = groupService.getGroup(deviceId, pointedGroupKey);
-        GroupBucket bucket = null;
-        if (nextObjective.type() == NextObjective.Type.HASHED) {
-            bucket = DefaultGroupBucket.createSelectGroupBucket(
-                                            DefaultTrafficTreatment.builder()
-                                            .group(pointedGroup.id())
-                                            .build());
-        } else {
-            bucket = DefaultGroupBucket.createAllGroupBucket(
-                                            DefaultTrafficTreatment.builder()
-                                            .group(pointedGroup.id())
-                                            .build());
-        }
-        GroupBuckets removeBuckets = new GroupBuckets(Collections
-                                                      .singletonList(bucket));
-        log.debug("Removing buckets from group id 0x{} pointing to group id 0x{} "
+
+        GroupBuckets removeBuckets = new GroupBuckets(bucketsToRemove);
+        List<String> pointedGroupIds; // for debug log
+        pointedGroupIds = bucketsToRemove.stream()
+                .map(GroupBucket::treatment)
+                .map(TrafficTreatment::allInstructions)
+                .flatMap(List::stream)
+                .filter(inst -> inst instanceof Instructions.GroupInstruction)
+                .map(inst -> (Instructions.GroupInstruction) inst)
+                .map(Instructions.GroupInstruction::groupId)
+                .map(GroupId::id)
+                .map(Integer::toHexString)
+                .map(id -> HEX_PREFIX + id)
+                .collect(Collectors.toList());
+
+
+
+        log.debug("Removing buckets from group id 0x{} pointing to group id(s) {} "
                 + "for next id {} in device {}", Integer.toHexString(modGroup.id().id()),
-                Integer.toHexString(pointedGroup.id().id()), nextObjective.id(), deviceId);
+                pointedGroupIds, nextObjective.id(), deviceId);
+        addPendingUpdateNextObjective(modGroupKey, nextObjective);
         groupService.removeBucketsFromGroup(deviceId, modGroupKey,
                                             removeBuckets, modGroupKey,
                                             nextObjective.appId());
@@ -1382,7 +1509,8 @@
             top.add(modGroupKey);
             allActiveKeys.add(top);
         }
-        allActiveKeys.remove(index);
+
+        allActiveKeys.removeAll(chainsToRemove);
         flowObjectiveStore.putNextGroup(nextObjective.id(),
                                         new OfdpaNextGroup(allActiveKeys,
                                                            nextObjective));
@@ -1432,6 +1560,17 @@
         }
     }
 
+    private void addPendingUpdateNextObjective(GroupKey groupKey, NextObjective nextObjective) {
+        pendingUpdateNextObjectives.compute(groupKey, (gKey, nextObjs) -> {
+            if (nextObjs != null) {
+                nextObjs.add(nextObjective);
+            } else {
+                nextObjs = Sets.newHashSet(nextObjective);
+            }
+            return nextObjs;
+        });
+    }
+
     /**
      * Processes next element of a group chain. Assumption is that if this
      * group points to another group, the latter has already been created
@@ -1491,12 +1630,32 @@
                 case GROUP_REMOVED:
                     processPendingRemoveNextObjs(event.subject().appCookie());
                     break;
+                case GROUP_UPDATED:
+                    processPendingUpdateNextObjs(event.subject().appCookie());
+                    break;
                 default:
                     break;
             }
         }
     }
 
+    private void processPendingUpdateNextObjs(GroupKey groupKey) {
+
+        pendingUpdateNextObjectives.compute(groupKey, (gKey, nextObjs) -> {
+            if (nextObjs != null) {
+
+                nextObjs.forEach(nextObj -> {
+                    log.debug("Group {} updated, update pending next objective {}.",
+                              groupKey, nextObj);
+
+                    Ofdpa2Pipeline.pass(nextObj);
+                });
+            }
+
+            return Sets.newHashSet();
+        });
+    }
+
     private void processPendingAddGroupsOrNextObjs(GroupKey key, boolean added) {
         //first check for group chain
         Set<GroupChainElem> gceSet = pendingGroups.remove(key);
@@ -1584,7 +1743,7 @@
 
     private Group retrieveTopLevelGroup(List<Deque<GroupKey>> allActiveKeys,
                                         int nextid) {
-        GroupKey topLevelGroupKey = null;
+        GroupKey topLevelGroupKey;
         if (!allActiveKeys.isEmpty()) {
             topLevelGroupKey = allActiveKeys.get(0).peekFirst();
         } else {