SLING-11339: resource resolver: add temporary vanity path cache for aysnc startup phase (#68)

* wip: run query in BG (causes test failures due to bad test assumptions)

* Revert "wip: run query in BG (causes test failures due to bad test assumptions)"

This reverts commit e90984c2b08279c330a0af84eb1fd194e85af608.

* SLING-11330: resource resolver: refactor event listening - structure onChange method

* Revert "SLING-11330: resource resolver: refactor event listening - structure onChange method"

This reverts commit 12b77dc7554a7d3b8b676f661d7b3800a5d16c94.

* SLING-11339: resource resolver: add temporary vanity path cache for aysnc startup phase

* SLING-11339: resource resolver: add temporary vanity path cache for aysnc startup phase - cleanup
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
index e104fd2..0969718 100644
--- a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
@@ -18,6 +18,7 @@
  */
 package org.apache.sling.resourceresolver.impl.mapping;
 
+import org.apache.commons.collections4.map.LRUMap;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.sling.api.SlingConstants;
 import org.apache.sling.api.resource.LoginException;
@@ -58,13 +59,13 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.NoSuchElementException;
 import java.util.Optional;
 import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
 import java.util.TreeSet;
-import java.util.Map.Entry;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -129,7 +130,11 @@
 
     private Map<String, List<MapEntry>> resolveMapsMap;
 
+    // Temporary cache for use while doing async vanity path query
+    private Map<String, List<MapEntry>> temporaryResolveMapsMap;
     private List<Map.Entry<String, ResourceChange.ChangeType>> resourceChangeQueue;
+    private AtomicLong temporaryResolveMapsMapHits = new AtomicLong();
+    private AtomicLong temporaryResolveMapsMapMisses = new AtomicLong();
 
     private Collection<MapEntry> mapMaps;
 
@@ -260,7 +265,7 @@
                     Thread vpinit = new Thread(vpi, "VanityPathInitializer");
                     vpinit.start();
                 } else {
-                    vpi.load();
+                    vpi.run();
                 }
             }
         } finally {
@@ -270,6 +275,8 @@
 
     private class VanityPathInitializer implements Runnable {
 
+        private int SIZELIMIT = 10000;
+
         private MapConfigurationProvider factory;
 
         public VanityPathInitializer(MapConfigurationProvider factory) {
@@ -278,10 +285,7 @@
 
         @Override
         public void run() {
-            execute();
-        }
-
-        public void load() {
+            temporaryResolveMapsMap = Collections.synchronizedMap(new LRUMap<>(SIZELIMIT));
             execute();
         }
 
@@ -331,6 +335,10 @@
                 log.debug("vanity path init - end");
             } catch (LoginException ex) {
                 log.error("VanityPath init failed", ex);
+            } finally {
+                log.debug("dropping temporary resolver map - {}/{} entries, {} hits, {} misses", temporaryResolveMapsMap.size(),
+                        SIZELIMIT, temporaryResolveMapsMapHits.get(), temporaryResolveMapsMapMisses.get());
+                temporaryResolveMapsMap = null;
             }
         }
     }
@@ -692,6 +700,9 @@
         return Collections.unmodifiableMap(vanityTargets);
     }
 
+    // special singleton entry for negative cache entries
+    private static final List<MapEntry> NO_MAP_ENTRIES = Collections.emptyList();
+
     /**
      * get the MapEntry list containing all the nodes having a specific vanityPath
      */
@@ -701,12 +712,27 @@
         if (!vanityPathsProcessed.get() || BloomFilterUtils.probablyContains(vanityBloomFilter, vanityPath)) {
             mapEntries = this.resolveMapsMap.get(vanityPath);
             if (mapEntries == null) {
-                Map<String, List<MapEntry>> mapEntry = getVanityPaths(vanityPath);
-                mapEntries = mapEntry.get(vanityPath);
+                if (!vanityPathsProcessed.get() && temporaryResolveMapsMap != null) {
+                    mapEntries = temporaryResolveMapsMap.get(vanityPath);
+                    if (mapEntries != null) {
+                        temporaryResolveMapsMapHits.incrementAndGet();
+                        log.trace("getMapEntryList: using temp map entries for {} -> {}", vanityPath, mapEntries);
+                    } else {
+                        temporaryResolveMapsMapMisses.incrementAndGet();
+                    }
+                }
+                if (mapEntries == null) {
+                    Map<String, List<MapEntry>> mapEntry = getVanityPaths(vanityPath);
+                    mapEntries = mapEntry.get(vanityPath);
+                    if (!vanityPathsProcessed.get() && temporaryResolveMapsMap != null) {
+                        log.trace("getMapEntryList: caching map entries for {} -> {}", vanityPath, mapEntries);
+                        temporaryResolveMapsMap.put(vanityPath, mapEntries == null ? NO_MAP_ENTRIES : mapEntries);
+                    }
+                }
             }
         }
 
-        return mapEntries;
+        return mapEntries == NO_MAP_ENTRIES ? null : mapEntries;
     }
 
     /**