Ensure ApplicationService::uninstall returns only after the features have been uninstalled

Change-Id: Iff9c1c0196bc8616858589c67ce4c876e99a8120
diff --git a/core/net/src/main/java/org/onosproject/app/impl/ApplicationManager.java b/core/net/src/main/java/org/onosproject/app/impl/ApplicationManager.java
index 8735b8c..a6297a6 100644
--- a/core/net/src/main/java/org/onosproject/app/impl/ApplicationManager.java
+++ b/core/net/src/main/java/org/onosproject/app/impl/ApplicationManager.java
@@ -15,8 +15,12 @@
  */
 package org.onosproject.app.impl;
 
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Multimap;
+import com.google.common.util.concurrent.Uninterruptibles;
+
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
@@ -41,6 +45,8 @@
 
 import java.io.InputStream;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static org.onosproject.app.ApplicationEvent.Type.APP_ACTIVATED;
@@ -63,6 +69,7 @@
     private final Logger log = getLogger(getClass());
 
     private static final String APP_ID_NULL = "Application ID cannot be null";
+    private static final long DEFAULT_OPERATION_TIMEOUT_MILLIS = 2000;
 
     private final ApplicationStoreDelegate delegate = new InternalStoreDelegate();
 
@@ -76,6 +83,10 @@
 
     // Application supplied hooks for pre-activation processing.
     private final Multimap<String, Runnable> deactivateHooks = HashMultimap.create();
+    private final Cache<ApplicationId, CountDownLatch> pendingUninstalls =
+            CacheBuilder.newBuilder()
+                        .expireAfterWrite(DEFAULT_OPERATION_TIMEOUT_MILLIS * 2, TimeUnit.MILLISECONDS)
+                        .build();
 
     @Activate
     public void activate() {
@@ -148,11 +159,16 @@
     @Override
     public void uninstall(ApplicationId appId) {
         checkNotNull(appId, APP_ID_NULL);
+        CountDownLatch latch = new CountDownLatch(1);
         try {
+            pendingUninstalls.put(appId, latch);
             store.remove(appId);
         } catch (Exception e) {
+            pendingUninstalls.invalidate(appId);
+            latch.countDown();
             log.warn("Unable to purge application directory for {}", appId.name());
         }
+        Uninterruptibles.awaitUninterruptibly(latch, DEFAULT_OPERATION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
     }
 
     @Override
@@ -182,6 +198,7 @@
         public void notify(ApplicationEvent event) {
             ApplicationEvent.Type type = event.type();
             Application app = event.subject();
+            CountDownLatch latch = pendingUninstalls.getIfPresent(app.id());
             try {
                 if (type == APP_ACTIVATED) {
                     if (installAppFeatures(app)) {
@@ -205,9 +222,13 @@
 
                 }
                 post(event);
-
             } catch (Exception e) {
                 log.warn("Unable to perform operation on application " + app.id().name(), e);
+            } finally {
+                if (latch != null) {
+                    latch.countDown();
+                    pendingUninstalls.invalidate(app.id());
+                }
             }
         }
     }