reliably forcing Guava map eviction to take place

后端 未结 7 1490
夕颜
夕颜 2021-01-03 21:04

EDIT: I\'ve reorganized this question to reflect the new information that since became available.

This question is based on the responses to a quest

相关标签:
7条回答
  • 2021-01-03 21:53

    Based on maaartinus's answer, I came up with the following code which uses reflection rather than directly modifying the source (If you find this useful please upvote his answer!). While it will come at a performance penalty for using reflection, the difference should be negligible since I'll run it about once every 20 minutes for each caching Map (I'm also caching the dynamic lookups in the static block which will help). I have done some initial testing and it appears to work as intended:

    public class GuavaEvictionHacker {
    
       //Class objects necessary for reflection on Guava classes - see Guava docs for info
       private static final Class<?> computingMapAdapterClass;
       private static final Class<?> nullConcurrentMapClass;
       private static final Class<?> nullComputingConcurrentMapClass;
       private static final Class<?> customConcurrentHashMapClass;
       private static final Class<?> computingConcurrentHashMapClass;
       private static final Class<?> segmentClass;
    
       //MapMaker$ComputingMapAdapter#cache points to the wrapped CustomConcurrentHashMap
       private static final Field cacheField;
    
       //CustomConcurrentHashMap#segments points to the array of Segments (map partitions)
       private static final Field segmentsField;
    
       //CustomConcurrentHashMap$Segment#runCleanup() enforces eviction on the calling Segment
       private static final Method runCleanupMethod;
    
       static {
          try {
    
             //look up Classes
             computingMapAdapterClass = Class.forName("com.google.common.collect.MapMaker$ComputingMapAdapter");
             nullConcurrentMapClass = Class.forName("com.google.common.collect.MapMaker$NullConcurrentMap");
             nullComputingConcurrentMapClass = Class.forName("com.google.common.collect.MapMaker$NullComputingConcurrentMap");
             customConcurrentHashMapClass = Class.forName("com.google.common.collect.CustomConcurrentHashMap");
             computingConcurrentHashMapClass = Class.forName("com.google.common.collect.ComputingConcurrentHashMap");
             segmentClass = Class.forName("com.google.common.collect.CustomConcurrentHashMap$Segment");
    
             //look up Fields and set accessible
             cacheField = computingMapAdapterClass.getDeclaredField("cache");
             segmentsField = customConcurrentHashMapClass.getDeclaredField("segments");
             cacheField.setAccessible(true);
             segmentsField.setAccessible(true);
    
             //look up the cleanup Method and set accessible
             runCleanupMethod = segmentClass.getDeclaredMethod("runCleanup");
             runCleanupMethod.setAccessible(true);
          }
          catch (ClassNotFoundException cnfe) {
             throw new RuntimeException("ClassNotFoundException thrown in GuavaEvictionHacker static initialization block.", cnfe);
          }
          catch (NoSuchFieldException nsfe) {
             throw new RuntimeException("NoSuchFieldException thrown in GuavaEvictionHacker static initialization block.", nsfe);
          }
          catch (NoSuchMethodException nsme) {
             throw new RuntimeException("NoSuchMethodException thrown in GuavaEvictionHacker static initialization block.", nsme);
          }
       }
    
       /**
        * Forces eviction to take place on the provided Guava Map. The Map must be an instance
        * of either {@code CustomConcurrentHashMap} or {@code MapMaker$ComputingMapAdapter}.
        * 
        * @param guavaMap the Guava Map to force eviction on.
        */
       public static void forceEvictionOnGuavaMap(ConcurrentMap<?, ?> guavaMap) {
    
          try {
    
             //we need to get the CustomConcurrentHashMap instance
             Object customConcurrentHashMap;
    
             //get the type of what was passed in
             Class<?> guavaMapClass = guavaMap.getClass();
    
             //if it's a CustomConcurrentHashMap we have what we need
             if (guavaMapClass == customConcurrentHashMapClass) {
                customConcurrentHashMap = guavaMap;
             }
             //if it's a NullConcurrentMap (auto-evictor), return early
             else if (guavaMapClass == nullConcurrentMapClass) {
                return;
             }
             //if it's a computing map we need to pull the instance from the adapter's "cache" field
             else if (guavaMapClass == computingMapAdapterClass) {
                customConcurrentHashMap = cacheField.get(guavaMap);
                //get the type of what we pulled out
                Class<?> innerCacheClass = customConcurrentHashMap.getClass();
                //if it's a NullComputingConcurrentMap (auto-evictor), return early
                if (innerCacheClass == nullComputingConcurrentMapClass) {
                   return;
                }
                //otherwise make sure it's a ComputingConcurrentHashMap - error if it isn't
                else if (innerCacheClass != computingConcurrentHashMapClass) {
                   throw new IllegalArgumentException("Provided ComputingMapAdapter's inner cache was an unexpected type: " + innerCacheClass);
                }
             }
             //error for anything else passed in
             else {
                throw new IllegalArgumentException("Provided ConcurrentMap was not an expected Guava Map: " + guavaMapClass);
             }
    
             //pull the array of Segments out of the CustomConcurrentHashMap instance
             Object[] segments = (Object[])segmentsField.get(customConcurrentHashMap);
    
             //loop over them and invoke the cleanup method on each one
             for (Object segment : segments) {
                runCleanupMethod.invoke(segment);
             }
          }
          catch (IllegalAccessException iae) {
             throw new RuntimeException(iae);
          }
          catch (InvocationTargetException ite) {
             throw new RuntimeException(ite.getCause());
          }
       }
    }
    

    I'm looking for feedback on whether this approach is advisable as a stopgap until the issue is resolved in a Guava release, particularly from members of the Guava team when they get a minute.

    EDIT: updated the solution to allow for auto-evicting maps (NullConcurrentMap or NullComputingConcurrentMap residing in a ComputingMapAdapter). This turned out to be necessary in my case, since I'm calling this method on all of my maps and a few of them are auto-evictors.

    0 讨论(0)
提交回复
热议问题