@VisibleForTesting
  static <K, V> Cache<K, V> forMaximumBytes(long maximumBytes) {
    // We specifically use Guava cache since it allows for recursive computeIfAbsent calls
    // preventing deadlock from occurring when a loading function mutates the underlying cache
    LongAdder weightInBytes = new LongAdder();
    return new SubCache<>(
        new ShrinkOnEviction(
                CacheBuilder.newBuilder()
                    .maximumWeight(maximumBytes >> WEIGHT_RATIO)
                    .weigher(
                        new Weigher<CompositeKey, WeightedValue<Object>>() {

                          @Override
                          public int weigh(CompositeKey key, WeightedValue<Object> value) {
                            // Round up to the next closest multiple of WEIGHT_RATIO
                            long size =
                                ((key.getWeight() + value.getWeight() - 1) >> WEIGHT_RATIO) + 1;
                            if (size > Integer.MAX_VALUE) {
                              LOG.warn(
                                  "Entry with size {} MiBs inserted into the cache. This is larger than the maximum individual entry size of {} MiBs. The cache will under report its memory usage by the difference. This may lead to OutOfMemoryErrors.",
                                  ((size - 1) >> 20) + 1,
                                  2 << (WEIGHT_RATIO + 10));
                              return Integer.MAX_VALUE;
                            }
                            return (int) size;
                          }
                        })
                    // The maximum size of an entry in the cache is maxWeight / concurrencyLevel
                    // which is why we set the concurrency level to 1. See
                    // https://github.com/google/guava/issues/3462 for further details.
                    //
                    // The PrecombineGroupingTable showed contention here since it was working in
                    // a tight loop. We were able to resolve the contention by reducing the
                    // frequency of updates. Reconsider this value if we could solve the maximum
                    // entry size issue. Note that using Runtime.getRuntime().availableProcessors()
                    // is subject to docker CPU shares issues
                    // (https://bugs.openjdk.org/browse/JDK-8281181).
                    //
                    // We could revisit the caffeine cache library based upon reinvestigating
                    // recursive computeIfAbsent calls since it doesn't have this limit.
                    .concurrencyLevel(1)
                    .recordStats(),
                weightInBytes)
            .getCache(),
        CompositeKeyPrefix.ROOT,
        maximumBytes,
        weightInBytes);
  }
@Test
  public void testShrinkableIsShrunk() throws Exception {
    WeightedValue<String> shrinkableKey = WeightedValue.of("shrinkable", MB);
    Shrinkable<Object> shrinkable =
        new Shrinkable<Object>() {

          @Override
          public Object shrink() {
            return WeightedValue.of("wasShrunk", 1);
          }
        };

    Cache<Object, Object> cache = Caches.forMaximumBytes(2 * MB);
    cache.put(shrinkableKey, WeightedValue.of(shrinkable, MB));
    // Check that we didn't evict it yet
    assertSame(shrinkable, cache.peek(shrinkableKey));

    // The next insertion should cause the value to be "shrunk"
    cache.put(WeightedValue.of("other", 1), WeightedValue.of("value", 1));
    assertEquals("wasShrunk", cache.peek(shrinkableKey));
  }